@ethlete/core 5.0.0-next.4 → 5.0.0-next.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/fesm2022/ethlete-core.mjs +1383 -1074
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ethlete-core.d.ts +236 -111
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, ElementRef, Directive, assertInInjectionContext, DestroyRef, TemplateRef, QueryList, NgZone, signal, isSignal, untracked,
|
|
3
|
-
import { takeUntilDestroyed,
|
|
4
|
-
import { Subject, BehaviorSubject, debounceTime, combineLatest, map, merge, fromEvent, filter, tap, switchMap, startWith, of, timer, takeUntil,
|
|
2
|
+
import { InjectionToken, inject, ElementRef, Directive, assertInInjectionContext, DestroyRef, TemplateRef, QueryList, NgZone, signal, isSignal, untracked, DOCUMENT, linkedSignal, isDevMode, computed, afterNextRender, effect, Injector, booleanAttribute, numberAttribute, runInInjectionContext, RendererStyleFlags2, PLATFORM_ID, ApplicationRef, RendererFactory2, model, ViewContainerRef, input, output, Pipe, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { takeUntilDestroyed, toSignal, toObservable, outputFromObservable } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { Subject, BehaviorSubject, debounceTime, combineLatest, map, merge, fromEvent, filter, tap, switchMap, startWith, of, timer, takeUntil, pairwise, distinctUntilChanged, take, Observable } from 'rxjs';
|
|
5
5
|
import { FormGroup, FormArray, FormControl } from '@angular/forms';
|
|
6
6
|
import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals';
|
|
7
|
-
import { coerceElement } from '@angular/cdk/coercion';
|
|
8
7
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
|
8
|
+
import { coerceElement } from '@angular/cdk/coercion';
|
|
9
9
|
import { isPlatformBrowser, DOCUMENT as DOCUMENT$1 } from '@angular/common';
|
|
10
10
|
import { Router, NavigationEnd, NavigationSkipped } from '@angular/router';
|
|
11
11
|
import { Overlay } from '@angular/cdk/overlay';
|
|
@@ -710,41 +710,82 @@ const easeOutBackStrong = (t) => {
|
|
|
710
710
|
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
|
711
711
|
};
|
|
712
712
|
|
|
713
|
-
const
|
|
714
|
-
const getRawValueSafe = (ctrl) => {
|
|
715
|
-
try {
|
|
716
|
-
return isSignal(ctrl) ? (ctrl()?.getRawValue() ?? null) : (ctrl?.getRawValue() ?? null);
|
|
717
|
-
}
|
|
718
|
-
catch {
|
|
719
|
-
// Ignore errors. This can happen if the passed control is a required input and is not yet initialized.
|
|
720
|
-
return null;
|
|
721
|
-
}
|
|
722
|
-
};
|
|
723
|
-
const initialValue = getRawValueSafe(control);
|
|
724
|
-
const controlStream = isSignal(control)
|
|
725
|
-
? toObservable(control)
|
|
726
|
-
: of(control);
|
|
727
|
-
const controlObs = controlStream.pipe(switchMap((ctrl) => {
|
|
728
|
-
if (!ctrl)
|
|
729
|
-
return of(null);
|
|
730
|
-
const vcsObs = options?.debounceTime
|
|
731
|
-
? ctrl.valueChanges.pipe(debounceTime(options.debounceTime))
|
|
732
|
-
: ctrl.valueChanges;
|
|
733
|
-
return vcsObs.pipe(startWith(ctrl.getRawValue()), map(() => ctrl.getRawValue()));
|
|
734
|
-
}));
|
|
735
|
-
const obs = !options?.debounceFirst ? merge(of(initialValue), controlObs) : controlObs;
|
|
736
|
-
return toSignal(obs.pipe(distinctUntilChanged((a, b) => equal(a, b))), {
|
|
737
|
-
initialValue,
|
|
738
|
-
});
|
|
739
|
-
};
|
|
713
|
+
const BREAKPOINT_ORDER = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
|
|
740
714
|
/**
|
|
741
|
-
*
|
|
715
|
+
* Default viewport config based on Tailwind CSS.
|
|
716
|
+
* @see https://tailwindcss.com/docs/screens
|
|
742
717
|
*/
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
718
|
+
const DEFAULT_VIEWPORT_CONFIG = {
|
|
719
|
+
breakpoints: {
|
|
720
|
+
xs: [0, 639],
|
|
721
|
+
sm: [640, 767],
|
|
722
|
+
md: [768, 1023],
|
|
723
|
+
lg: [1024, 1279],
|
|
724
|
+
xl: [1280, 1535],
|
|
725
|
+
'2xl': [1536, Infinity],
|
|
726
|
+
},
|
|
747
727
|
};
|
|
728
|
+
const [provideViewportConfig, injectViewportConfig] = createStaticRootProvider(DEFAULT_VIEWPORT_CONFIG, {
|
|
729
|
+
name: 'Viewport Config',
|
|
730
|
+
});
|
|
731
|
+
const [provideBreakpointObserver, injectBreakpointObserver] = createRootProvider(() => {
|
|
732
|
+
const breakpointObserver = inject(BreakpointObserver);
|
|
733
|
+
const viewportConfig = injectViewportConfig();
|
|
734
|
+
const isMediaQueryMatched = (mediaQuery) => breakpointObserver.isMatched(mediaQuery);
|
|
735
|
+
const observeMediaQuery = (mediaQuery) => {
|
|
736
|
+
return toSignal(breakpointObserver.observe(mediaQuery).pipe(map((x) => x.matches), startWith(isMediaQueryMatched(mediaQuery))), { requireSync: true });
|
|
737
|
+
};
|
|
738
|
+
const observeBreakpoint = (options) => observeMediaQuery(buildMediaQueryString(options));
|
|
739
|
+
const isBreakpointMatched = (options) => isMediaQueryMatched(buildMediaQueryString(options));
|
|
740
|
+
const getBreakpointSize = (type, option) => {
|
|
741
|
+
const index = option === 'min' ? 0 : 1;
|
|
742
|
+
const size = viewportConfig.breakpoints[type][index];
|
|
743
|
+
if (size === Infinity || size === 0) {
|
|
744
|
+
return size;
|
|
745
|
+
}
|
|
746
|
+
if (option === 'min') {
|
|
747
|
+
return size;
|
|
748
|
+
}
|
|
749
|
+
// Due to scaling, the actual size of the viewport may be a decimal number.
|
|
750
|
+
// Eg. on Windows 11 with 150% scaling, the viewport size may be 1535.33px
|
|
751
|
+
// and thus not matching any of the default breakpoints.
|
|
752
|
+
return size + 0.9;
|
|
753
|
+
};
|
|
754
|
+
const buildMediaQueryString = (options) => {
|
|
755
|
+
if (!options.min && !options.max) {
|
|
756
|
+
throw new Error('At least one of min or max must be defined');
|
|
757
|
+
}
|
|
758
|
+
const mediaQueryParts = [];
|
|
759
|
+
if (options.min) {
|
|
760
|
+
if (typeof options.min === 'number') {
|
|
761
|
+
mediaQueryParts.push(`(min-width: ${options.min}px)`);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
mediaQueryParts.push(`(min-width: ${getBreakpointSize(options.min, 'min')}px)`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (options.min && options.max) {
|
|
768
|
+
mediaQueryParts.push('and');
|
|
769
|
+
}
|
|
770
|
+
if (options.max) {
|
|
771
|
+
if (typeof options.max === 'number') {
|
|
772
|
+
mediaQueryParts.push(`(max-width: ${options.max}px)`);
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
mediaQueryParts.push(`(max-width: ${getBreakpointSize(options.max, 'max')}px)`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return mediaQueryParts.join(' ');
|
|
779
|
+
};
|
|
780
|
+
return {
|
|
781
|
+
observeBreakpoint,
|
|
782
|
+
isBreakpointMatched,
|
|
783
|
+
getBreakpointSize,
|
|
784
|
+
buildMediaQueryString,
|
|
785
|
+
observeMediaQuery,
|
|
786
|
+
isMediaQueryMatched,
|
|
787
|
+
};
|
|
788
|
+
}, { name: 'Breakpoint Observer' });
|
|
748
789
|
|
|
749
790
|
const isElementSignal = (el) => {
|
|
750
791
|
if (!isSignal(el))
|
|
@@ -886,698 +927,1127 @@ const createCanAnimateSignal = () => {
|
|
|
886
927
|
};
|
|
887
928
|
};
|
|
888
929
|
|
|
889
|
-
const
|
|
930
|
+
const boundingClientRectToElementRect = (rect) => ({
|
|
931
|
+
bottom: rect.bottom,
|
|
932
|
+
height: rect.height,
|
|
933
|
+
left: rect.left,
|
|
934
|
+
right: rect.right,
|
|
935
|
+
top: rect.top,
|
|
936
|
+
width: rect.width,
|
|
937
|
+
x: rect.x,
|
|
938
|
+
y: rect.y,
|
|
939
|
+
});
|
|
940
|
+
const createElementDimensions = (el, rect) => {
|
|
941
|
+
if (!el) {
|
|
942
|
+
return {
|
|
943
|
+
rect: null,
|
|
944
|
+
client: null,
|
|
945
|
+
scroll: null,
|
|
946
|
+
offset: null,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const cachedNormalizedRect = rect ? boundingClientRectToElementRect(rect) : null;
|
|
950
|
+
const rectFn = () => cachedNormalizedRect ? cachedNormalizedRect : boundingClientRectToElementRect(el.getBoundingClientRect());
|
|
951
|
+
return {
|
|
952
|
+
rect: rectFn,
|
|
953
|
+
client: { width: el.clientWidth, height: el.clientHeight },
|
|
954
|
+
scroll: { width: el.scrollWidth, height: el.scrollHeight },
|
|
955
|
+
offset: { width: el.offsetWidth, height: el.offsetHeight },
|
|
956
|
+
};
|
|
957
|
+
};
|
|
958
|
+
const signalElementDimensions = (el) => {
|
|
890
959
|
const destroyRef = inject(DestroyRef);
|
|
891
960
|
const elements = buildElementSignal(el);
|
|
892
961
|
const firstEl = firstElementSignal(elements);
|
|
893
962
|
const zone = inject(NgZone);
|
|
894
963
|
const isRendered = signalIsRendered();
|
|
895
|
-
const
|
|
896
|
-
const
|
|
964
|
+
const initialValue = () => createElementDimensions(firstEl().currentElement);
|
|
965
|
+
const elementDimensionsSignal = signal(initialValue(), ...(ngDevMode ? [{ debugName: "elementDimensionsSignal" }] : []));
|
|
966
|
+
const observer = new ResizeObserver((e) => {
|
|
897
967
|
if (!isRendered())
|
|
898
968
|
return;
|
|
899
969
|
const entry = e[0];
|
|
900
970
|
if (entry) {
|
|
901
|
-
|
|
971
|
+
const target = entry.target;
|
|
972
|
+
const newDimensions = createElementDimensions(target);
|
|
973
|
+
zone.run(() => elementDimensionsSignal.set(newDimensions));
|
|
902
974
|
}
|
|
903
975
|
});
|
|
904
976
|
effect(() => {
|
|
905
977
|
const els = firstEl();
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
978
|
+
untracked(() => {
|
|
979
|
+
elementDimensionsSignal.set(initialValue());
|
|
980
|
+
if (els.previousElement) {
|
|
981
|
+
observer.disconnect();
|
|
982
|
+
}
|
|
983
|
+
if (els.currentElement) {
|
|
984
|
+
const computedDisplay = getComputedStyle(els.currentElement).display;
|
|
985
|
+
const currentElIsAngularComponent = els.currentElement?.tagName.toLowerCase().includes('-');
|
|
986
|
+
if (computedDisplay === 'inline' && isDevMode() && currentElIsAngularComponent) {
|
|
987
|
+
console.error(`Element <${els.currentElement?.tagName.toLowerCase()}> is an Angular component and has a display of 'inline'. Inline elements cannot be observed for dimensions. Please change it to 'block' or something else.`);
|
|
988
|
+
}
|
|
989
|
+
observer.observe(els.currentElement);
|
|
990
|
+
}
|
|
991
|
+
});
|
|
913
992
|
});
|
|
914
993
|
destroyRef.onDestroy(() => observer.disconnect());
|
|
915
|
-
return
|
|
994
|
+
return computed(() => elementDimensionsSignal(), {
|
|
995
|
+
equal: (a, b) => equal(a, b),
|
|
996
|
+
});
|
|
916
997
|
};
|
|
917
|
-
const
|
|
998
|
+
const signalHostElementDimensions = () => signalElementDimensions(inject(ElementRef));
|
|
918
999
|
|
|
919
|
-
const
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
const isRendered = signalIsRendered();
|
|
923
|
-
const elementMutations = signalElementMutations(elements, { childList: true, subtree: true, attributes: true });
|
|
924
|
-
return computed(() => {
|
|
925
|
-
if (!isRendered())
|
|
926
|
-
return [];
|
|
927
|
-
const els = firstEl();
|
|
928
|
-
// We are not interested what the mutation is, just that there is one.
|
|
929
|
-
// Changes to the DOM may affect the children of the element.
|
|
930
|
-
elementMutations();
|
|
931
|
-
if (!els.currentElement)
|
|
932
|
-
return [];
|
|
933
|
-
const children = [];
|
|
934
|
-
for (let index = 0; index < els.currentElement.children.length; index++) {
|
|
935
|
-
const element = els.currentElement.children[index];
|
|
936
|
-
if (element instanceof HTMLElement) {
|
|
937
|
-
children.push(element);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
return children;
|
|
941
|
-
}, { equal: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) });
|
|
1000
|
+
const previousSignalValue = (signal) => {
|
|
1001
|
+
const obs = toObservable(signal).pipe(pairwise(), map(([prev]) => prev));
|
|
1002
|
+
return toSignal(obs);
|
|
942
1003
|
};
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
if (currentElements.includes(previousEl))
|
|
951
|
-
continue;
|
|
952
|
-
const tokens = Object.keys(config.tokenMap)
|
|
953
|
-
.map((key) => key.split(' '))
|
|
954
|
-
.flat();
|
|
955
|
-
if (!tokens.length)
|
|
956
|
-
continue;
|
|
957
|
-
config.cleanupFn(previousEl, tokens);
|
|
1004
|
+
const syncSignal = (from, to, options) => {
|
|
1005
|
+
let isFirstRun = options?.skipSyncRead ? false : true;
|
|
1006
|
+
if (!options?.skipSyncRead) {
|
|
1007
|
+
try {
|
|
1008
|
+
// this might throw if the signal is not yet initialized (eg. a required signal input inside the constructor)
|
|
1009
|
+
// in that case we just skip the initial sync
|
|
1010
|
+
to.set(from());
|
|
958
1011
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
untracked(() => {
|
|
964
|
-
const tokenArray = tokens.split(' ');
|
|
965
|
-
if (!tokenArray.length)
|
|
966
|
-
return;
|
|
967
|
-
config.updateFn(currentEl, tokenArray, condition());
|
|
968
|
-
});
|
|
1012
|
+
catch {
|
|
1013
|
+
isFirstRun = false;
|
|
1014
|
+
if (isDevMode()) {
|
|
1015
|
+
console.warn('Failed to sync signals. The target signal is not yet initialized.', { from, to });
|
|
969
1016
|
}
|
|
970
1017
|
}
|
|
971
|
-
}
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1018
|
+
}
|
|
1019
|
+
const ref = effect(() => {
|
|
1020
|
+
const formVal = from();
|
|
1021
|
+
if (options?.skipFirstRun && isFirstRun) {
|
|
1022
|
+
isFirstRun = false;
|
|
976
1023
|
return;
|
|
977
|
-
runInInjectionContext(injector, () => {
|
|
978
|
-
effects[tokens] = effect(() => {
|
|
979
|
-
const { currentElements } = untracked(() => elements());
|
|
980
|
-
const value = signal();
|
|
981
|
-
for (const el of currentElements) {
|
|
982
|
-
const tokenArray = tokens.split(' ');
|
|
983
|
-
if (!tokenArray.length)
|
|
984
|
-
continue;
|
|
985
|
-
config.updateFn(el, tokenArray, value);
|
|
986
|
-
}
|
|
987
|
-
});
|
|
988
|
-
});
|
|
989
|
-
};
|
|
990
|
-
const pushMany = (map) => {
|
|
991
|
-
for (const [tokens, signal] of Object.entries(map)) {
|
|
992
|
-
push(tokens, signal);
|
|
993
1024
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1025
|
+
untracked(() => {
|
|
1026
|
+
to.set(formVal);
|
|
1027
|
+
});
|
|
1028
|
+
}, ...(ngDevMode ? [{ debugName: "ref" }] : []));
|
|
1029
|
+
return ref;
|
|
1030
|
+
};
|
|
1031
|
+
const maybeSignalValue = (value) => {
|
|
1032
|
+
if (isSignal(value)) {
|
|
1033
|
+
return value();
|
|
1034
|
+
}
|
|
1035
|
+
return value;
|
|
1036
|
+
};
|
|
1037
|
+
/**
|
|
1038
|
+
* A computed that will only be reactive until the source signal contains a truthy value.
|
|
1039
|
+
* All subsequent changes inside the computation will be ignored.
|
|
1040
|
+
*/
|
|
1041
|
+
const computedTillTruthy = (source) => {
|
|
1042
|
+
const value = signal(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1043
|
+
const ref = effect(() => {
|
|
1044
|
+
const val = source();
|
|
1045
|
+
if (val) {
|
|
1046
|
+
value.set(val);
|
|
1047
|
+
ref.destroy();
|
|
1003
1048
|
}
|
|
1004
|
-
};
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1049
|
+
}, ...(ngDevMode ? [{ debugName: "ref" }] : []));
|
|
1050
|
+
return value.asReadonly();
|
|
1051
|
+
};
|
|
1052
|
+
/**
|
|
1053
|
+
* A computed that will only be reactive until the source signal contains a falsy value.
|
|
1054
|
+
* All subsequent changes inside the computation will be ignored.
|
|
1055
|
+
*/
|
|
1056
|
+
const computedTillFalsy = (source) => {
|
|
1057
|
+
const value = signal(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1058
|
+
const ref = effect(() => {
|
|
1059
|
+
const val = source();
|
|
1060
|
+
if (!val) {
|
|
1061
|
+
value.set(val);
|
|
1062
|
+
ref.destroy();
|
|
1008
1063
|
}
|
|
1064
|
+
}, ...(ngDevMode ? [{ debugName: "ref" }] : []));
|
|
1065
|
+
return value.asReadonly();
|
|
1066
|
+
};
|
|
1067
|
+
/**
|
|
1068
|
+
* A writeable signal that will be set to the provided value once all inputs are set.
|
|
1069
|
+
* During that time, the signal will be set to `null`.
|
|
1070
|
+
*/
|
|
1071
|
+
const deferredSignal = (valueFn) => {
|
|
1072
|
+
const valueSignal = signal(null, ...(ngDevMode ? [{ debugName: "valueSignal" }] : []));
|
|
1073
|
+
afterNextRender(() => {
|
|
1074
|
+
valueSignal.set(valueFn());
|
|
1075
|
+
});
|
|
1076
|
+
return valueSignal;
|
|
1077
|
+
};
|
|
1078
|
+
const memoizeSignal = (factory) => {
|
|
1079
|
+
let cached = null;
|
|
1080
|
+
return () => {
|
|
1081
|
+
if (!cached)
|
|
1082
|
+
cached = factory();
|
|
1083
|
+
return cached;
|
|
1009
1084
|
};
|
|
1010
|
-
pushMany(config.tokenMap);
|
|
1011
|
-
return { remove, removeMany, has, push, pushMany };
|
|
1012
1085
|
};
|
|
1013
|
-
|
|
1086
|
+
|
|
1087
|
+
/** Inject a signal containing a boolean value indicating if the viewport is xs */
|
|
1088
|
+
const injectIsXs = memoizeSignal(() => injectObserveBreakpoint({ max: 'xs' }));
|
|
1089
|
+
/** Inject a signal containing a boolean value indicating if the viewport is sm */
|
|
1090
|
+
const injectIsSm = memoizeSignal(() => injectObserveBreakpoint({ min: 'sm', max: 'sm' }));
|
|
1091
|
+
/** Inject a signal containing a boolean value indicating if the viewport is md */
|
|
1092
|
+
const injectIsMd = memoizeSignal(() => injectObserveBreakpoint({ min: 'md', max: 'md' }));
|
|
1093
|
+
/** Inject a signal containing a boolean value indicating if the viewport is lg */
|
|
1094
|
+
const injectIsLg = memoizeSignal(() => injectObserveBreakpoint({ min: 'lg', max: 'lg' }));
|
|
1095
|
+
/** Inject a signal containing a boolean value indicating if the viewport is xl */
|
|
1096
|
+
const injectIsXl = memoizeSignal(() => injectObserveBreakpoint({ min: 'xl', max: 'xl' }));
|
|
1097
|
+
/** Inject a signal containing a boolean value indicating if the viewport is 2xl */
|
|
1098
|
+
const injectIs2Xl = memoizeSignal(() => injectObserveBreakpoint({ min: '2xl' }));
|
|
1099
|
+
/**
|
|
1100
|
+
* Inject a boolean value indicating if the viewport is matching the provided options.
|
|
1101
|
+
* This value is not reactive. If you want to react to changes, use the {@link injectObserveBreakpoint} function instead.
|
|
1102
|
+
*/
|
|
1103
|
+
const injectBreakpointIsMatched = (options) => injectBreakpointObserver().isBreakpointMatched(options);
|
|
1104
|
+
/**
|
|
1105
|
+
* Inject a boolean value indicating if the media query is matched.
|
|
1106
|
+
* This value is not reactive. If you want to react to changes, use the {@link injectObserveMediaQuery} function instead.
|
|
1107
|
+
*/
|
|
1108
|
+
const injectMediaQueryIsMatched = (mediaQuery) => injectBreakpointObserver().isMediaQueryMatched(mediaQuery);
|
|
1109
|
+
/**
|
|
1110
|
+
* Inject a signal containing a boolean value indicating if the viewport is matching the provided options.
|
|
1111
|
+
*/
|
|
1112
|
+
const injectObserveBreakpoint = (options) => injectBreakpointObserver().observeBreakpoint(options);
|
|
1113
|
+
/**
|
|
1114
|
+
* Inject a signal containing a boolean value indicating if the media query is matched.
|
|
1115
|
+
*/
|
|
1116
|
+
const injectObserveMediaQuery = (mediaQuery) => injectBreakpointObserver().observeMediaQuery(mediaQuery);
|
|
1117
|
+
/** Inject a signal containing the current breakpoint. */
|
|
1118
|
+
const injectCurrentBreakpoint = memoizeSignal(() => {
|
|
1119
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1120
|
+
const first = BREAKPOINT_ORDER[0];
|
|
1121
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1122
|
+
const last = BREAKPOINT_ORDER[BREAKPOINT_ORDER.length - 1];
|
|
1123
|
+
const highToLow = [...BREAKPOINT_ORDER].reverse();
|
|
1124
|
+
const signals = highToLow.map((bp) => injectObserveBreakpoint(bp === first ? { max: bp } : bp === last ? { min: bp } : { min: bp, max: bp }));
|
|
1125
|
+
return computed(() => {
|
|
1126
|
+
for (let i = 0; i < highToLow.length; i++) {
|
|
1127
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1128
|
+
if (signals[i]())
|
|
1129
|
+
return highToLow[i];
|
|
1130
|
+
}
|
|
1131
|
+
return first;
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
/** Inject a signal that indicates if the user is using a portrait display */
|
|
1135
|
+
const injectIsPortrait = memoizeSignal(() => injectObserveMediaQuery('(orientation: portrait)'));
|
|
1136
|
+
/** Inject a signal that indicates if the user is using a landscape display */
|
|
1137
|
+
const injectIsLandscape = memoizeSignal(() => injectObserveMediaQuery('(orientation: landscape)'));
|
|
1138
|
+
/** Inject a signal containing the current display orientation */
|
|
1139
|
+
const injectDisplayOrientation = memoizeSignal(() => {
|
|
1140
|
+
const isPortrait = injectIsPortrait();
|
|
1141
|
+
return computed(() => {
|
|
1142
|
+
if (isPortrait())
|
|
1143
|
+
return 'portrait';
|
|
1144
|
+
return 'landscape';
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
/** Inject a signal that indicates if the device has a touch input */
|
|
1148
|
+
const injectHasTouchInput = memoizeSignal(() => injectObserveMediaQuery('(pointer: coarse)'));
|
|
1149
|
+
/** Inject a signal that indicates if the device has a fine input (mouse or stylus) */
|
|
1150
|
+
const injectHasPrecisionInput = memoizeSignal(() => injectObserveMediaQuery('(pointer: fine)'));
|
|
1151
|
+
/** Inject a signal containing the current device input type */
|
|
1152
|
+
const injectDeviceInputType = memoizeSignal(() => {
|
|
1153
|
+
const isTouch = injectHasTouchInput();
|
|
1154
|
+
return computed(() => {
|
|
1155
|
+
if (isTouch())
|
|
1156
|
+
return 'touch';
|
|
1157
|
+
return 'mouse';
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
/** Inject a signal containing a boolean value indicating if the user can hover (eg. using a mouse) */
|
|
1161
|
+
const injectCanHover = memoizeSignal(() => injectObserveMediaQuery('(hover: hover)'));
|
|
1162
|
+
/** Inject a signal containing the viewport dimensions */
|
|
1163
|
+
const injectViewportDimensions = memoizeSignal(() => signalElementDimensions(createDocumentElementSignal()));
|
|
1164
|
+
/** Inject a signal containing the scrollbar dimensions. Dimensions will be 0 if scrollbars overlap the page contents (like on mobile). */
|
|
1165
|
+
const injectScrollbarDimensions = memoizeSignal(() => {
|
|
1166
|
+
const document = inject(DOCUMENT);
|
|
1014
1167
|
const renderer = injectRenderer();
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1168
|
+
const scrollbarRuler = renderer.createElement('div');
|
|
1169
|
+
scrollbarRuler.style.width = '100px';
|
|
1170
|
+
scrollbarRuler.style.height = '100px';
|
|
1171
|
+
scrollbarRuler.style.overflow = 'scroll';
|
|
1172
|
+
scrollbarRuler.style.position = 'absolute';
|
|
1173
|
+
scrollbarRuler.style.top = '-9999px';
|
|
1174
|
+
scrollbarRuler.style.scrollbarWidth = getComputedStyle(document.documentElement).scrollbarWidth;
|
|
1175
|
+
renderer.appendChild(document.body, scrollbarRuler);
|
|
1176
|
+
const scrollContainerDimensions = signalElementDimensions(scrollbarRuler);
|
|
1177
|
+
return computed(() => {
|
|
1178
|
+
const client = scrollContainerDimensions().client;
|
|
1179
|
+
if (!client)
|
|
1180
|
+
return null;
|
|
1181
|
+
return {
|
|
1182
|
+
width: Math.max(0, 100 - client.width),
|
|
1183
|
+
height: Math.max(0, 100 - client.height),
|
|
1184
|
+
};
|
|
1026
1185
|
});
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
const BREAKPOINT_KEY_SET = new Set(BREAKPOINT_ORDER);
|
|
1189
|
+
const isBreakpointMap = (value) => {
|
|
1190
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
1191
|
+
return false;
|
|
1192
|
+
const keys = Object.keys(value);
|
|
1193
|
+
return keys.length > 0 && keys.every((k) => BREAKPOINT_KEY_SET.has(k));
|
|
1027
1194
|
};
|
|
1028
|
-
const
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1195
|
+
const resolveFromMap = (map, bp, defaultValue) => {
|
|
1196
|
+
const idx = BREAKPOINT_ORDER.indexOf(bp);
|
|
1197
|
+
for (let i = idx; i >= 0; i--) {
|
|
1198
|
+
const v = map[BREAKPOINT_ORDER[i]];
|
|
1199
|
+
if (v !== undefined)
|
|
1200
|
+
return v;
|
|
1201
|
+
}
|
|
1202
|
+
return defaultValue;
|
|
1203
|
+
};
|
|
1204
|
+
const breakpointTransformBase = (coerce) => {
|
|
1205
|
+
const currentBp = injectCurrentBreakpoint();
|
|
1206
|
+
const injector = inject(Injector);
|
|
1207
|
+
const raw = signal(undefined, ...(ngDevMode ? [{ debugName: "raw" }] : []));
|
|
1208
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1209
|
+
let capturedDefault = undefined;
|
|
1210
|
+
let initialized = false;
|
|
1211
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1212
|
+
let cachedSig = null;
|
|
1213
|
+
const transformFn = (value) => {
|
|
1214
|
+
const coerced = isBreakpointMap(value) ? value : coerce(value);
|
|
1215
|
+
if (!initialized) {
|
|
1216
|
+
capturedDefault = coerced;
|
|
1217
|
+
initialized = true;
|
|
1218
|
+
}
|
|
1219
|
+
raw.set(coerced);
|
|
1220
|
+
return isBreakpointMap(coerced)
|
|
1221
|
+
? resolveFromMap(coerced, currentBp(), capturedDefault)
|
|
1222
|
+
: coerced;
|
|
1223
|
+
};
|
|
1224
|
+
effect(() => {
|
|
1225
|
+
const bp = currentBp();
|
|
1226
|
+
const r = raw();
|
|
1227
|
+
if (!cachedSig) {
|
|
1228
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1229
|
+
const instance = injector.get(BREAKPOINT_INSTANCE_TOKEN);
|
|
1230
|
+
for (const key of Object.keys(instance)) {
|
|
1231
|
+
const val = instance[key];
|
|
1232
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1233
|
+
if (val && typeof val === 'function' && val[SIGNAL]?.transformFn === transformFn) {
|
|
1234
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1235
|
+
cachedSig = val;
|
|
1236
|
+
break;
|
|
1051
1237
|
}
|
|
1052
1238
|
}
|
|
1053
|
-
}
|
|
1239
|
+
}
|
|
1240
|
+
if (!cachedSig || r === undefined || !isBreakpointMap(r))
|
|
1241
|
+
return;
|
|
1242
|
+
const resolved = resolveFromMap(r, bp, capturedDefault);
|
|
1243
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1244
|
+
untracked(() => setInputSignal(cachedSig, resolved));
|
|
1054
1245
|
});
|
|
1246
|
+
return transformFn;
|
|
1055
1247
|
};
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1248
|
+
/**
|
|
1249
|
+
* Transform factory for boolean inputs.
|
|
1250
|
+
* Coerces plain values with `booleanAttribute`; resolves {@link BreakpointMap} mobile-first.
|
|
1251
|
+
*
|
|
1252
|
+
* @example
|
|
1253
|
+
* snap = input(false, { transform: boolBreakpointTransform() });
|
|
1254
|
+
* // Template: `snap` | `[snap]="true"` | `[snap]="{ xs: false, md: true }"`
|
|
1255
|
+
*/
|
|
1256
|
+
const boolBreakpointTransform = () => breakpointTransformBase(booleanAttribute);
|
|
1257
|
+
/**
|
|
1258
|
+
* Transform factory for number inputs.
|
|
1259
|
+
* Coerces plain values with `numberAttribute`; resolves {@link BreakpointMap} mobile-first.
|
|
1260
|
+
*
|
|
1261
|
+
* @example
|
|
1262
|
+
* scrollMargin = input(0, { transform: numberBreakpointTransform() });
|
|
1263
|
+
* // Template: `[scrollMargin]="16"` | `[scrollMargin]="{ xs: 0, md: 16 }"`
|
|
1264
|
+
*/
|
|
1265
|
+
const numberBreakpointTransform = () => breakpointTransformBase((v) => numberAttribute(v));
|
|
1266
|
+
/**
|
|
1267
|
+
* Transform factory for any typed input (string unions, arrays, objects, etc.).
|
|
1268
|
+
* Passes plain values through as-is; resolves {@link BreakpointMap} mobile-first.
|
|
1269
|
+
* A value is treated as a {@link BreakpointMap} only when all its keys are valid breakpoint names.
|
|
1270
|
+
*
|
|
1271
|
+
* @example
|
|
1272
|
+
* itemSize = input('auto', { transform: typedBreakpointTransform<ScrollableItemSize>() });
|
|
1273
|
+
* tags = input([], { transform: typedBreakpointTransform<string[]>() });
|
|
1274
|
+
* // Template: `[itemSize]="'third'"` | `[itemSize]="{ xs: 'full', md: 'third' }"`
|
|
1275
|
+
*/
|
|
1276
|
+
const typedBreakpointTransform = () => breakpointTransformBase((v) => v);
|
|
1277
|
+
const injectBreakpointInput = (inputSignal, defaultValue) => {
|
|
1278
|
+
const currentBreakpoint = injectCurrentBreakpoint();
|
|
1279
|
+
return computed(() => {
|
|
1280
|
+
const value = inputSignal();
|
|
1281
|
+
if (!isBreakpointMap(value))
|
|
1282
|
+
return value;
|
|
1283
|
+
return resolveFromMap(value, currentBreakpoint(), defaultValue);
|
|
1074
1284
|
});
|
|
1075
1285
|
};
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1286
|
+
const booleanBreakpointAttribute = (value) => {
|
|
1287
|
+
if (isBreakpointMap(value))
|
|
1288
|
+
return value;
|
|
1289
|
+
return booleanAttribute(value);
|
|
1290
|
+
};
|
|
1291
|
+
const numberBreakpointAttribute = (value) => {
|
|
1292
|
+
if (isBreakpointMap(value))
|
|
1293
|
+
return value;
|
|
1294
|
+
return numberAttribute(value);
|
|
1295
|
+
};
|
|
1296
|
+
const BREAKPOINT_INSTANCE_TOKEN = new InjectionToken('BREAKPOINT_INSTANCE_TOKEN');
|
|
1297
|
+
const provideBreakpointInstance = (componentClass) => ({
|
|
1298
|
+
provide: BREAKPOINT_INSTANCE_TOKEN,
|
|
1299
|
+
useExisting: componentClass,
|
|
1087
1300
|
});
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const rectFn = () => cachedNormalizedRect ? cachedNormalizedRect : boundingClientRectToElementRect(el.getBoundingClientRect());
|
|
1099
|
-
return {
|
|
1100
|
-
rect: rectFn,
|
|
1101
|
-
client: { width: el.clientWidth, height: el.clientHeight },
|
|
1102
|
-
scroll: { width: el.scrollWidth, height: el.scrollHeight },
|
|
1103
|
-
offset: { width: el.offsetWidth, height: el.offsetHeight },
|
|
1301
|
+
|
|
1302
|
+
const controlValueSignal = (control, options) => {
|
|
1303
|
+
const getRawValueSafe = (ctrl) => {
|
|
1304
|
+
try {
|
|
1305
|
+
return isSignal(ctrl) ? (ctrl()?.getRawValue() ?? null) : (ctrl?.getRawValue() ?? null);
|
|
1306
|
+
}
|
|
1307
|
+
catch {
|
|
1308
|
+
// Ignore errors. This can happen if the passed control is a required input and is not yet initialized.
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1104
1311
|
};
|
|
1312
|
+
const initialValue = getRawValueSafe(control);
|
|
1313
|
+
const controlStream = isSignal(control)
|
|
1314
|
+
? toObservable(control)
|
|
1315
|
+
: of(control);
|
|
1316
|
+
const controlObs = controlStream.pipe(switchMap((ctrl) => {
|
|
1317
|
+
if (!ctrl)
|
|
1318
|
+
return of(null);
|
|
1319
|
+
const vcsObs = options?.debounceTime
|
|
1320
|
+
? ctrl.valueChanges.pipe(debounceTime(options.debounceTime))
|
|
1321
|
+
: ctrl.valueChanges;
|
|
1322
|
+
return vcsObs.pipe(startWith(ctrl.getRawValue()), map(() => ctrl.getRawValue()));
|
|
1323
|
+
}));
|
|
1324
|
+
const obs = !options?.debounceFirst ? merge(of(initialValue), controlObs) : controlObs;
|
|
1325
|
+
return toSignal(obs.pipe(distinctUntilChanged((a, b) => equal(a, b))), {
|
|
1326
|
+
initialValue,
|
|
1327
|
+
});
|
|
1105
1328
|
};
|
|
1106
|
-
|
|
1329
|
+
/**
|
|
1330
|
+
* The first item in the pair is the previous value and the second item is the current value.
|
|
1331
|
+
*/
|
|
1332
|
+
const controlValueSignalWithPrevious = (control, options) => {
|
|
1333
|
+
const data = linkedSignal({ ...(ngDevMode ? { debugName: "data" } : {}), source: controlValueSignal(control, options),
|
|
1334
|
+
computation: (curr, prev) => [prev?.source ?? null, curr] });
|
|
1335
|
+
return data.asReadonly();
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
const signalElementMutations = (el, options) => {
|
|
1107
1339
|
const destroyRef = inject(DestroyRef);
|
|
1108
1340
|
const elements = buildElementSignal(el);
|
|
1109
1341
|
const firstEl = firstElementSignal(elements);
|
|
1110
1342
|
const zone = inject(NgZone);
|
|
1111
1343
|
const isRendered = signalIsRendered();
|
|
1112
|
-
const
|
|
1113
|
-
const
|
|
1114
|
-
const observer = new ResizeObserver((e) => {
|
|
1344
|
+
const elementMutationsSignal = signal(null, ...(ngDevMode ? [{ debugName: "elementMutationsSignal" }] : []));
|
|
1345
|
+
const observer = new MutationObserver((e) => {
|
|
1115
1346
|
if (!isRendered())
|
|
1116
1347
|
return;
|
|
1117
1348
|
const entry = e[0];
|
|
1118
1349
|
if (entry) {
|
|
1119
|
-
|
|
1120
|
-
const newDimensions = createElementDimensions(target);
|
|
1121
|
-
zone.run(() => elementDimensionsSignal.set(newDimensions));
|
|
1350
|
+
zone.run(() => elementMutationsSignal.set(entry));
|
|
1122
1351
|
}
|
|
1123
1352
|
});
|
|
1124
1353
|
effect(() => {
|
|
1125
1354
|
const els = firstEl();
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
const currentElIsAngularComponent = els.currentElement?.tagName.toLowerCase().includes('-');
|
|
1134
|
-
if (computedDisplay === 'inline' && isDevMode() && currentElIsAngularComponent) {
|
|
1135
|
-
console.error(`Element <${els.currentElement?.tagName.toLowerCase()}> is an Angular component and has a display of 'inline'. Inline elements cannot be observed for dimensions. Please change it to 'block' or something else.`);
|
|
1136
|
-
}
|
|
1137
|
-
observer.observe(els.currentElement);
|
|
1138
|
-
}
|
|
1139
|
-
});
|
|
1355
|
+
elementMutationsSignal.set(null);
|
|
1356
|
+
if (els.previousElement) {
|
|
1357
|
+
observer.disconnect();
|
|
1358
|
+
}
|
|
1359
|
+
if (els.currentElement) {
|
|
1360
|
+
observer.observe(els.currentElement, options);
|
|
1361
|
+
}
|
|
1140
1362
|
});
|
|
1141
1363
|
destroyRef.onDestroy(() => observer.disconnect());
|
|
1142
|
-
return
|
|
1143
|
-
equal: (a, b) => equal(a, b),
|
|
1144
|
-
});
|
|
1364
|
+
return elementMutationsSignal.asReadonly();
|
|
1145
1365
|
};
|
|
1146
|
-
const
|
|
1366
|
+
const signalHostElementMutations = (options) => signalElementMutations(inject(ElementRef), options);
|
|
1147
1367
|
|
|
1148
|
-
const
|
|
1149
|
-
const boundingRect = entry.boundingClientRect;
|
|
1150
|
-
const rootBounds = entry.rootBounds;
|
|
1151
|
-
let isAbove = false;
|
|
1152
|
-
let isBelow = false;
|
|
1153
|
-
let isLeft = false;
|
|
1154
|
-
let isRight = false;
|
|
1155
|
-
if (rootBounds) {
|
|
1156
|
-
isAbove = boundingRect.bottom < rootBounds.top;
|
|
1157
|
-
isBelow = boundingRect.top > rootBounds.bottom;
|
|
1158
|
-
isLeft = boundingRect.right < rootBounds.left;
|
|
1159
|
-
isRight = boundingRect.left > rootBounds.right;
|
|
1160
|
-
}
|
|
1161
|
-
// We cant use entry.isIntersecting to determine actual visibility since we are using a big threshold array to get more intersection events.
|
|
1162
|
-
const isVisible = !isAbove && !isBelow && !isLeft && !isRight && entry.intersectionRatio > 0;
|
|
1163
|
-
return {
|
|
1164
|
-
isAbove,
|
|
1165
|
-
isBelow,
|
|
1166
|
-
isLeft,
|
|
1167
|
-
isRight,
|
|
1168
|
-
isVisible,
|
|
1169
|
-
};
|
|
1170
|
-
};
|
|
1171
|
-
const signalElementIntersection = (el, options) => {
|
|
1172
|
-
const destroyRef = inject(DestroyRef);
|
|
1368
|
+
const signalElementChildren = (el) => {
|
|
1173
1369
|
const elements = buildElementSignal(el);
|
|
1174
|
-
const
|
|
1175
|
-
const zone = inject(NgZone);
|
|
1370
|
+
const firstEl = firstElementSignal(elements);
|
|
1176
1371
|
const isRendered = signalIsRendered();
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
rootBounds: entry.rootBounds,
|
|
1193
|
-
target: entry.target,
|
|
1194
|
-
time: entry.time,
|
|
1195
|
-
...createPositionObject(entry),
|
|
1196
|
-
};
|
|
1197
|
-
if (existingEntryIndex !== -1) {
|
|
1198
|
-
currentValues = [
|
|
1199
|
-
...currentValues.slice(0, existingEntryIndex),
|
|
1200
|
-
intersectionEntry,
|
|
1201
|
-
...currentValues.slice(existingEntryIndex + 1),
|
|
1202
|
-
];
|
|
1203
|
-
}
|
|
1204
|
-
else {
|
|
1205
|
-
currentValues = [...currentValues, intersectionEntry];
|
|
1372
|
+
const elementMutations = signalElementMutations(elements, { childList: true, subtree: true, attributes: true });
|
|
1373
|
+
return computed(() => {
|
|
1374
|
+
if (!isRendered())
|
|
1375
|
+
return [];
|
|
1376
|
+
const els = firstEl();
|
|
1377
|
+
// We are not interested what the mutation is, just that there is one.
|
|
1378
|
+
// Changes to the DOM may affect the children of the element.
|
|
1379
|
+
elementMutations();
|
|
1380
|
+
if (!els.currentElement)
|
|
1381
|
+
return [];
|
|
1382
|
+
const children = [];
|
|
1383
|
+
for (let index = 0; index < els.currentElement.children.length; index++) {
|
|
1384
|
+
const element = els.currentElement.children[index];
|
|
1385
|
+
if (element instanceof HTMLElement) {
|
|
1386
|
+
children.push(element);
|
|
1206
1387
|
}
|
|
1207
1388
|
}
|
|
1208
|
-
|
|
1209
|
-
};
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
}
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
root: rootEl,
|
|
1220
|
-
});
|
|
1221
|
-
observer.set(newObserver);
|
|
1222
|
-
};
|
|
1223
|
-
const updateObservedElements = (observer, elements) => {
|
|
1224
|
-
const rootEl = root().currentElement;
|
|
1225
|
-
if (!observer)
|
|
1226
|
-
return;
|
|
1227
|
-
const currIntersectionValue = elementIntersectionSignal();
|
|
1228
|
-
const newIntersectionValue = [];
|
|
1229
|
-
for (const el of elements.currentElements) {
|
|
1230
|
-
if (currentlyObservedElements.has(el)) {
|
|
1231
|
-
const existingEntryIndex = currIntersectionValue.findIndex((v) => v.target === el);
|
|
1232
|
-
const existingEntry = currIntersectionValue[existingEntryIndex];
|
|
1233
|
-
if (!existingEntry) {
|
|
1234
|
-
console.warn('Could not find existing entry for element. The intersection observer might be broken now.', el);
|
|
1235
|
-
continue;
|
|
1236
|
-
}
|
|
1237
|
-
newIntersectionValue.push(existingEntry);
|
|
1389
|
+
return children;
|
|
1390
|
+
}, { equal: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) });
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
const buildSignalEffects = (el, config) => {
|
|
1394
|
+
const elements = buildElementSignal(el);
|
|
1395
|
+
const injector = inject(Injector);
|
|
1396
|
+
effect(() => {
|
|
1397
|
+
const { currentElements, previousElements } = elements();
|
|
1398
|
+
for (const previousEl of previousElements) {
|
|
1399
|
+
if (currentElements.includes(previousEl))
|
|
1238
1400
|
continue;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
});
|
|
1244
|
-
if (!initialElementVisibility) {
|
|
1245
|
-
console.error('No visibility data found for element.', {
|
|
1246
|
-
element: el,
|
|
1247
|
-
container: rootEl,
|
|
1248
|
-
});
|
|
1401
|
+
const tokens = Object.keys(config.tokenMap)
|
|
1402
|
+
.map((key) => key.split(' '))
|
|
1403
|
+
.flat();
|
|
1404
|
+
if (!tokens.length)
|
|
1249
1405
|
continue;
|
|
1250
|
-
|
|
1251
|
-
const intersectionEntry = {
|
|
1252
|
-
boundingClientRect: initialElementVisibility.elementRect,
|
|
1253
|
-
intersectionRatio: initialElementVisibility.intersectionRatio,
|
|
1254
|
-
intersectionRect: initialElementVisibility.elementRect,
|
|
1255
|
-
isIntersecting: initialElementVisibility.isIntersecting,
|
|
1256
|
-
rootBounds: initialElementVisibility.containerRect,
|
|
1257
|
-
target: el,
|
|
1258
|
-
time: performance.now(),
|
|
1259
|
-
};
|
|
1260
|
-
newIntersectionValue.push({
|
|
1261
|
-
...intersectionEntry,
|
|
1262
|
-
...createPositionObject(intersectionEntry),
|
|
1263
|
-
});
|
|
1264
|
-
currentlyObservedElements.add(el);
|
|
1265
|
-
observer.observe(el);
|
|
1406
|
+
config.cleanupFn(previousEl, tokens);
|
|
1266
1407
|
}
|
|
1267
|
-
for (const
|
|
1268
|
-
if (
|
|
1408
|
+
for (const currentEl of currentElements) {
|
|
1409
|
+
if (previousElements.includes(currentEl))
|
|
1269
1410
|
continue;
|
|
1270
|
-
|
|
1271
|
-
|
|
1411
|
+
for (const [tokens, condition] of Object.entries(config.tokenMap)) {
|
|
1412
|
+
untracked(() => {
|
|
1413
|
+
const tokenArray = tokens.split(' ');
|
|
1414
|
+
if (!tokenArray.length)
|
|
1415
|
+
return;
|
|
1416
|
+
config.updateFn(currentEl, tokenArray, condition());
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1272
1419
|
}
|
|
1273
|
-
elementIntersectionSignal.set(newIntersectionValue);
|
|
1274
|
-
};
|
|
1275
|
-
effect(() => {
|
|
1276
|
-
const rootEl = root().currentElement;
|
|
1277
|
-
const rendered = isRendered();
|
|
1278
|
-
const enabled = isEnabled();
|
|
1279
|
-
untracked(() => updateIntersectionObserver(rendered, enabled, rootEl));
|
|
1280
1420
|
});
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
};
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
lastScrollTop = 0;
|
|
1303
|
-
lastScrollLeft = 0;
|
|
1304
|
-
return of(null);
|
|
1421
|
+
const effects = {};
|
|
1422
|
+
const has = (tokens) => tokens in effects;
|
|
1423
|
+
const push = (tokens, signal) => {
|
|
1424
|
+
if (has(tokens))
|
|
1425
|
+
return;
|
|
1426
|
+
runInInjectionContext(injector, () => {
|
|
1427
|
+
effects[tokens] = effect(() => {
|
|
1428
|
+
const { currentElements } = untracked(() => elements());
|
|
1429
|
+
const value = signal();
|
|
1430
|
+
for (const el of currentElements) {
|
|
1431
|
+
const tokenArray = tokens.split(' ');
|
|
1432
|
+
if (!tokenArray.length)
|
|
1433
|
+
continue;
|
|
1434
|
+
config.updateFn(el, tokenArray, value);
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
});
|
|
1438
|
+
};
|
|
1439
|
+
const pushMany = (map) => {
|
|
1440
|
+
for (const [tokens, signal] of Object.entries(map)) {
|
|
1441
|
+
push(tokens, signal);
|
|
1305
1442
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1443
|
+
};
|
|
1444
|
+
const remove = (tokens) => {
|
|
1445
|
+
effects[tokens]?.destroy();
|
|
1446
|
+
delete effects[tokens];
|
|
1447
|
+
for (const el of elements().currentElements) {
|
|
1448
|
+
const tokenArray = tokens.split(' ');
|
|
1449
|
+
if (!tokenArray.length)
|
|
1450
|
+
continue;
|
|
1451
|
+
config.cleanupFn(el, tokenArray);
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
const removeMany = (tokens) => {
|
|
1455
|
+
for (const token of tokens) {
|
|
1456
|
+
remove(token);
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
pushMany(config.tokenMap);
|
|
1460
|
+
return { remove, removeMany, has, push, pushMany };
|
|
1461
|
+
};
|
|
1462
|
+
const signalClasses = (el, classMap) => {
|
|
1463
|
+
const renderer = injectRenderer();
|
|
1464
|
+
return buildSignalEffects(el, {
|
|
1465
|
+
tokenMap: classMap,
|
|
1466
|
+
cleanupFn: (el, tokens) => tokens.forEach((token) => renderer.removeClass(el, token)),
|
|
1467
|
+
updateFn: (el, tokens, condition) => {
|
|
1468
|
+
if (!condition) {
|
|
1469
|
+
tokens.forEach((token) => renderer.removeClass(el, token));
|
|
1317
1470
|
}
|
|
1318
|
-
else
|
|
1319
|
-
|
|
1471
|
+
else {
|
|
1472
|
+
tokens.forEach((token) => renderer.addClass(el, token));
|
|
1320
1473
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
}));
|
|
1324
|
-
}), takeUntilDestroyed(destroyRef))
|
|
1325
|
-
.subscribe();
|
|
1326
|
-
return lastScrollDirection.asReadonly();
|
|
1474
|
+
},
|
|
1475
|
+
});
|
|
1327
1476
|
};
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1477
|
+
const signalHostClasses = (classMap) => signalClasses(inject(ElementRef), classMap);
|
|
1478
|
+
const ALWAYS_TRUE_ATTRIBUTE_KEYS = ['disabled', 'readonly', 'required', 'checked', 'selected', 'hidden', 'inert'];
|
|
1479
|
+
const signalAttributes = (el, attributeMap) => {
|
|
1480
|
+
const renderer = injectRenderer();
|
|
1481
|
+
return buildSignalEffects(el, {
|
|
1482
|
+
tokenMap: attributeMap,
|
|
1483
|
+
cleanupFn: (el, tokens) => tokens.forEach((token) => el.removeAttribute(token)),
|
|
1484
|
+
updateFn: (el, tokens, condition) => {
|
|
1485
|
+
for (const token of tokens) {
|
|
1486
|
+
if (ALWAYS_TRUE_ATTRIBUTE_KEYS.includes(token)) {
|
|
1487
|
+
if (condition) {
|
|
1488
|
+
renderer.setAttribute(el, token, '');
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
renderer.removeAttribute(el, token);
|
|
1492
|
+
}
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
if (condition === null || condition === undefined) {
|
|
1496
|
+
renderer.removeAttribute(el, token);
|
|
1497
|
+
}
|
|
1498
|
+
else {
|
|
1499
|
+
renderer.setAttribute(el, token, `${condition}`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
},
|
|
1503
|
+
});
|
|
1335
1504
|
};
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
if (scrollPosition.top !== undefined)
|
|
1353
|
-
element.scrollTop = scrollPosition.top;
|
|
1354
|
-
ref.destroy();
|
|
1505
|
+
const signalHostAttributes = (attributeMap) => signalAttributes(inject(ElementRef), attributeMap);
|
|
1506
|
+
const signalStyles = (el, styleMap) => {
|
|
1507
|
+
const renderer = injectRenderer();
|
|
1508
|
+
return buildSignalEffects(el, {
|
|
1509
|
+
tokenMap: styleMap,
|
|
1510
|
+
cleanupFn: (el, tokens) => tokens.forEach((token) => renderer.removeStyle(el, token, RendererStyleFlags2.DashCase)),
|
|
1511
|
+
updateFn: (el, tokens, condition) => {
|
|
1512
|
+
for (const token of tokens) {
|
|
1513
|
+
if (condition === null || condition === undefined) {
|
|
1514
|
+
renderer.removeStyle(el, token, RendererStyleFlags2.DashCase);
|
|
1515
|
+
}
|
|
1516
|
+
else {
|
|
1517
|
+
renderer.setStyle(el, {
|
|
1518
|
+
[token]: `${condition}`,
|
|
1519
|
+
}, RendererStyleFlags2.DashCase);
|
|
1520
|
+
}
|
|
1355
1521
|
}
|
|
1356
|
-
},
|
|
1357
|
-
}
|
|
1358
|
-
const notScrollable = (dimensions) => ({
|
|
1359
|
-
canScroll: false,
|
|
1360
|
-
canScrollHorizontally: false,
|
|
1361
|
-
canScrollVertically: false,
|
|
1362
|
-
elementDimensions: dimensions,
|
|
1522
|
+
},
|
|
1363
1523
|
});
|
|
1364
|
-
return computed(() => {
|
|
1365
|
-
const element = observedEl().currentElement;
|
|
1366
|
-
const dimensions = elementDimensions();
|
|
1367
|
-
// We are not interested what the mutation is, just that there is one.
|
|
1368
|
-
// Changes to the DOM can affect the scroll state of the element.
|
|
1369
|
-
elementMutations();
|
|
1370
|
-
if (!element || !isRendered())
|
|
1371
|
-
return notScrollable(dimensions);
|
|
1372
|
-
const { scrollWidth, scrollHeight, clientHeight, clientWidth } = element;
|
|
1373
|
-
const canScrollHorizontally = scrollWidth > clientWidth;
|
|
1374
|
-
const canScrollVertically = scrollHeight > clientHeight;
|
|
1375
|
-
return {
|
|
1376
|
-
canScroll: canScrollHorizontally || canScrollVertically,
|
|
1377
|
-
canScrollHorizontally,
|
|
1378
|
-
canScrollVertically,
|
|
1379
|
-
elementDimensions: dimensions,
|
|
1380
|
-
};
|
|
1381
|
-
}, { equal: (a, b) => areScrollStatesEqual(a, b) });
|
|
1382
1524
|
};
|
|
1383
|
-
const
|
|
1525
|
+
const signalHostStyles = (styleMap) => signalStyles(inject(ElementRef), styleMap);
|
|
1384
1526
|
|
|
1385
|
-
const
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
let isFirstRun = options?.skipSyncRead ? false : true;
|
|
1391
|
-
if (!options?.skipSyncRead) {
|
|
1392
|
-
try {
|
|
1393
|
-
// this might throw if the signal is not yet initialized (eg. a required signal input inside the constructor)
|
|
1394
|
-
// in that case we just skip the initial sync
|
|
1395
|
-
to.set(from());
|
|
1396
|
-
}
|
|
1397
|
-
catch {
|
|
1398
|
-
isFirstRun = false;
|
|
1399
|
-
if (isDevMode()) {
|
|
1400
|
-
console.warn('Failed to sync signals. The target signal is not yet initialized.', { from, to });
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1527
|
+
const elementCanScroll = (element, direction) => {
|
|
1528
|
+
const el = element || document.documentElement;
|
|
1529
|
+
const { scrollHeight, clientHeight, scrollWidth, clientWidth } = el;
|
|
1530
|
+
if (direction === 'x') {
|
|
1531
|
+
return scrollWidth > clientWidth;
|
|
1403
1532
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
if (options?.skipFirstRun && isFirstRun) {
|
|
1407
|
-
isFirstRun = false;
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
untracked(() => {
|
|
1411
|
-
to.set(formVal);
|
|
1412
|
-
});
|
|
1413
|
-
}, ...(ngDevMode ? [{ debugName: "ref" }] : []));
|
|
1414
|
-
return ref;
|
|
1415
|
-
};
|
|
1416
|
-
const maybeSignalValue = (value) => {
|
|
1417
|
-
if (isSignal(value)) {
|
|
1418
|
-
return value();
|
|
1533
|
+
else if (direction === 'y') {
|
|
1534
|
+
return scrollHeight > clientHeight;
|
|
1419
1535
|
}
|
|
1420
|
-
return
|
|
1421
|
-
};
|
|
1422
|
-
/**
|
|
1423
|
-
* A computed that will only be reactive until the source signal contains a truthy value.
|
|
1424
|
-
* All subsequent changes inside the computation will be ignored.
|
|
1425
|
-
*/
|
|
1426
|
-
const computedTillTruthy = (source) => {
|
|
1427
|
-
const value = signal(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1428
|
-
const ref = effect(() => {
|
|
1429
|
-
const val = source();
|
|
1430
|
-
if (val) {
|
|
1431
|
-
value.set(val);
|
|
1432
|
-
ref.destroy();
|
|
1433
|
-
}
|
|
1434
|
-
}, ...(ngDevMode ? [{ debugName: "ref" }] : []));
|
|
1435
|
-
return value.asReadonly();
|
|
1536
|
+
return scrollHeight > clientHeight || scrollWidth > clientWidth;
|
|
1436
1537
|
};
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1538
|
+
const createViewportRect = () => ({
|
|
1539
|
+
left: 0,
|
|
1540
|
+
top: 0,
|
|
1541
|
+
right: window.innerWidth,
|
|
1542
|
+
bottom: window.innerHeight,
|
|
1543
|
+
width: window.innerWidth,
|
|
1544
|
+
height: window.innerHeight,
|
|
1545
|
+
x: 0,
|
|
1546
|
+
y: 0,
|
|
1547
|
+
toJSON: () => ({}),
|
|
1548
|
+
});
|
|
1549
|
+
const isElementVisible = (options) => {
|
|
1550
|
+
const { container, element } = options;
|
|
1551
|
+
if (!element) {
|
|
1552
|
+
return null;
|
|
1553
|
+
}
|
|
1554
|
+
const elementRect = options.elementRect || element.getBoundingClientRect();
|
|
1555
|
+
const containerRect = container ? options.containerRect || container.getBoundingClientRect() : createViewportRect();
|
|
1556
|
+
const canScroll = elementCanScroll(container);
|
|
1557
|
+
if (!canScroll) {
|
|
1558
|
+
return {
|
|
1559
|
+
inline: true,
|
|
1560
|
+
block: true,
|
|
1561
|
+
blockIntersection: 1,
|
|
1562
|
+
inlineIntersection: 1,
|
|
1563
|
+
intersectionRatio: 1,
|
|
1564
|
+
isIntersecting: true,
|
|
1565
|
+
element,
|
|
1566
|
+
containerRect,
|
|
1567
|
+
elementRect,
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
const elLeft = elementRect.left;
|
|
1571
|
+
const elTop = elementRect.top;
|
|
1572
|
+
const elWidth = elementRect.width || 1;
|
|
1573
|
+
const elHeight = elementRect.height || 1;
|
|
1574
|
+
const elRight = elLeft + elWidth;
|
|
1575
|
+
const elBottom = elTop + elHeight;
|
|
1576
|
+
const conLeft = containerRect.left;
|
|
1577
|
+
const conTop = containerRect.top;
|
|
1578
|
+
const conRight = conLeft + containerRect.width;
|
|
1579
|
+
const conBottom = conTop + containerRect.height;
|
|
1580
|
+
const isElementInlineVisible = elLeft >= conLeft && elRight <= conRight;
|
|
1581
|
+
const isElementBlockVisible = elTop >= conTop && elBottom <= conBottom;
|
|
1582
|
+
const inlineIntersection = Math.min(elRight, conRight) - Math.max(elLeft, conLeft);
|
|
1583
|
+
const blockIntersection = Math.min(elBottom, conBottom) - Math.max(elTop, conTop);
|
|
1584
|
+
const inlineIntersectionPercentage = clamp(inlineIntersection / elWidth, 0, 1);
|
|
1585
|
+
const blockIntersectionPercentage = clamp(blockIntersection / elHeight, 0, 1);
|
|
1586
|
+
return {
|
|
1587
|
+
inline: isElementInlineVisible,
|
|
1588
|
+
block: isElementBlockVisible,
|
|
1589
|
+
inlineIntersection: inlineIntersectionPercentage,
|
|
1590
|
+
blockIntersection: blockIntersectionPercentage,
|
|
1591
|
+
isIntersecting: isElementInlineVisible && isElementBlockVisible,
|
|
1592
|
+
element,
|
|
1593
|
+
containerRect,
|
|
1594
|
+
elementRect,
|
|
1595
|
+
// Round the intersection ratio to the nearest 0.01 to avoid floating point errors and system scaling issues.
|
|
1596
|
+
intersectionRatio: Math.round(Math.min(inlineIntersectionPercentage, blockIntersectionPercentage) * 100) / 100,
|
|
1597
|
+
};
|
|
1598
|
+
};
|
|
1599
|
+
const getElementScrollCoordinates = (options) => {
|
|
1600
|
+
const { container, element, direction, behavior = 'smooth', origin = 'nearest', scrollBlockMargin = 0, scrollInlineMargin = 0, } = options;
|
|
1601
|
+
if (!element || !container || !elementCanScroll(container)) {
|
|
1602
|
+
return {
|
|
1603
|
+
behavior,
|
|
1604
|
+
left: undefined,
|
|
1605
|
+
top: undefined,
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
const elementRect = element.getBoundingClientRect();
|
|
1609
|
+
const containerRect = container.getBoundingClientRect();
|
|
1610
|
+
const { scrollLeft, scrollTop } = container;
|
|
1611
|
+
const elWidth = elementRect.width;
|
|
1612
|
+
const elHeight = elementRect.height;
|
|
1613
|
+
const elLeft = elementRect.left;
|
|
1614
|
+
const elTop = elementRect.top;
|
|
1615
|
+
const elRight = elementRect.right;
|
|
1616
|
+
const elBottom = elementRect.bottom;
|
|
1617
|
+
const conWidth = containerRect.width;
|
|
1618
|
+
const conHeight = containerRect.height;
|
|
1619
|
+
const conLeft = containerRect.left;
|
|
1620
|
+
const conTop = containerRect.top;
|
|
1621
|
+
const conRight = containerRect.right;
|
|
1622
|
+
const conBottom = containerRect.bottom;
|
|
1623
|
+
const shouldScrollLeft = direction === 'inline' || direction === 'both' || !direction;
|
|
1624
|
+
const shouldScrollTop = direction === 'block' || direction === 'both' || !direction;
|
|
1625
|
+
let scrollLeftTo = scrollLeft;
|
|
1626
|
+
let scrollTopTo = scrollTop;
|
|
1627
|
+
const relativeTop = elTop - conTop;
|
|
1628
|
+
const relativeLeft = elLeft - conLeft;
|
|
1629
|
+
const calculateScrollToStart = () => {
|
|
1630
|
+
scrollLeftTo = scrollLeft + relativeLeft - scrollInlineMargin;
|
|
1631
|
+
scrollTopTo = scrollTop + relativeTop - scrollBlockMargin;
|
|
1632
|
+
};
|
|
1633
|
+
const calculateScrollToEnd = () => {
|
|
1634
|
+
scrollLeftTo = scrollLeft + relativeLeft - conWidth + elWidth + scrollInlineMargin;
|
|
1635
|
+
scrollTopTo = scrollTop + relativeTop - conHeight + elHeight + scrollBlockMargin;
|
|
1636
|
+
};
|
|
1637
|
+
const calculateScrollToCenter = () => {
|
|
1638
|
+
scrollLeftTo = scrollLeft + relativeLeft - conWidth / 2 + elWidth / 2;
|
|
1639
|
+
scrollTopTo = scrollTop + relativeTop - conHeight / 2 + elHeight / 2;
|
|
1640
|
+
};
|
|
1641
|
+
const calculateScrollToNearest = () => {
|
|
1642
|
+
const isAbove = elBottom < conTop;
|
|
1643
|
+
const isPartialAbove = elTop < conTop && elBottom > conTop;
|
|
1644
|
+
const isBelow = elTop > conBottom;
|
|
1645
|
+
const isPartialBelow = elTop < conBottom && elBottom > conBottom;
|
|
1646
|
+
const isLeft = elRight < conLeft;
|
|
1647
|
+
const isPartialLeft = elLeft < conLeft && elRight > conLeft;
|
|
1648
|
+
const isRight = elLeft > conRight;
|
|
1649
|
+
const isPartialRight = elLeft < conRight && elRight > conRight;
|
|
1650
|
+
if (isAbove || isPartialAbove || isLeft || isPartialLeft) {
|
|
1651
|
+
calculateScrollToStart();
|
|
1448
1652
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1653
|
+
else if (isBelow || isPartialBelow || isRight || isPartialRight) {
|
|
1654
|
+
calculateScrollToEnd();
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
switch (origin) {
|
|
1658
|
+
case 'start':
|
|
1659
|
+
calculateScrollToStart();
|
|
1660
|
+
break;
|
|
1661
|
+
case 'end':
|
|
1662
|
+
calculateScrollToEnd();
|
|
1663
|
+
break;
|
|
1664
|
+
case 'center':
|
|
1665
|
+
calculateScrollToCenter();
|
|
1666
|
+
break;
|
|
1667
|
+
case 'nearest':
|
|
1668
|
+
calculateScrollToNearest();
|
|
1669
|
+
break;
|
|
1670
|
+
}
|
|
1671
|
+
return {
|
|
1672
|
+
behavior,
|
|
1673
|
+
left: shouldScrollLeft ? scrollLeftTo : undefined,
|
|
1674
|
+
top: shouldScrollTop ? scrollTopTo : undefined,
|
|
1675
|
+
};
|
|
1451
1676
|
};
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
* During that time, the signal will be set to `null`.
|
|
1455
|
-
*/
|
|
1456
|
-
const deferredSignal = (valueFn) => {
|
|
1457
|
-
const valueSignal = signal(null, ...(ngDevMode ? [{ debugName: "valueSignal" }] : []));
|
|
1458
|
-
afterNextRender(() => {
|
|
1459
|
-
valueSignal.set(valueFn());
|
|
1460
|
-
});
|
|
1461
|
-
return valueSignal;
|
|
1677
|
+
const scrollToElement = (options) => {
|
|
1678
|
+
options.container?.scrollTo(getElementScrollCoordinates(options));
|
|
1462
1679
|
};
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1680
|
+
|
|
1681
|
+
const MIN_SNAP_DELTA_PX = 1;
|
|
1682
|
+
const getScrollSnapTarget = (items, container, direction, origin, margin = 0) => {
|
|
1683
|
+
if (!items.length)
|
|
1684
|
+
return null;
|
|
1685
|
+
const containerRect = container.getBoundingClientRect();
|
|
1686
|
+
const containerSize = direction === 'horizontal' ? containerRect.width : containerRect.height;
|
|
1687
|
+
let bestElement = null;
|
|
1688
|
+
let bestOrigin = 'start';
|
|
1689
|
+
let bestAbsDelta = Infinity;
|
|
1690
|
+
const updateBest = (element, o, delta) => {
|
|
1691
|
+
const abs = Math.abs(delta);
|
|
1692
|
+
if (abs < bestAbsDelta) {
|
|
1693
|
+
bestAbsDelta = abs;
|
|
1694
|
+
bestElement = element;
|
|
1695
|
+
bestOrigin = o;
|
|
1696
|
+
}
|
|
1469
1697
|
};
|
|
1698
|
+
for (const item of items) {
|
|
1699
|
+
const itemRect = item.getBoundingClientRect();
|
|
1700
|
+
const itemSize = direction === 'horizontal' ? itemRect.width : itemRect.height;
|
|
1701
|
+
const relativeStart = direction === 'horizontal' ? itemRect.left - containerRect.left : itemRect.top - containerRect.top;
|
|
1702
|
+
const relativeEnd = relativeStart + itemSize;
|
|
1703
|
+
if (itemSize > containerSize) {
|
|
1704
|
+
const isStartEdgeVisible = relativeStart >= 0;
|
|
1705
|
+
const isEndEdgeVisible = relativeEnd <= containerSize;
|
|
1706
|
+
if (!isStartEdgeVisible && !isEndEdgeVisible)
|
|
1707
|
+
continue;
|
|
1708
|
+
if (isStartEdgeVisible) {
|
|
1709
|
+
updateBest(item, 'start', relativeStart - margin);
|
|
1710
|
+
}
|
|
1711
|
+
else {
|
|
1712
|
+
updateBest(item, 'end', relativeEnd - containerSize + margin);
|
|
1713
|
+
}
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
const computeDelta = (o) => {
|
|
1717
|
+
switch (o) {
|
|
1718
|
+
case 'start':
|
|
1719
|
+
return relativeStart - margin;
|
|
1720
|
+
case 'end':
|
|
1721
|
+
return relativeEnd - containerSize + margin;
|
|
1722
|
+
case 'center':
|
|
1723
|
+
return relativeStart + itemSize / 2 - containerSize / 2;
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
if (origin === 'auto') {
|
|
1727
|
+
for (const o of ['start', 'center', 'end']) {
|
|
1728
|
+
updateBest(item, o, computeDelta(o));
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
else {
|
|
1732
|
+
updateBest(item, origin, computeDelta(origin));
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if (!bestElement || bestAbsDelta < MIN_SNAP_DELTA_PX)
|
|
1736
|
+
return null;
|
|
1737
|
+
return { element: bestElement, origin: bestOrigin };
|
|
1738
|
+
};
|
|
1739
|
+
const getScrollContainerTarget = (entries, direction) => {
|
|
1740
|
+
const firstVisible = entries.find((i) => i.intersectionRatio > 0);
|
|
1741
|
+
const lastVisible = [...entries].reverse().find((i) => i.intersectionRatio > 0);
|
|
1742
|
+
const relevantEntry = direction === 'start' ? firstVisible : lastVisible;
|
|
1743
|
+
if (!relevantEntry)
|
|
1744
|
+
return null;
|
|
1745
|
+
const relevantIndex = entries.indexOf(relevantEntry);
|
|
1746
|
+
const nextIndex = relevantEntry.intersectionRatio !== 1
|
|
1747
|
+
? relevantIndex
|
|
1748
|
+
: direction === 'start'
|
|
1749
|
+
? relevantIndex - 1
|
|
1750
|
+
: relevantIndex + 1;
|
|
1751
|
+
const element = entries[nextIndex]?.target ?? relevantEntry.target;
|
|
1752
|
+
return { element, origin: direction === 'end' ? 'start' : 'end' };
|
|
1753
|
+
};
|
|
1754
|
+
const getScrollItemTarget = (entries, container, direction, scrollOrigin, axisDirection) => {
|
|
1755
|
+
const firstVisible = entries.find((i) => i.intersectionRatio > 0);
|
|
1756
|
+
const lastVisible = [...entries].reverse().find((i) => i.intersectionRatio > 0);
|
|
1757
|
+
if (!firstVisible || !lastVisible)
|
|
1758
|
+
return null;
|
|
1759
|
+
const firstIndex = entries.indexOf(firstVisible);
|
|
1760
|
+
const lastIndex = entries.indexOf(lastVisible);
|
|
1761
|
+
const containerRect = container.getBoundingClientRect();
|
|
1762
|
+
// Only a single item is visible — it must be oversized (wider/taller than the container).
|
|
1763
|
+
if (firstVisible === lastVisible) {
|
|
1764
|
+
const itemRect = firstVisible.target.getBoundingClientRect();
|
|
1765
|
+
const isStartEdgeVisible = axisDirection === 'horizontal'
|
|
1766
|
+
? Math.round(itemRect.left) >= Math.round(containerRect.left)
|
|
1767
|
+
: Math.round(itemRect.top) >= Math.round(containerRect.top);
|
|
1768
|
+
const isEndEdgeVisible = axisDirection === 'horizontal'
|
|
1769
|
+
? Math.round(itemRect.right) <= Math.round(containerRect.right)
|
|
1770
|
+
: Math.round(itemRect.bottom) <= Math.round(containerRect.bottom);
|
|
1771
|
+
if (!isStartEdgeVisible || !isEndEdgeVisible) {
|
|
1772
|
+
if (direction === 'start') {
|
|
1773
|
+
if (isStartEdgeVisible) {
|
|
1774
|
+
const prevIndex = firstIndex - 1;
|
|
1775
|
+
const element = entries[prevIndex]?.target;
|
|
1776
|
+
if (!element)
|
|
1777
|
+
return null;
|
|
1778
|
+
return { element, index: prevIndex, origin: 'end' };
|
|
1779
|
+
}
|
|
1780
|
+
return { element: firstVisible.target, index: firstIndex, origin: 'start' };
|
|
1781
|
+
}
|
|
1782
|
+
else {
|
|
1783
|
+
if (isEndEdgeVisible) {
|
|
1784
|
+
const nextIndex = lastIndex + 1;
|
|
1785
|
+
const element = entries[nextIndex]?.target;
|
|
1786
|
+
if (!element)
|
|
1787
|
+
return null;
|
|
1788
|
+
return { element, index: nextIndex, origin: 'start' };
|
|
1789
|
+
}
|
|
1790
|
+
return { element: lastVisible.target, index: lastIndex, origin: 'end' };
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
else if (scrollOrigin === 'center') {
|
|
1795
|
+
const entry = direction === 'start' ? firstVisible : lastVisible;
|
|
1796
|
+
const index = direction === 'start' ? firstIndex : lastIndex;
|
|
1797
|
+
return { element: entry.target, index, origin: 'center' };
|
|
1798
|
+
}
|
|
1799
|
+
const entry = direction === 'start' ? firstVisible : lastVisible;
|
|
1800
|
+
const entryIndex = direction === 'start' ? firstIndex : lastIndex;
|
|
1801
|
+
if (Math.round(entry.intersectionRatio) === 1) {
|
|
1802
|
+
if (direction === 'start' && entryIndex === 0)
|
|
1803
|
+
return null;
|
|
1804
|
+
if (direction === 'end' && entryIndex === entries.length - 1)
|
|
1805
|
+
return null;
|
|
1806
|
+
const nextIndex = direction === 'start' ? entryIndex - 1 : entryIndex + 1;
|
|
1807
|
+
const element = entries[nextIndex]?.target;
|
|
1808
|
+
if (!element)
|
|
1809
|
+
return null;
|
|
1810
|
+
return { element, index: nextIndex, origin: direction };
|
|
1811
|
+
}
|
|
1812
|
+
return { element: entry.target, index: entryIndex, origin: direction };
|
|
1470
1813
|
};
|
|
1471
1814
|
|
|
1472
|
-
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
const
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1503
|
-
const
|
|
1504
|
-
const
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1815
|
+
const createPositionObject = (entry) => {
|
|
1816
|
+
const boundingRect = entry.boundingClientRect;
|
|
1817
|
+
const rootBounds = entry.rootBounds;
|
|
1818
|
+
let isAbove = false;
|
|
1819
|
+
let isBelow = false;
|
|
1820
|
+
let isLeft = false;
|
|
1821
|
+
let isRight = false;
|
|
1822
|
+
if (rootBounds) {
|
|
1823
|
+
isAbove = boundingRect.bottom < rootBounds.top;
|
|
1824
|
+
isBelow = boundingRect.top > rootBounds.bottom;
|
|
1825
|
+
isLeft = boundingRect.right < rootBounds.left;
|
|
1826
|
+
isRight = boundingRect.left > rootBounds.right;
|
|
1827
|
+
}
|
|
1828
|
+
// We cant use entry.isIntersecting to determine actual visibility since we are using a big threshold array to get more intersection events.
|
|
1829
|
+
const isVisible = !isAbove && !isBelow && !isLeft && !isRight && entry.intersectionRatio > 0;
|
|
1830
|
+
return {
|
|
1831
|
+
isAbove,
|
|
1832
|
+
isBelow,
|
|
1833
|
+
isLeft,
|
|
1834
|
+
isRight,
|
|
1835
|
+
isVisible,
|
|
1836
|
+
};
|
|
1837
|
+
};
|
|
1838
|
+
const signalElementIntersection = (el, options) => {
|
|
1839
|
+
const destroyRef = inject(DestroyRef);
|
|
1840
|
+
const elements = buildElementSignal(el);
|
|
1841
|
+
const root = firstElementSignal(options?.root ? buildElementSignal(options?.root) : createEmptyElementSignal());
|
|
1842
|
+
const zone = inject(NgZone);
|
|
1843
|
+
const isRendered = signalIsRendered();
|
|
1844
|
+
const isEnabled = options?.enabled ?? signal(true);
|
|
1845
|
+
const elementIntersectionSignal = signal([], ...(ngDevMode ? [{ debugName: "elementIntersectionSignal" }] : []));
|
|
1846
|
+
const observer = signal(null, ...(ngDevMode ? [{ debugName: "observer" }] : []));
|
|
1847
|
+
const currentlyObservedElements = new Set();
|
|
1848
|
+
const updateIntersections = (entries) => {
|
|
1849
|
+
let currentValues = [...elementIntersectionSignal()];
|
|
1850
|
+
for (const entry of entries) {
|
|
1851
|
+
const existingEntryIndex = currentValues.findIndex((v) => v.target === entry.target);
|
|
1852
|
+
// Round the intersection ratio to the nearest 0.01 to avoid floating point errors and system scaling issues.
|
|
1853
|
+
const roundedIntersectionRatio = Math.round(entry.intersectionRatio * 100) / 100;
|
|
1854
|
+
const intersectionEntry = {
|
|
1855
|
+
boundingClientRect: entry.boundingClientRect,
|
|
1856
|
+
intersectionRatio: roundedIntersectionRatio,
|
|
1857
|
+
intersectionRect: entry.intersectionRect,
|
|
1858
|
+
isIntersecting: entry.isIntersecting,
|
|
1859
|
+
rootBounds: entry.rootBounds,
|
|
1860
|
+
target: entry.target,
|
|
1861
|
+
time: entry.time,
|
|
1862
|
+
...createPositionObject(entry),
|
|
1863
|
+
};
|
|
1864
|
+
if (existingEntryIndex !== -1) {
|
|
1865
|
+
currentValues = [
|
|
1866
|
+
...currentValues.slice(0, existingEntryIndex),
|
|
1867
|
+
intersectionEntry,
|
|
1868
|
+
...currentValues.slice(existingEntryIndex + 1),
|
|
1869
|
+
];
|
|
1870
|
+
}
|
|
1871
|
+
else {
|
|
1872
|
+
currentValues = [...currentValues, intersectionEntry];
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
zone.run(() => elementIntersectionSignal.set(currentValues));
|
|
1876
|
+
};
|
|
1877
|
+
const updateIntersectionObserver = (rendered, enabled, rootEl) => {
|
|
1878
|
+
observer()?.disconnect();
|
|
1879
|
+
currentlyObservedElements.clear();
|
|
1880
|
+
if (!rendered || !enabled) {
|
|
1881
|
+
observer.set(null);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const newObserver = new IntersectionObserver((entries) => updateIntersections(entries), {
|
|
1885
|
+
...options,
|
|
1886
|
+
root: rootEl,
|
|
1887
|
+
});
|
|
1888
|
+
observer.set(newObserver);
|
|
1889
|
+
};
|
|
1890
|
+
const updateObservedElements = (observer, elements) => {
|
|
1891
|
+
const rootEl = root().currentElement;
|
|
1892
|
+
if (!observer)
|
|
1893
|
+
return;
|
|
1894
|
+
const currIntersectionValue = elementIntersectionSignal();
|
|
1895
|
+
const newIntersectionValue = [];
|
|
1896
|
+
for (const el of elements.currentElements) {
|
|
1897
|
+
if (currentlyObservedElements.has(el)) {
|
|
1898
|
+
const existingEntryIndex = currIntersectionValue.findIndex((v) => v.target === el);
|
|
1899
|
+
const existingEntry = currIntersectionValue[existingEntryIndex];
|
|
1900
|
+
if (!existingEntry) {
|
|
1901
|
+
console.warn('Could not find existing entry for element. The intersection observer might be broken now.', el);
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
newIntersectionValue.push(existingEntry);
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
const initialElementVisibility = isElementVisible({
|
|
1908
|
+
container: rootEl,
|
|
1909
|
+
element: el,
|
|
1910
|
+
});
|
|
1911
|
+
if (!initialElementVisibility) {
|
|
1912
|
+
console.error('No visibility data found for element.', {
|
|
1913
|
+
element: el,
|
|
1914
|
+
container: rootEl,
|
|
1915
|
+
});
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1918
|
+
const intersectionEntry = {
|
|
1919
|
+
boundingClientRect: initialElementVisibility.elementRect,
|
|
1920
|
+
intersectionRatio: initialElementVisibility.intersectionRatio,
|
|
1921
|
+
intersectionRect: initialElementVisibility.elementRect,
|
|
1922
|
+
isIntersecting: initialElementVisibility.isIntersecting,
|
|
1923
|
+
rootBounds: initialElementVisibility.containerRect,
|
|
1924
|
+
target: el,
|
|
1925
|
+
time: performance.now(),
|
|
1926
|
+
};
|
|
1927
|
+
newIntersectionValue.push({
|
|
1928
|
+
...intersectionEntry,
|
|
1929
|
+
...createPositionObject(intersectionEntry),
|
|
1930
|
+
});
|
|
1931
|
+
currentlyObservedElements.add(el);
|
|
1932
|
+
observer.observe(el);
|
|
1933
|
+
}
|
|
1934
|
+
for (const el of elements.previousElements) {
|
|
1935
|
+
if (elements.currentElements.includes(el))
|
|
1936
|
+
continue;
|
|
1937
|
+
observer.unobserve(el);
|
|
1938
|
+
currentlyObservedElements.delete(el);
|
|
1525
1939
|
}
|
|
1940
|
+
elementIntersectionSignal.set(newIntersectionValue);
|
|
1941
|
+
};
|
|
1942
|
+
effect(() => {
|
|
1943
|
+
const rootEl = root().currentElement;
|
|
1944
|
+
const rendered = isRendered();
|
|
1945
|
+
const enabled = isEnabled();
|
|
1946
|
+
untracked(() => updateIntersectionObserver(rendered, enabled, rootEl));
|
|
1526
1947
|
});
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
const injectIsLandscape = memoizeSignal(() => injectObserveMediaQuery('(orientation: landscape)'));
|
|
1532
|
-
/** Inject a signal containing the current display orientation */
|
|
1533
|
-
const injectDisplayOrientation = memoizeSignal(() => {
|
|
1534
|
-
const isPortrait = injectIsPortrait();
|
|
1535
|
-
return computed(() => {
|
|
1536
|
-
if (isPortrait())
|
|
1537
|
-
return 'portrait';
|
|
1538
|
-
return 'landscape';
|
|
1948
|
+
effect(() => {
|
|
1949
|
+
const els = elements();
|
|
1950
|
+
const obs = observer();
|
|
1951
|
+
untracked(() => updateObservedElements(obs, els));
|
|
1539
1952
|
});
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
const
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1953
|
+
destroyRef.onDestroy(() => observer()?.disconnect());
|
|
1954
|
+
return elementIntersectionSignal.asReadonly();
|
|
1955
|
+
};
|
|
1956
|
+
const signalHostElementIntersection = (options) => signalElementIntersection(inject(ElementRef), options);
|
|
1957
|
+
|
|
1958
|
+
const signalElementLastScrollDirection = (el) => {
|
|
1959
|
+
const elements = buildElementSignal(el);
|
|
1960
|
+
const element = firstElementSignal(elements);
|
|
1961
|
+
const destroyRef = inject(DestroyRef);
|
|
1962
|
+
const lastScrollDirection = signal(null, ...(ngDevMode ? [{ debugName: "lastScrollDirection" }] : []));
|
|
1963
|
+
let lastScrollTop = 0;
|
|
1964
|
+
let lastScrollLeft = 0;
|
|
1965
|
+
toObservable(element)
|
|
1966
|
+
.pipe(switchMap(({ currentElement }) => {
|
|
1967
|
+
if (!currentElement) {
|
|
1968
|
+
lastScrollDirection.set(null);
|
|
1969
|
+
lastScrollTop = 0;
|
|
1970
|
+
lastScrollLeft = 0;
|
|
1971
|
+
return of(null);
|
|
1972
|
+
}
|
|
1973
|
+
return fromEvent(currentElement, 'scroll').pipe(tap(() => {
|
|
1974
|
+
const { scrollTop, scrollLeft } = currentElement;
|
|
1975
|
+
const time = Date.now();
|
|
1976
|
+
if (scrollTop > lastScrollTop) {
|
|
1977
|
+
lastScrollDirection.set({ type: 'down', time });
|
|
1978
|
+
}
|
|
1979
|
+
else if (scrollTop < lastScrollTop) {
|
|
1980
|
+
lastScrollDirection.set({ type: 'up', time });
|
|
1981
|
+
}
|
|
1982
|
+
else if (scrollLeft > lastScrollLeft) {
|
|
1983
|
+
lastScrollDirection.set({ type: 'right', time });
|
|
1984
|
+
}
|
|
1985
|
+
else if (scrollLeft < lastScrollLeft) {
|
|
1986
|
+
lastScrollDirection.set({ type: 'left', time });
|
|
1987
|
+
}
|
|
1988
|
+
lastScrollTop = scrollTop;
|
|
1989
|
+
lastScrollLeft = scrollLeft;
|
|
1990
|
+
}));
|
|
1991
|
+
}), takeUntilDestroyed(destroyRef))
|
|
1992
|
+
.subscribe();
|
|
1993
|
+
return lastScrollDirection.asReadonly();
|
|
1994
|
+
};
|
|
1995
|
+
const signalHostElementLastScrollDirection = () => signalElementLastScrollDirection(inject(ElementRef));
|
|
1996
|
+
|
|
1997
|
+
const areScrollStatesEqual = (a, b) => {
|
|
1998
|
+
return (a.canScroll === b.canScroll &&
|
|
1999
|
+
a.canScrollHorizontally === b.canScrollHorizontally &&
|
|
2000
|
+
a.canScrollVertically === b.canScrollVertically &&
|
|
2001
|
+
equal(a.elementDimensions, b.elementDimensions));
|
|
2002
|
+
};
|
|
2003
|
+
const signalElementScrollState = (el, options) => {
|
|
2004
|
+
const elements = buildElementSignal(el);
|
|
2005
|
+
const observedEl = firstElementSignal(elements);
|
|
2006
|
+
const elementDimensions = signalElementDimensions(elements);
|
|
2007
|
+
const elementMutations = signalElementMutations(elements, { childList: true, subtree: true, attributes: true });
|
|
2008
|
+
const isRendered = signalIsRendered();
|
|
2009
|
+
const initialScrollPosition = options?.initialScrollPosition;
|
|
2010
|
+
if (initialScrollPosition) {
|
|
2011
|
+
const ref = effect(() => {
|
|
2012
|
+
if (!isRendered())
|
|
2013
|
+
return;
|
|
2014
|
+
const scrollPosition = initialScrollPosition();
|
|
2015
|
+
const element = observedEl().currentElement;
|
|
2016
|
+
if (scrollPosition && element) {
|
|
2017
|
+
if (scrollPosition.left !== undefined)
|
|
2018
|
+
element.scrollLeft = scrollPosition.left;
|
|
2019
|
+
if (scrollPosition.top !== undefined)
|
|
2020
|
+
element.scrollTop = scrollPosition.top;
|
|
2021
|
+
ref.destroy();
|
|
2022
|
+
}
|
|
2023
|
+
}, ...(ngDevMode ? [{ debugName: "ref" }] : []));
|
|
2024
|
+
}
|
|
2025
|
+
const notScrollable = (dimensions) => ({
|
|
2026
|
+
canScroll: false,
|
|
2027
|
+
canScrollHorizontally: false,
|
|
2028
|
+
canScrollVertically: false,
|
|
2029
|
+
elementDimensions: dimensions,
|
|
1552
2030
|
});
|
|
1553
|
-
});
|
|
1554
|
-
/** Inject a signal containing a boolean value indicating if the user can hover (eg. using a mouse) */
|
|
1555
|
-
const injectCanHover = memoizeSignal(() => injectObserveMediaQuery('(hover: hover)'));
|
|
1556
|
-
/** Inject a signal containing the viewport dimensions */
|
|
1557
|
-
const injectViewportDimensions = memoizeSignal(() => signalElementDimensions(createDocumentElementSignal()));
|
|
1558
|
-
/** Inject a signal containing the scrollbar dimensions. Dimensions will be 0 if scrollbars overlap the page contents (like on mobile). */
|
|
1559
|
-
const injectScrollbarDimensions = memoizeSignal(() => {
|
|
1560
|
-
const document = inject(DOCUMENT);
|
|
1561
|
-
const renderer = injectRenderer();
|
|
1562
|
-
const scrollbarRuler = renderer.createElement('div');
|
|
1563
|
-
scrollbarRuler.style.width = '100px';
|
|
1564
|
-
scrollbarRuler.style.height = '100px';
|
|
1565
|
-
scrollbarRuler.style.overflow = 'scroll';
|
|
1566
|
-
scrollbarRuler.style.position = 'absolute';
|
|
1567
|
-
scrollbarRuler.style.top = '-9999px';
|
|
1568
|
-
scrollbarRuler.style.scrollbarWidth = getComputedStyle(document.documentElement).scrollbarWidth;
|
|
1569
|
-
renderer.appendChild(document.body, scrollbarRuler);
|
|
1570
|
-
const scrollContainerDimensions = signalElementDimensions(scrollbarRuler);
|
|
1571
2031
|
return computed(() => {
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
|
|
2032
|
+
const element = observedEl().currentElement;
|
|
2033
|
+
const dimensions = elementDimensions();
|
|
2034
|
+
// We are not interested what the mutation is, just that there is one.
|
|
2035
|
+
// Changes to the DOM can affect the scroll state of the element.
|
|
2036
|
+
elementMutations();
|
|
2037
|
+
if (!element || !isRendered())
|
|
2038
|
+
return notScrollable(dimensions);
|
|
2039
|
+
const { scrollWidth, scrollHeight, clientHeight, clientWidth } = element;
|
|
2040
|
+
const canScrollHorizontally = scrollWidth > clientWidth;
|
|
2041
|
+
const canScrollVertically = scrollHeight > clientHeight;
|
|
1575
2042
|
return {
|
|
1576
|
-
|
|
1577
|
-
|
|
2043
|
+
canScroll: canScrollHorizontally || canScrollVertically,
|
|
2044
|
+
canScrollHorizontally,
|
|
2045
|
+
canScrollVertically,
|
|
2046
|
+
elementDimensions: dimensions,
|
|
1578
2047
|
};
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
2048
|
+
}, { equal: (a, b) => areScrollStatesEqual(a, b) });
|
|
2049
|
+
};
|
|
2050
|
+
const signalHostElementScrollState = () => signalElementScrollState(inject(ElementRef));
|
|
1581
2051
|
|
|
1582
2052
|
let hasWrittenScrollbarSizes = false;
|
|
1583
2053
|
/**
|
|
@@ -1918,384 +2388,224 @@ const injectRouteTitle = (config) => {
|
|
|
1918
2388
|
const title = computed(() => routerState().title, ...(ngDevMode ? [{ debugName: "title" }] : []));
|
|
1919
2389
|
return transformOrReturn(title, config);
|
|
1920
2390
|
};
|
|
1921
|
-
/** Inject all currently available path parameters as a signal */
|
|
1922
|
-
const injectPathParams = () => {
|
|
1923
|
-
const routerState = injectRouterState();
|
|
1924
|
-
const pathParams = computed(() => routerState().pathParams, ...(ngDevMode ? [{ debugName: "pathParams" }] : []));
|
|
1925
|
-
return pathParams;
|
|
1926
|
-
};
|
|
1927
|
-
const getQueryParamFromUrl = (key) => {
|
|
1928
|
-
if (typeof window === 'undefined')
|
|
1929
|
-
return null;
|
|
1930
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
1931
|
-
return urlParams.get(key);
|
|
1932
|
-
};
|
|
1933
|
-
/** Inject a specific query parameter as a signal */
|
|
1934
|
-
const injectQueryParam = (key, config) => {
|
|
1935
|
-
const queryParams = injectQueryParams();
|
|
1936
|
-
const src = computed(() => queryParams()[key] ?? (config?.requireSync ? getQueryParamFromUrl(key) : null));
|
|
1937
|
-
return transformOrReturn(src, config);
|
|
1938
|
-
};
|
|
1939
|
-
/** Inject a specific route data item as a signal */
|
|
1940
|
-
const injectRouteDataItem = (key, config) => {
|
|
1941
|
-
const data = injectRouteData();
|
|
1942
|
-
const src = computed(() => data()[key] ?? null);
|
|
1943
|
-
return transformOrReturn(src, config);
|
|
1944
|
-
};
|
|
1945
|
-
/** Inject a specific path parameter as a signal */
|
|
1946
|
-
const injectPathParam = (key, config) => {
|
|
1947
|
-
const pathParams = injectPathParams();
|
|
1948
|
-
const src = computed(() => pathParams()[key] ?? null);
|
|
1949
|
-
return transformOrReturn(src, config);
|
|
1950
|
-
};
|
|
1951
|
-
/**
|
|
1952
|
-
* Inject query params that changed during navigation. Unchanged query params will be ignored.
|
|
1953
|
-
* Removed query params will be represented by the symbol `ET_PROPERTY_REMOVED`.
|
|
1954
|
-
*/
|
|
1955
|
-
const injectQueryParamChanges = memoizeSignal(() => {
|
|
1956
|
-
const queryParams = injectQueryParams();
|
|
1957
|
-
const prevQueryParams = previousSignalValue(queryParams);
|
|
1958
|
-
return computed(() => {
|
|
1959
|
-
const current = queryParams();
|
|
1960
|
-
const previous = prevQueryParams() ?? {};
|
|
1961
|
-
const changes = {};
|
|
1962
|
-
const allKeys = new Set([
|
|
1963
|
-
...Object.keys(previous),
|
|
1964
|
-
...Object.keys(current),
|
|
1965
|
-
]);
|
|
1966
|
-
for (const key of allKeys) {
|
|
1967
|
-
if (!equal(previous[key], current[key])) {
|
|
1968
|
-
const val = current[key] === undefined ? ET_PROPERTY_REMOVED : current[key];
|
|
1969
|
-
changes[key] = val;
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
return changes;
|
|
1973
|
-
});
|
|
1974
|
-
});
|
|
1975
|
-
/**
|
|
1976
|
-
* Inject path params that changed during navigation. Unchanged path params will be ignored.
|
|
1977
|
-
* Removed path params will be represented by the symbol `ET_PROPERTY_REMOVED`.
|
|
1978
|
-
*/
|
|
1979
|
-
const injectPathParamChanges = memoizeSignal(() => {
|
|
1980
|
-
const pathParams = injectPathParams();
|
|
1981
|
-
const prevPathParams = previousSignalValue(pathParams);
|
|
1982
|
-
return computed(() => {
|
|
1983
|
-
const current = pathParams();
|
|
1984
|
-
const previous = prevPathParams() ?? {};
|
|
1985
|
-
const changes = {};
|
|
1986
|
-
const allKeys = new Set([
|
|
1987
|
-
...Object.keys(previous),
|
|
1988
|
-
...Object.keys(current),
|
|
1989
|
-
]);
|
|
1990
|
-
for (const key of allKeys) {
|
|
1991
|
-
if (!equal(previous[key], current[key])) {
|
|
1992
|
-
const val = current[key] === undefined ? ET_PROPERTY_REMOVED : current[key];
|
|
1993
|
-
changes[key] = val;
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
return changes;
|
|
1997
|
-
});
|
|
1998
|
-
});
|
|
1999
|
-
|
|
2000
|
-
const ET_DISABLE_SCROLL_TOP = Symbol('ET_DISABLE_SCROLL_TOP');
|
|
2001
|
-
const ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE = Symbol('ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE');
|
|
2002
|
-
const ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE = Symbol('ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE');
|
|
2003
|
-
const routerDisableScrollTop = (config = {}) => {
|
|
2004
|
-
return {
|
|
2005
|
-
...(!config.asReturnRoute ? { [ET_DISABLE_SCROLL_TOP]: true } : { [ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE]: true }),
|
|
2006
|
-
...(config.onPathParamChange ? { [ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE]: true } : {}),
|
|
2007
|
-
};
|
|
2008
|
-
};
|
|
2009
|
-
const setupScrollRestoration = (config = {}) => {
|
|
2010
|
-
if (!isPlatformBrowser(inject(PLATFORM_ID))) {
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2013
|
-
const state = injectRouterState();
|
|
2014
|
-
const route = injectRoute();
|
|
2015
|
-
const document = inject(DOCUMENT);
|
|
2016
|
-
combineLatest([toObservable(state).pipe(pairwise()), toObservable(route).pipe(pairwise())])
|
|
2017
|
-
.pipe(debounceTime(1))
|
|
2018
|
-
.subscribe(([[prevState, currState], [prevRoute, currRoute]]) => {
|
|
2019
|
-
const sameUrlNavigation = prevRoute === currRoute;
|
|
2020
|
-
const didFragmentChange = prevState.fragment !== currState.fragment;
|
|
2021
|
-
if (sameUrlNavigation) {
|
|
2022
|
-
const allQueryParams = [
|
|
2023
|
-
...new Set(Object.keys(prevState.queryParams).concat(Object.keys(currState.queryParams))),
|
|
2024
|
-
];
|
|
2025
|
-
const changedQueryParams = allQueryParams.filter((key) => currState.queryParams[key] !== prevState.queryParams[key]);
|
|
2026
|
-
if (!config.queryParamTriggerList?.length && !didFragmentChange) {
|
|
2027
|
-
return;
|
|
2028
|
-
}
|
|
2029
|
-
const caseQueryParams = changedQueryParams.some((key) => config.queryParamTriggerList?.includes(key));
|
|
2030
|
-
const caseFragment = didFragmentChange && config.fragment?.enabled;
|
|
2031
|
-
if (caseQueryParams) {
|
|
2032
|
-
(config.scrollElement ?? document.documentElement).scrollTop = 0;
|
|
2033
|
-
}
|
|
2034
|
-
else if (caseFragment) {
|
|
2035
|
-
const fragmentElement = document.getElementById(currState.fragment ?? '');
|
|
2036
|
-
if (fragmentElement) {
|
|
2037
|
-
fragmentElement.scrollIntoView({ behavior: config.fragment?.smooth ? 'smooth' : 'auto' });
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
else {
|
|
2042
|
-
const viaReturnRoute = currState.data[ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE] && prevState.data[ET_DISABLE_SCROLL_TOP];
|
|
2043
|
-
const explicitly = currState.data[ET_DISABLE_SCROLL_TOP];
|
|
2044
|
-
const pathParamsChange = currState.data[ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE];
|
|
2045
|
-
if (viaReturnRoute || explicitly || pathParamsChange) {
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
const el = config.scrollElement ?? document.documentElement;
|
|
2049
|
-
el.scrollTop = 0;
|
|
2050
|
-
}
|
|
2051
|
-
});
|
|
2052
|
-
};
|
|
2053
|
-
|
|
2054
|
-
const DISABLE_LOGGER_PARAM = 'et-logger-quiet';
|
|
2055
|
-
const createLogger = (config) => {
|
|
2056
|
-
const { scope, feature } = config;
|
|
2057
|
-
const disableLogging = injectQueryParam(DISABLE_LOGGER_PARAM);
|
|
2058
|
-
const writeLog = (...args) => {
|
|
2059
|
-
if (disableLogging()) {
|
|
2060
|
-
return;
|
|
2061
|
-
}
|
|
2062
|
-
console.log(...args);
|
|
2063
|
-
};
|
|
2064
|
-
return {
|
|
2065
|
-
log: (...args) => writeLog(`\x1B[32;40;24m[${scope} ${feature}]\x1B[m`, ...args),
|
|
2066
|
-
warn: (...args) => writeLog(`\x1B[93;40;24m[${scope} ${feature}]\x1B[m`, ...args),
|
|
2067
|
-
error: (...args) => writeLog(`\x1B[31;40;24m[${scope} ${feature}]\x1B[m`, ...args),
|
|
2068
|
-
};
|
|
2069
|
-
};
|
|
2070
|
-
|
|
2071
|
-
const clamp = (value, min = 0, max = 100) => {
|
|
2072
|
-
return Math.max(min, Math.min(max, value));
|
|
2391
|
+
/** Inject all currently available path parameters as a signal */
|
|
2392
|
+
const injectPathParams = () => {
|
|
2393
|
+
const routerState = injectRouterState();
|
|
2394
|
+
const pathParams = computed(() => routerState().pathParams, ...(ngDevMode ? [{ debugName: "pathParams" }] : []));
|
|
2395
|
+
return pathParams;
|
|
2073
2396
|
};
|
|
2074
|
-
const
|
|
2075
|
-
|
|
2076
|
-
|
|
2397
|
+
const getQueryParamFromUrl = (key) => {
|
|
2398
|
+
if (typeof window === 'undefined')
|
|
2399
|
+
return null;
|
|
2400
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
2401
|
+
return urlParams.get(key);
|
|
2077
2402
|
};
|
|
2078
|
-
|
|
2079
|
-
const
|
|
2080
|
-
|
|
2403
|
+
/** Inject a specific query parameter as a signal */
|
|
2404
|
+
const injectQueryParam = (key, config) => {
|
|
2405
|
+
const queryParams = injectQueryParams();
|
|
2406
|
+
const src = computed(() => queryParams()[key] ?? (config?.requireSync ? getQueryParamFromUrl(key) : null));
|
|
2407
|
+
return transformOrReturn(src, config);
|
|
2081
2408
|
};
|
|
2082
|
-
|
|
2083
|
-
|
|
2409
|
+
/** Inject a specific route data item as a signal */
|
|
2410
|
+
const injectRouteDataItem = (key, config) => {
|
|
2411
|
+
const data = injectRouteData();
|
|
2412
|
+
const src = computed(() => data()[key] ?? null);
|
|
2413
|
+
return transformOrReturn(src, config);
|
|
2084
2414
|
};
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
const
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
const props = prop.split('.');
|
|
2091
|
-
let value = obj;
|
|
2092
|
-
for (const prop of props) {
|
|
2093
|
-
if (!isObject(value))
|
|
2094
|
-
return undefined;
|
|
2095
|
-
if (prop.includes('[')) {
|
|
2096
|
-
const [key, index] = prop.split('[').map((part) => part.replace(']', ''));
|
|
2097
|
-
const arr = value[key];
|
|
2098
|
-
if (!Array.isArray(arr))
|
|
2099
|
-
return undefined;
|
|
2100
|
-
value = arr[+index];
|
|
2101
|
-
}
|
|
2102
|
-
else {
|
|
2103
|
-
value = value[prop];
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
return value;
|
|
2415
|
+
/** Inject a specific path parameter as a signal */
|
|
2416
|
+
const injectPathParam = (key, config) => {
|
|
2417
|
+
const pathParams = injectPathParams();
|
|
2418
|
+
const src = computed(() => pathParams()[key] ?? null);
|
|
2419
|
+
return transformOrReturn(src, config);
|
|
2107
2420
|
};
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2421
|
+
/**
|
|
2422
|
+
* Inject query params that changed during navigation. Unchanged query params will be ignored.
|
|
2423
|
+
* Removed query params will be represented by the symbol `ET_PROPERTY_REMOVED`.
|
|
2424
|
+
*/
|
|
2425
|
+
const injectQueryParamChanges = memoizeSignal(() => {
|
|
2426
|
+
const queryParams = injectQueryParams();
|
|
2427
|
+
const prevQueryParams = previousSignalValue(queryParams);
|
|
2428
|
+
return computed(() => {
|
|
2429
|
+
const current = queryParams();
|
|
2430
|
+
const previous = prevQueryParams() ?? {};
|
|
2431
|
+
const changes = {};
|
|
2432
|
+
const allKeys = new Set([
|
|
2433
|
+
...Object.keys(previous),
|
|
2434
|
+
...Object.keys(current),
|
|
2435
|
+
]);
|
|
2436
|
+
for (const key of allKeys) {
|
|
2437
|
+
if (!equal(previous[key], current[key])) {
|
|
2438
|
+
const val = current[key] === undefined ? ET_PROPERTY_REMOVED : current[key];
|
|
2439
|
+
changes[key] = val;
|
|
2127
2440
|
}
|
|
2128
2441
|
}
|
|
2129
|
-
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
// prefix code with zeros if it's less than 100
|
|
2133
|
-
const codeWithZeros = code < 10 ? `00${code}` : code < 100 ? `0${code}` : code;
|
|
2134
|
-
const fullCode = `ET${codeWithZeros}`;
|
|
2135
|
-
const devOnlyText = devOnly ? ' [DEV ONLY] ' : '';
|
|
2136
|
-
return `${devOnlyText}${fullCode}${message ? ': ' + message : ''}`;
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2442
|
+
return changes;
|
|
2443
|
+
});
|
|
2444
|
+
});
|
|
2139
2445
|
/**
|
|
2140
|
-
*
|
|
2141
|
-
*
|
|
2142
|
-
* @param direction The direction to check. If not provided, checks both directions.
|
|
2143
|
-
* @returns true if the element or viewport can scroll in the given direction.
|
|
2446
|
+
* Inject path params that changed during navigation. Unchanged path params will be ignored.
|
|
2447
|
+
* Removed path params will be represented by the symbol `ET_PROPERTY_REMOVED`.
|
|
2144
2448
|
*/
|
|
2145
|
-
const
|
|
2146
|
-
const
|
|
2147
|
-
const
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
y: 0,
|
|
2165
|
-
toJSON: () => ({}),
|
|
2449
|
+
const injectPathParamChanges = memoizeSignal(() => {
|
|
2450
|
+
const pathParams = injectPathParams();
|
|
2451
|
+
const prevPathParams = previousSignalValue(pathParams);
|
|
2452
|
+
return computed(() => {
|
|
2453
|
+
const current = pathParams();
|
|
2454
|
+
const previous = prevPathParams() ?? {};
|
|
2455
|
+
const changes = {};
|
|
2456
|
+
const allKeys = new Set([
|
|
2457
|
+
...Object.keys(previous),
|
|
2458
|
+
...Object.keys(current),
|
|
2459
|
+
]);
|
|
2460
|
+
for (const key of allKeys) {
|
|
2461
|
+
if (!equal(previous[key], current[key])) {
|
|
2462
|
+
const val = current[key] === undefined ? ET_PROPERTY_REMOVED : current[key];
|
|
2463
|
+
changes[key] = val;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return changes;
|
|
2467
|
+
});
|
|
2166
2468
|
});
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
blockIntersection: blockIntersectionPercentage,
|
|
2209
|
-
isIntersecting: isElementInlineVisible && isElementBlockVisible,
|
|
2210
|
-
element,
|
|
2211
|
-
containerRect,
|
|
2212
|
-
elementRect,
|
|
2213
|
-
// Round the intersection ratio to the nearest 0.01 to avoid floating point errors and system scaling issues.
|
|
2214
|
-
intersectionRatio: Math.round(Math.min(inlineIntersectionPercentage, blockIntersectionPercentage) * 100) / 100,
|
|
2215
|
-
};
|
|
2216
|
-
};
|
|
2217
|
-
const getElementScrollCoordinates = (options) => {
|
|
2218
|
-
const { container, element, direction, behavior = 'smooth', origin = 'nearest', scrollBlockMargin = 0, scrollInlineMargin = 0, } = options;
|
|
2219
|
-
if (!element || !container || !elementCanScroll(container)) {
|
|
2220
|
-
return {
|
|
2221
|
-
behavior,
|
|
2222
|
-
left: undefined,
|
|
2223
|
-
top: undefined,
|
|
2224
|
-
};
|
|
2225
|
-
}
|
|
2226
|
-
const elementRect = element.getBoundingClientRect();
|
|
2227
|
-
const containerRect = container.getBoundingClientRect();
|
|
2228
|
-
const { scrollLeft, scrollTop } = container;
|
|
2229
|
-
const elWidth = elementRect.width;
|
|
2230
|
-
const elHeight = elementRect.height;
|
|
2231
|
-
const elLeft = elementRect.left;
|
|
2232
|
-
const elTop = elementRect.top;
|
|
2233
|
-
const elRight = elementRect.right;
|
|
2234
|
-
const elBottom = elementRect.bottom;
|
|
2235
|
-
const conWidth = containerRect.width;
|
|
2236
|
-
const conHeight = containerRect.height;
|
|
2237
|
-
const conLeft = containerRect.left;
|
|
2238
|
-
const conTop = containerRect.top;
|
|
2239
|
-
const conRight = containerRect.right;
|
|
2240
|
-
const conBottom = containerRect.bottom;
|
|
2241
|
-
const shouldScrollLeft = direction === 'inline' || direction === 'both' || !direction;
|
|
2242
|
-
const shouldScrollTop = direction === 'block' || direction === 'both' || !direction;
|
|
2243
|
-
let scrollLeftTo = scrollLeft;
|
|
2244
|
-
let scrollTopTo = scrollTop;
|
|
2245
|
-
const relativeTop = elTop - conTop;
|
|
2246
|
-
const relativeLeft = elLeft - conLeft;
|
|
2247
|
-
const calculateScrollToStart = () => {
|
|
2248
|
-
scrollLeftTo = scrollLeft + relativeLeft - scrollInlineMargin;
|
|
2249
|
-
scrollTopTo = scrollTop + relativeTop - scrollBlockMargin;
|
|
2250
|
-
};
|
|
2251
|
-
const calculateScrollToEnd = () => {
|
|
2252
|
-
scrollLeftTo = scrollLeft + relativeLeft - conWidth + elWidth + scrollInlineMargin;
|
|
2253
|
-
scrollTopTo = scrollTop + relativeTop - conHeight + elHeight + scrollBlockMargin;
|
|
2254
|
-
};
|
|
2255
|
-
const calculateScrollToCenter = () => {
|
|
2256
|
-
scrollLeftTo = scrollLeft + relativeLeft - conWidth / 2 + elWidth / 2;
|
|
2257
|
-
scrollTopTo = scrollTop + relativeTop - conHeight / 2 + elHeight / 2;
|
|
2258
|
-
};
|
|
2259
|
-
const calculateScrollToNearest = () => {
|
|
2260
|
-
const isAbove = elBottom < conTop;
|
|
2261
|
-
const isPartialAbove = elTop < conTop && elBottom > conTop;
|
|
2262
|
-
const isBelow = elTop > conBottom;
|
|
2263
|
-
const isPartialBelow = elTop < conBottom && elBottom > conBottom;
|
|
2264
|
-
const isLeft = elRight < conLeft;
|
|
2265
|
-
const isPartialLeft = elLeft < conLeft && elRight > conLeft;
|
|
2266
|
-
const isRight = elLeft > conRight;
|
|
2267
|
-
const isPartialRight = elLeft < conRight && elRight > conRight;
|
|
2268
|
-
if (isAbove || isPartialAbove || isLeft || isPartialLeft) {
|
|
2269
|
-
calculateScrollToStart();
|
|
2469
|
+
|
|
2470
|
+
const ET_DISABLE_SCROLL_TOP = Symbol('ET_DISABLE_SCROLL_TOP');
|
|
2471
|
+
const ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE = Symbol('ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE');
|
|
2472
|
+
const ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE = Symbol('ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE');
|
|
2473
|
+
const routerDisableScrollTop = (config = {}) => {
|
|
2474
|
+
return {
|
|
2475
|
+
...(!config.asReturnRoute ? { [ET_DISABLE_SCROLL_TOP]: true } : { [ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE]: true }),
|
|
2476
|
+
...(config.onPathParamChange ? { [ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE]: true } : {}),
|
|
2477
|
+
};
|
|
2478
|
+
};
|
|
2479
|
+
const setupScrollRestoration = (config = {}) => {
|
|
2480
|
+
if (!isPlatformBrowser(inject(PLATFORM_ID))) {
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
const state = injectRouterState();
|
|
2484
|
+
const route = injectRoute();
|
|
2485
|
+
const document = inject(DOCUMENT);
|
|
2486
|
+
combineLatest([toObservable(state).pipe(pairwise()), toObservable(route).pipe(pairwise())])
|
|
2487
|
+
.pipe(debounceTime(1))
|
|
2488
|
+
.subscribe(([[prevState, currState], [prevRoute, currRoute]]) => {
|
|
2489
|
+
const sameUrlNavigation = prevRoute === currRoute;
|
|
2490
|
+
const didFragmentChange = prevState.fragment !== currState.fragment;
|
|
2491
|
+
if (sameUrlNavigation) {
|
|
2492
|
+
const allQueryParams = [
|
|
2493
|
+
...new Set(Object.keys(prevState.queryParams).concat(Object.keys(currState.queryParams))),
|
|
2494
|
+
];
|
|
2495
|
+
const changedQueryParams = allQueryParams.filter((key) => currState.queryParams[key] !== prevState.queryParams[key]);
|
|
2496
|
+
if (!config.queryParamTriggerList?.length && !didFragmentChange) {
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
const caseQueryParams = changedQueryParams.some((key) => config.queryParamTriggerList?.includes(key));
|
|
2500
|
+
const caseFragment = didFragmentChange && config.fragment?.enabled;
|
|
2501
|
+
if (caseQueryParams) {
|
|
2502
|
+
(config.scrollElement ?? document.documentElement).scrollTop = 0;
|
|
2503
|
+
}
|
|
2504
|
+
else if (caseFragment) {
|
|
2505
|
+
const fragmentElement = document.getElementById(currState.fragment ?? '');
|
|
2506
|
+
if (fragmentElement) {
|
|
2507
|
+
fragmentElement.scrollIntoView({ behavior: config.fragment?.smooth ? 'smooth' : 'auto' });
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2270
2510
|
}
|
|
2271
|
-
else
|
|
2272
|
-
|
|
2511
|
+
else {
|
|
2512
|
+
const viaReturnRoute = currState.data[ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE] && prevState.data[ET_DISABLE_SCROLL_TOP];
|
|
2513
|
+
const explicitly = currState.data[ET_DISABLE_SCROLL_TOP];
|
|
2514
|
+
const pathParamsChange = currState.data[ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE];
|
|
2515
|
+
if (viaReturnRoute || explicitly || pathParamsChange) {
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
const el = config.scrollElement ?? document.documentElement;
|
|
2519
|
+
el.scrollTop = 0;
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
};
|
|
2523
|
+
|
|
2524
|
+
const DISABLE_LOGGER_PARAM = 'et-logger-quiet';
|
|
2525
|
+
const createLogger = (config) => {
|
|
2526
|
+
const { scope, feature } = config;
|
|
2527
|
+
const disableLogging = injectQueryParam(DISABLE_LOGGER_PARAM);
|
|
2528
|
+
const writeLog = (...args) => {
|
|
2529
|
+
if (disableLogging()) {
|
|
2530
|
+
return;
|
|
2273
2531
|
}
|
|
2532
|
+
console.log(...args);
|
|
2274
2533
|
};
|
|
2275
|
-
switch (origin) {
|
|
2276
|
-
case 'start':
|
|
2277
|
-
calculateScrollToStart();
|
|
2278
|
-
break;
|
|
2279
|
-
case 'end':
|
|
2280
|
-
calculateScrollToEnd();
|
|
2281
|
-
break;
|
|
2282
|
-
case 'center':
|
|
2283
|
-
calculateScrollToCenter();
|
|
2284
|
-
break;
|
|
2285
|
-
case 'nearest':
|
|
2286
|
-
calculateScrollToNearest();
|
|
2287
|
-
break;
|
|
2288
|
-
}
|
|
2289
2534
|
return {
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2535
|
+
log: (...args) => writeLog(`\x1B[32;40;24m[${scope} ${feature}]\x1B[m`, ...args),
|
|
2536
|
+
warn: (...args) => writeLog(`\x1B[93;40;24m[${scope} ${feature}]\x1B[m`, ...args),
|
|
2537
|
+
error: (...args) => writeLog(`\x1B[31;40;24m[${scope} ${feature}]\x1B[m`, ...args),
|
|
2293
2538
|
};
|
|
2294
2539
|
};
|
|
2295
|
-
|
|
2296
|
-
|
|
2540
|
+
|
|
2541
|
+
const clamp = (value, min = 0, max = 100) => {
|
|
2542
|
+
return Math.max(min, Math.min(max, value));
|
|
2543
|
+
};
|
|
2544
|
+
const round = (value, precision = 0) => {
|
|
2545
|
+
const multiplier = Math.pow(10, precision);
|
|
2546
|
+
return Math.round(value * multiplier) / multiplier;
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2549
|
+
const isObject = (value) => {
|
|
2550
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2551
|
+
};
|
|
2552
|
+
const isArray = (value) => {
|
|
2553
|
+
return Array.isArray(value);
|
|
2554
|
+
};
|
|
2555
|
+
const getObjectProperty = (obj, prop) => {
|
|
2556
|
+
const hasDotNotation = prop.includes('.');
|
|
2557
|
+
const hasBracketNotation = prop.includes('[');
|
|
2558
|
+
if (!hasDotNotation && !hasBracketNotation)
|
|
2559
|
+
return obj[prop];
|
|
2560
|
+
const props = prop.split('.');
|
|
2561
|
+
let value = obj;
|
|
2562
|
+
for (const prop of props) {
|
|
2563
|
+
if (!isObject(value))
|
|
2564
|
+
return undefined;
|
|
2565
|
+
if (prop.includes('[')) {
|
|
2566
|
+
const [key, index] = prop.split('[').map((part) => part.replace(']', ''));
|
|
2567
|
+
const arr = value[key];
|
|
2568
|
+
if (!Array.isArray(arr))
|
|
2569
|
+
return undefined;
|
|
2570
|
+
value = arr[+index];
|
|
2571
|
+
}
|
|
2572
|
+
else {
|
|
2573
|
+
value = value[prop];
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
return value;
|
|
2297
2577
|
};
|
|
2298
2578
|
|
|
2579
|
+
const RUNTIME_ERROR_NO_DATA = '__ET_NO_DATA__';
|
|
2580
|
+
class RuntimeError extends Error {
|
|
2581
|
+
constructor(code, message, devOnly = false, data = RUNTIME_ERROR_NO_DATA) {
|
|
2582
|
+
super(formatRuntimeError(code, message, devOnly));
|
|
2583
|
+
this.code = code;
|
|
2584
|
+
this.devOnly = devOnly;
|
|
2585
|
+
this.data = data;
|
|
2586
|
+
if (data !== RUNTIME_ERROR_NO_DATA) {
|
|
2587
|
+
try {
|
|
2588
|
+
const _data = clone(data);
|
|
2589
|
+
setTimeout(() => {
|
|
2590
|
+
console.error(_data);
|
|
2591
|
+
}, 1);
|
|
2592
|
+
}
|
|
2593
|
+
catch {
|
|
2594
|
+
setTimeout(() => {
|
|
2595
|
+
console.error(data);
|
|
2596
|
+
}, 1);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
function formatRuntimeError(code, message, devOnly) {
|
|
2602
|
+
// prefix code with zeros if it's less than 100
|
|
2603
|
+
const codeWithZeros = code < 10 ? `00${code}` : code < 100 ? `0${code}` : code;
|
|
2604
|
+
const fullCode = `ET${codeWithZeros}`;
|
|
2605
|
+
const devOnlyText = devOnly ? ' [DEV ONLY] ' : '';
|
|
2606
|
+
return `${devOnlyText}${fullCode}${message ? ': ' + message : ''}`;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2299
2609
|
const [, injectAngularRootElement] = createRootProvider(() => {
|
|
2300
2610
|
const appRef = inject(ApplicationRef);
|
|
2301
2611
|
const rootElement = signal(null, ...(ngDevMode ? [{ debugName: "rootElement" }] : []));
|
|
@@ -2323,82 +2633,6 @@ const [provideBoundaryElement, injectBoundaryElement, BOUNDARY_ELEMENT_TOKEN] =
|
|
|
2323
2633
|
};
|
|
2324
2634
|
}, { name: 'Boundary Element' });
|
|
2325
2635
|
|
|
2326
|
-
/**
|
|
2327
|
-
* Default viewport config based on Tailwind CSS.
|
|
2328
|
-
* @see https://tailwindcss.com/docs/screens
|
|
2329
|
-
*/
|
|
2330
|
-
const DEFAULT_VIEWPORT_CONFIG = {
|
|
2331
|
-
breakpoints: {
|
|
2332
|
-
xs: [0, 639],
|
|
2333
|
-
sm: [640, 767],
|
|
2334
|
-
md: [768, 1023],
|
|
2335
|
-
lg: [1024, 1279],
|
|
2336
|
-
xl: [1280, 1535],
|
|
2337
|
-
'2xl': [1536, Infinity],
|
|
2338
|
-
},
|
|
2339
|
-
};
|
|
2340
|
-
const [provideViewportConfig, injectViewportConfig] = createStaticRootProvider(DEFAULT_VIEWPORT_CONFIG, {
|
|
2341
|
-
name: 'Viewport Config',
|
|
2342
|
-
});
|
|
2343
|
-
const [provideBreakpointObserver, injectBreakpointObserver] = createRootProvider(() => {
|
|
2344
|
-
const breakpointObserver = inject(BreakpointObserver);
|
|
2345
|
-
const viewportConfig = injectViewportConfig();
|
|
2346
|
-
const isMediaQueryMatched = (mediaQuery) => breakpointObserver.isMatched(mediaQuery);
|
|
2347
|
-
const observeMediaQuery = (mediaQuery) => {
|
|
2348
|
-
return toSignal(breakpointObserver.observe(mediaQuery).pipe(map((x) => x.matches), startWith(isMediaQueryMatched(mediaQuery))), { requireSync: true });
|
|
2349
|
-
};
|
|
2350
|
-
const observeBreakpoint = (options) => observeMediaQuery(buildMediaQueryString(options));
|
|
2351
|
-
const isBreakpointMatched = (options) => isMediaQueryMatched(buildMediaQueryString(options));
|
|
2352
|
-
const getBreakpointSize = (type, option) => {
|
|
2353
|
-
const index = option === 'min' ? 0 : 1;
|
|
2354
|
-
const size = viewportConfig.breakpoints[type][index];
|
|
2355
|
-
if (size === Infinity || size === 0) {
|
|
2356
|
-
return size;
|
|
2357
|
-
}
|
|
2358
|
-
if (option === 'min') {
|
|
2359
|
-
return size;
|
|
2360
|
-
}
|
|
2361
|
-
// Due to scaling, the actual size of the viewport may be a decimal number.
|
|
2362
|
-
// Eg. on Windows 11 with 150% scaling, the viewport size may be 1535.33px
|
|
2363
|
-
// and thus not matching any of the default breakpoints.
|
|
2364
|
-
return size + 0.9;
|
|
2365
|
-
};
|
|
2366
|
-
const buildMediaQueryString = (options) => {
|
|
2367
|
-
if (!options.min && !options.max) {
|
|
2368
|
-
throw new Error('At least one of min or max must be defined');
|
|
2369
|
-
}
|
|
2370
|
-
const mediaQueryParts = [];
|
|
2371
|
-
if (options.min) {
|
|
2372
|
-
if (typeof options.min === 'number') {
|
|
2373
|
-
mediaQueryParts.push(`(min-width: ${options.min}px)`);
|
|
2374
|
-
}
|
|
2375
|
-
else {
|
|
2376
|
-
mediaQueryParts.push(`(min-width: ${getBreakpointSize(options.min, 'min')}px)`);
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
if (options.min && options.max) {
|
|
2380
|
-
mediaQueryParts.push('and');
|
|
2381
|
-
}
|
|
2382
|
-
if (options.max) {
|
|
2383
|
-
if (typeof options.max === 'number') {
|
|
2384
|
-
mediaQueryParts.push(`(max-width: ${options.max}px)`);
|
|
2385
|
-
}
|
|
2386
|
-
else {
|
|
2387
|
-
mediaQueryParts.push(`(max-width: ${getBreakpointSize(options.max, 'max')}px)`);
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
return mediaQueryParts.join(' ');
|
|
2391
|
-
};
|
|
2392
|
-
return {
|
|
2393
|
-
observeBreakpoint,
|
|
2394
|
-
isBreakpointMatched,
|
|
2395
|
-
getBreakpointSize,
|
|
2396
|
-
buildMediaQueryString,
|
|
2397
|
-
observeMediaQuery,
|
|
2398
|
-
isMediaQueryMatched,
|
|
2399
|
-
};
|
|
2400
|
-
}, { name: 'Breakpoint Observer' });
|
|
2401
|
-
|
|
2402
2636
|
const [provideFocusVisibleTracker, injectFocusVisibleTracker, FOCUS_VISIBLE_TRACKER_TOKEN] = createRootProvider(() => {
|
|
2403
2637
|
const document = inject(DOCUMENT);
|
|
2404
2638
|
const destroyRef = inject(DestroyRef);
|
|
@@ -2619,6 +2853,15 @@ const [provideRenderer, injectRenderer] = createRootProvider(() => {
|
|
|
2619
2853
|
};
|
|
2620
2854
|
}, { name: 'Angular Renderer' });
|
|
2621
2855
|
|
|
2856
|
+
const createUserConsentProvider = (options) => ({
|
|
2857
|
+
provide: options.for,
|
|
2858
|
+
useFactory: () => ({
|
|
2859
|
+
isGranted: options.isGranted(),
|
|
2860
|
+
grant: options.grant(),
|
|
2861
|
+
...(options.revoke ? { revoke: options.revoke() } : {}),
|
|
2862
|
+
}),
|
|
2863
|
+
});
|
|
2864
|
+
|
|
2622
2865
|
const nextFrame = (cb) => {
|
|
2623
2866
|
requestAnimationFrame(() => {
|
|
2624
2867
|
requestAnimationFrame(cb);
|
|
@@ -3421,6 +3664,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImpor
|
|
|
3421
3664
|
}]
|
|
3422
3665
|
}], ctorParameters: () => [], propDecorators: { repeatCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "etRepeat", required: false }] }] } });
|
|
3423
3666
|
|
|
3667
|
+
class ScrollObserverDirective {
|
|
3668
|
+
constructor() {
|
|
3669
|
+
this.elementRef = inject(ElementRef);
|
|
3670
|
+
this.enabled = model(true, ...(ngDevMode ? [{ debugName: "enabled" }] : []));
|
|
3671
|
+
this._startEl = signal(null, ...(ngDevMode ? [{ debugName: "_startEl" }] : []));
|
|
3672
|
+
this._endEl = signal(null, ...(ngDevMode ? [{ debugName: "_endEl" }] : []));
|
|
3673
|
+
this._startIntersection = signalElementIntersection(this._startEl, {
|
|
3674
|
+
root: this.elementRef,
|
|
3675
|
+
enabled: this.enabled,
|
|
3676
|
+
});
|
|
3677
|
+
this._endIntersection = signalElementIntersection(this._endEl, {
|
|
3678
|
+
root: this.elementRef,
|
|
3679
|
+
enabled: this.enabled,
|
|
3680
|
+
});
|
|
3681
|
+
this.isAtStart = computed(() => this._startIntersection()[0]?.isIntersecting ?? false, ...(ngDevMode ? [{ debugName: "isAtStart" }] : []));
|
|
3682
|
+
this.isAtEnd = computed(() => this._endIntersection()[0]?.isIntersecting ?? false, ...(ngDevMode ? [{ debugName: "isAtEnd" }] : []));
|
|
3683
|
+
}
|
|
3684
|
+
_registerStart(el) {
|
|
3685
|
+
this._startEl.set(el);
|
|
3686
|
+
}
|
|
3687
|
+
_registerEnd(el) {
|
|
3688
|
+
this._endEl.set(el);
|
|
3689
|
+
}
|
|
3690
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScrollObserverDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
3691
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.1", type: ScrollObserverDirective, isStandalone: true, selector: "[etScrollObserver]", inputs: { enabled: { classPropertyName: "enabled", publicName: "enabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { enabled: "enabledChange" }, exportAs: ["etScrollObserver"], ngImport: i0 }); }
|
|
3692
|
+
}
|
|
3693
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScrollObserverDirective, decorators: [{
|
|
3694
|
+
type: Directive,
|
|
3695
|
+
args: [{
|
|
3696
|
+
selector: '[etScrollObserver]',
|
|
3697
|
+
exportAs: 'etScrollObserver',
|
|
3698
|
+
}]
|
|
3699
|
+
}], propDecorators: { enabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "enabled", required: false }] }, { type: i0.Output, args: ["enabledChange"] }] } });
|
|
3700
|
+
|
|
3701
|
+
class ScrollObserverEndDirective {
|
|
3702
|
+
constructor() {
|
|
3703
|
+
this.elementRef = inject(ElementRef);
|
|
3704
|
+
this.host = inject(ScrollObserverDirective);
|
|
3705
|
+
this.host._registerEnd(this.elementRef);
|
|
3706
|
+
}
|
|
3707
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScrollObserverEndDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
3708
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.1", type: ScrollObserverEndDirective, isStandalone: true, selector: "[etScrollObserverEnd]", ngImport: i0 }); }
|
|
3709
|
+
}
|
|
3710
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScrollObserverEndDirective, decorators: [{
|
|
3711
|
+
type: Directive,
|
|
3712
|
+
args: [{
|
|
3713
|
+
selector: '[etScrollObserverEnd]',
|
|
3714
|
+
}]
|
|
3715
|
+
}], ctorParameters: () => [] });
|
|
3716
|
+
|
|
3717
|
+
class ScrollObserverStartDirective {
|
|
3718
|
+
constructor() {
|
|
3719
|
+
this.elementRef = inject(ElementRef);
|
|
3720
|
+
this.host = inject(ScrollObserverDirective);
|
|
3721
|
+
this.host._registerStart(this.elementRef);
|
|
3722
|
+
}
|
|
3723
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScrollObserverStartDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
3724
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.1", type: ScrollObserverStartDirective, isStandalone: true, selector: "[etScrollObserverStart]", ngImport: i0 }); }
|
|
3725
|
+
}
|
|
3726
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScrollObserverStartDirective, decorators: [{
|
|
3727
|
+
type: Directive,
|
|
3728
|
+
args: [{
|
|
3729
|
+
selector: '[etScrollObserverStart]',
|
|
3730
|
+
}]
|
|
3731
|
+
}], ctorParameters: () => [] });
|
|
3732
|
+
|
|
3424
3733
|
class NormalizeGameResultTypePipe {
|
|
3425
3734
|
constructor() {
|
|
3426
3735
|
this.transform = normalizeGameResultType;
|
|
@@ -5348,5 +5657,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImpor
|
|
|
5348
5657
|
* Generated bundle index. Do not edit.
|
|
5349
5658
|
*/
|
|
5350
5659
|
|
|
5351
|
-
export { ANIMATABLE_TOKEN, ANIMATED_IF_TOKEN, ANIMATED_LIFECYCLE_TOKEN, AT_LEAST_ONE_REQUIRED, AnimatableDirective, AnimatedIfDirective, AnimatedLifecycleDirective, AnimatedOverlayDirective, BOUNDARY_ELEMENT_TOKEN, ClickOutsideDirective, DEFAULT_MULTI_INSTANCE_RELS, DEFAULT_MULTI_INSTANCE_TAGS, DEFAULT_VIEWPORT_CONFIG, DISABLE_LOGGER_PARAM, ET_DISABLE_SCROLL_TOP, ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE, ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE, ET_PROPERTY_REMOVED, FOCUS_VISIBLE_TRACKER_TOKEN, IS_ARRAY_NOT_EMPTY, IS_EMAIL, InferMimeTypePipe, IsArrayNotEmpty, IsEmail, jsonLd as JsonLD, KeyPressManager, MUST_MATCH, MustMatch, NormalizeGameResultTypePipe, NormalizeMatchParticipantsPipe, NormalizeMatchScorePipe, NormalizeMatchStatePipe, NormalizeMatchTypePipe, PropsDirective, ProvideThemeDirective, RUNTIME_ERROR_NO_DATA, RepeatDirective, RuntimeError, SEO_DIRECTIVE_TOKEN, SeoDirective, StructuredDataComponent, THEME_PROVIDER, ToArrayPipe, TypedQueryList, ValidateAtLeastOneRequired, Validators, applyAlternateBinding, applyAlternateLanguagesBindings, applyArticleBindings, applyAuthorBinding, applyCanonicalBinding, applyDescriptionBinding, applyHeadBinding, applyHeadTitleBinding, applyHostListener, applyHostListeners, applyKeywordsBinding, applyLinkBinding, applyMetaBinding, applyNextBinding, applyOgBinding, applyOpenGraphBindings, applyPrevBinding, applyResourceHintsBindings, applyRobotsBinding, applySocialMediaBindings, applyStructuredDataBinding, applyTwitterCardBindings, areScrollStatesEqual, bindProps, boundingClientRectToElementRect, buildElementSignal, buildSignalEffects, clamp, clone, cloneFormGroup, computedTillFalsy, computedTillTruthy, controlValueSignal, controlValueSignalWithPrevious, createArrayPropertyBinding, createBulkPropertyBinding, createCanAnimateSignal, createComponentId, createCssThemeName, createDependencyStash, createDestroy, createDocumentElementSignal, createElementDictionary, createElementDimensions, createEmptyElementSignal, createFlipAnimation, createFlipAnimationGroup, createHostProps, createIsRenderedSignal, createLogger, createPropHandlers, createPropertyBinding, createProps, createProvider, createRootProvider, createRootThemeCss, createRxHostListener, createSetup, createStaticProvider, createStaticRootProvider, createSwatchCss, createTailwindColorThemes, createTailwindCssVar, createTailwindRgbVar, createThemeStyle, deferredSignal, deleteCookie, easeElastic, easeIn, easeInOut, easeLinear, easeOut, easeOutBack, easeOutBackStrong, elementCanScroll, equal, firstElementSignal, forceReflow, formatRuntimeError, fromNextFrame, getCookie, getDomain, getElementScrollCoordinates, getFormGroupValue, getGroupMatchPoints, getGroupMatchScore, getKnockoutMatchScore, getMatchScoreSubLine, getObjectProperty, hasCookie, inferMimeType, injectAngularRootElement, injectBoundaryElement, injectBreakpointIsMatched, injectBreakpointObserver, injectCanHover, injectColorThemes, injectCurrentBreakpoint, injectDeviceInputType, injectDisplayOrientation, injectFocusVisibleTracker, injectFragment, injectHasPrecisionInput, injectHasTouchInput, injectHostElement, injectIs2Xl, injectIsLandscape, injectIsLg, injectIsMd, injectIsPortrait, injectIsRouterInitialized, injectIsSm, injectIsXl, injectIsXs, injectLinkStore, injectLinkStoreConfig, injectLocale, injectMediaQueryIsMatched, injectMetaConfig, injectMetaStore, injectObserveBreakpoint, injectObserveMediaQuery, injectPathParam, injectPathParamChanges, injectPathParams, injectQueryParam, injectQueryParamChanges, injectQueryParams, injectRenderer, injectRoute, injectRouteData, injectRouteDataItem, injectRouteTitle, injectRouterEvent, injectRouterState, injectScrollbarDimensions, injectStructuredDataConfig, injectStructuredDataStore, injectTemplateRef, injectThemesPrefix, injectTitleConfig, injectTitleStore, injectUrl, injectViewportConfig, injectViewportDimensions, isArray, isElementSignal, isElementVisible, isGroupMatch, isKnockoutMatch, isObject, maybeSignalValue, memoizeSignal, nextFrame, normalizeGameResultType, normalizeMatchParticipant, normalizeMatchParticipants, normalizeMatchScore, normalizeMatchState, normalizeMatchType, previousSignalValue, provideBoundaryElement, provideBreakpointObserver, provideColorThemes, provideColorThemesWithTailwind4, provideFocusVisibleTracker, provideLinkStore, provideLinkStoreConfig, provideLocale, provideMetaConfig, provideMetaStore, provideRenderer, provideStructuredDataConfig, provideStructuredDataStore, provideTitleConfig, provideTitleStore, provideViewportConfig, round, routerDisableScrollTop, scrollToElement, setCookie, setInputSignal, setupScrollRestoration, signalAnimatedNumber, signalAttributes, signalClasses, signalElementChildren, signalElementDimensions, signalElementIntersection, signalElementLastScrollDirection, signalElementMutations, signalElementScrollState, signalHostAttributes, signalHostClasses, signalHostElementDimensions, signalHostElementIntersection, signalHostElementLastScrollDirection, signalHostElementMutations, signalHostElementScrollState, signalHostStyles, signalIsRendered, signalStyles, switchQueryListChanges, syncSignal, templateComputed, toArray, toArrayTrackByFn, toStringBinding, transformOrReturn, unbindProps, useCursorDragScroll, writeScrollbarSizeToCssVariables, writeViewportSizeToCssVariables, ɵProvideColorThemes, ɵProvideThemesPrefix };
|
|
5660
|
+
export { ANIMATABLE_TOKEN, ANIMATED_IF_TOKEN, ANIMATED_LIFECYCLE_TOKEN, AT_LEAST_ONE_REQUIRED, AnimatableDirective, AnimatedIfDirective, AnimatedLifecycleDirective, AnimatedOverlayDirective, BOUNDARY_ELEMENT_TOKEN, BREAKPOINT_INSTANCE_TOKEN, BREAKPOINT_ORDER, ClickOutsideDirective, DEFAULT_MULTI_INSTANCE_RELS, DEFAULT_MULTI_INSTANCE_TAGS, DEFAULT_VIEWPORT_CONFIG, DISABLE_LOGGER_PARAM, ET_DISABLE_SCROLL_TOP, ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE, ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE, ET_PROPERTY_REMOVED, FOCUS_VISIBLE_TRACKER_TOKEN, IS_ARRAY_NOT_EMPTY, IS_EMAIL, InferMimeTypePipe, IsArrayNotEmpty, IsEmail, jsonLd as JsonLD, KeyPressManager, MUST_MATCH, MustMatch, NormalizeGameResultTypePipe, NormalizeMatchParticipantsPipe, NormalizeMatchScorePipe, NormalizeMatchStatePipe, NormalizeMatchTypePipe, PropsDirective, ProvideThemeDirective, RUNTIME_ERROR_NO_DATA, RepeatDirective, RuntimeError, SEO_DIRECTIVE_TOKEN, ScrollObserverDirective, ScrollObserverEndDirective, ScrollObserverStartDirective, SeoDirective, StructuredDataComponent, THEME_PROVIDER, ToArrayPipe, TypedQueryList, ValidateAtLeastOneRequired, Validators, applyAlternateBinding, applyAlternateLanguagesBindings, applyArticleBindings, applyAuthorBinding, applyCanonicalBinding, applyDescriptionBinding, applyHeadBinding, applyHeadTitleBinding, applyHostListener, applyHostListeners, applyKeywordsBinding, applyLinkBinding, applyMetaBinding, applyNextBinding, applyOgBinding, applyOpenGraphBindings, applyPrevBinding, applyResourceHintsBindings, applyRobotsBinding, applySocialMediaBindings, applyStructuredDataBinding, applyTwitterCardBindings, areScrollStatesEqual, bindProps, boolBreakpointTransform, booleanBreakpointAttribute, boundingClientRectToElementRect, breakpointTransformBase, buildElementSignal, buildSignalEffects, clamp, clone, cloneFormGroup, computedTillFalsy, computedTillTruthy, controlValueSignal, controlValueSignalWithPrevious, createArrayPropertyBinding, createBulkPropertyBinding, createCanAnimateSignal, createComponentId, createCssThemeName, createDependencyStash, createDestroy, createDocumentElementSignal, createElementDictionary, createElementDimensions, createEmptyElementSignal, createFlipAnimation, createFlipAnimationGroup, createHostProps, createIsRenderedSignal, createLogger, createPropHandlers, createPropertyBinding, createProps, createProvider, createRootProvider, createRootThemeCss, createRxHostListener, createSetup, createStaticProvider, createStaticRootProvider, createSwatchCss, createTailwindColorThemes, createTailwindCssVar, createTailwindRgbVar, createThemeStyle, createUserConsentProvider, deferredSignal, deleteCookie, easeElastic, easeIn, easeInOut, easeLinear, easeOut, easeOutBack, easeOutBackStrong, elementCanScroll, equal, firstElementSignal, forceReflow, formatRuntimeError, fromNextFrame, getCookie, getDomain, getElementScrollCoordinates, getFormGroupValue, getGroupMatchPoints, getGroupMatchScore, getKnockoutMatchScore, getMatchScoreSubLine, getObjectProperty, getScrollContainerTarget, getScrollItemTarget, getScrollSnapTarget, hasCookie, inferMimeType, injectAngularRootElement, injectBoundaryElement, injectBreakpointInput, injectBreakpointIsMatched, injectBreakpointObserver, injectCanHover, injectColorThemes, injectCurrentBreakpoint, injectDeviceInputType, injectDisplayOrientation, injectFocusVisibleTracker, injectFragment, injectHasPrecisionInput, injectHasTouchInput, injectHostElement, injectIs2Xl, injectIsLandscape, injectIsLg, injectIsMd, injectIsPortrait, injectIsRouterInitialized, injectIsSm, injectIsXl, injectIsXs, injectLinkStore, injectLinkStoreConfig, injectLocale, injectMediaQueryIsMatched, injectMetaConfig, injectMetaStore, injectObserveBreakpoint, injectObserveMediaQuery, injectPathParam, injectPathParamChanges, injectPathParams, injectQueryParam, injectQueryParamChanges, injectQueryParams, injectRenderer, injectRoute, injectRouteData, injectRouteDataItem, injectRouteTitle, injectRouterEvent, injectRouterState, injectScrollbarDimensions, injectStructuredDataConfig, injectStructuredDataStore, injectTemplateRef, injectThemesPrefix, injectTitleConfig, injectTitleStore, injectUrl, injectViewportConfig, injectViewportDimensions, isArray, isElementSignal, isElementVisible, isGroupMatch, isKnockoutMatch, isObject, maybeSignalValue, memoizeSignal, nextFrame, normalizeGameResultType, normalizeMatchParticipant, normalizeMatchParticipants, normalizeMatchScore, normalizeMatchState, normalizeMatchType, numberBreakpointAttribute, numberBreakpointTransform, previousSignalValue, provideBoundaryElement, provideBreakpointInstance, provideBreakpointObserver, provideColorThemes, provideColorThemesWithTailwind4, provideFocusVisibleTracker, provideLinkStore, provideLinkStoreConfig, provideLocale, provideMetaConfig, provideMetaStore, provideRenderer, provideStructuredDataConfig, provideStructuredDataStore, provideTitleConfig, provideTitleStore, provideViewportConfig, round, routerDisableScrollTop, scrollToElement, setCookie, setInputSignal, setupScrollRestoration, signalAnimatedNumber, signalAttributes, signalClasses, signalElementChildren, signalElementDimensions, signalElementIntersection, signalElementLastScrollDirection, signalElementMutations, signalElementScrollState, signalHostAttributes, signalHostClasses, signalHostElementDimensions, signalHostElementIntersection, signalHostElementLastScrollDirection, signalHostElementMutations, signalHostElementScrollState, signalHostStyles, signalIsRendered, signalStyles, switchQueryListChanges, syncSignal, templateComputed, toArray, toArrayTrackByFn, toStringBinding, transformOrReturn, typedBreakpointTransform, unbindProps, useCursorDragScroll, writeScrollbarSizeToCssVariables, writeViewportSizeToCssVariables, ɵProvideColorThemes, ɵProvideThemesPrefix };
|
|
5352
5661
|
//# sourceMappingURL=ethlete-core.mjs.map
|