@acorex/cdk 22.3.16 → 22.3.18

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.
@@ -0,0 +1,9 @@
1
+ # @acorex/cdk/auto-follow-scroll
2
+
3
+ Chat-style auto-follow scroll for message lists (Telegram / Discord behavior).
4
+
5
+ ```typescript
6
+ import { AutoFollowScrollDirective } from '@acorex/cdk/auto-follow-scroll';
7
+ ```
8
+
9
+ Apply the directive to the scroll container (`overflow: auto`). Call `messageAdded()` on the directive instance after each new message is rendered.
@@ -0,0 +1,187 @@
1
+ import { isPlatformBrowser } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { inject, ElementRef, DestroyRef, PLATFORM_ID, NgZone, input, output, signal, afterNextRender, Directive } from '@angular/core';
4
+
5
+ const PROGRAMMATIC_SCROLL_FALLBACK_MS = 400;
6
+ /**
7
+ * Manages chat-style auto-follow scroll: sticks to the bottom while the user is
8
+ * "following", and preserves scroll position with unread counts when they scroll up.
9
+ *
10
+ * Apply to the element that has `overflow: auto` (the scroll container).
11
+ *
12
+ * @example
13
+ * ```html
14
+ * <div
15
+ * autoFollowScroll
16
+ * [threshold]="100"
17
+ * [smooth]="true"
18
+ * (followStateChange)="onFollowState($event)"
19
+ * (newMessages)="onNewMessages($event)"
20
+ * >
21
+ * @for (message of messages; track message.id) {
22
+ * <div>{{ message.text }}</div>
23
+ * }
24
+ * </div>
25
+ * ```
26
+ */
27
+ class AXAutoFollowScrollDirective {
28
+ constructor() {
29
+ this.host = inject((ElementRef));
30
+ this.destroyRef = inject(DestroyRef);
31
+ this.platformId = inject(PLATFORM_ID);
32
+ this.ngZone = inject(NgZone);
33
+ /** Distance from the bottom (px) at or below which the user is considered "following". */
34
+ this.threshold = input(100, /* @ts-ignore */
35
+ ...(ngDevMode ? [{ debugName: "threshold" }] : /* istanbul ignore next */ []));
36
+ /** Whether programmatic scroll uses smooth behavior. */
37
+ this.smooth = input(true, /* @ts-ignore */
38
+ ...(ngDevMode ? [{ debugName: "smooth" }] : /* istanbul ignore next */ []));
39
+ this.followStateChange = output();
40
+ this.newMessages = output();
41
+ this.state = signal('following', /* @ts-ignore */
42
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
43
+ this.unreadCount = signal(0, /* @ts-ignore */
44
+ ...(ngDevMode ? [{ debugName: "unreadCount" }] : /* istanbul ignore next */ []));
45
+ this.isProgrammaticScroll = false;
46
+ this.scrollRafId = null;
47
+ this.programmaticScrollTimer = null;
48
+ this.scrollListener = null;
49
+ afterNextRender(() => {
50
+ if (!isPlatformBrowser(this.platformId)) {
51
+ return;
52
+ }
53
+ this.attachScrollListener();
54
+ });
55
+ this.destroyRef.onDestroy(() => this.teardown());
56
+ }
57
+ /**
58
+ * Call after a new message is rendered in the list.
59
+ */
60
+ messageAdded() {
61
+ requestAnimationFrame(() => {
62
+ if (this.state() === 'following') {
63
+ this.scrollToBottom();
64
+ return;
65
+ }
66
+ this.unreadCount.update((count) => count + 1);
67
+ this.newMessages.emit({ count: this.unreadCount() });
68
+ });
69
+ }
70
+ /** Scrolls to the bottom and resumes following. */
71
+ scrollToBottom() {
72
+ this.unreadCount.set(0);
73
+ this.setState('following');
74
+ this.runProgrammaticScroll(this.smooth());
75
+ }
76
+ /** Resets unread count and following state without scrolling. */
77
+ reset() {
78
+ this.clearProgrammaticScrollGuard();
79
+ this.unreadCount.set(0);
80
+ this.setState('following');
81
+ }
82
+ attachScrollListener() {
83
+ const element = this.host.nativeElement;
84
+ this.scrollListener = () => this.scheduleScrollHandling();
85
+ element.addEventListener('scroll', this.scrollListener, { passive: true });
86
+ }
87
+ scheduleScrollHandling() {
88
+ if (this.scrollRafId !== null) {
89
+ return;
90
+ }
91
+ this.scrollRafId = requestAnimationFrame(() => {
92
+ this.scrollRafId = null;
93
+ this.handleUserScroll();
94
+ });
95
+ }
96
+ handleUserScroll() {
97
+ if (this.isProgrammaticScroll) {
98
+ return;
99
+ }
100
+ const atBottom = this.isNearBottom();
101
+ if (atBottom) {
102
+ if (this.state() === 'user-scrolling') {
103
+ this.unreadCount.set(0);
104
+ this.setState('following');
105
+ }
106
+ return;
107
+ }
108
+ if (this.state() === 'following') {
109
+ this.setState('user-scrolling');
110
+ }
111
+ }
112
+ isNearBottom() {
113
+ const element = this.host.nativeElement;
114
+ const distanceFromBottom = element.scrollHeight - element.clientHeight - element.scrollTop;
115
+ return distanceFromBottom <= this.threshold();
116
+ }
117
+ runProgrammaticScroll(smooth) {
118
+ const element = this.host.nativeElement;
119
+ const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
120
+ this.beginProgrammaticScroll();
121
+ element.scrollTo({
122
+ top: maxScrollTop,
123
+ behavior: smooth ? 'smooth' : 'auto',
124
+ });
125
+ if (typeof window !== 'undefined' && 'onscrollend' in window) {
126
+ const onScrollEnd = () => {
127
+ element.removeEventListener('scrollend', onScrollEnd);
128
+ this.endProgrammaticScroll();
129
+ };
130
+ element.addEventListener('scrollend', onScrollEnd, { once: true });
131
+ return;
132
+ }
133
+ this.programmaticScrollTimer = setTimeout(() => this.endProgrammaticScroll(), smooth ? PROGRAMMATIC_SCROLL_FALLBACK_MS : 0);
134
+ }
135
+ beginProgrammaticScroll() {
136
+ this.clearProgrammaticScrollGuard();
137
+ this.isProgrammaticScroll = true;
138
+ }
139
+ endProgrammaticScroll() {
140
+ this.isProgrammaticScroll = false;
141
+ this.programmaticScrollTimer = null;
142
+ }
143
+ clearProgrammaticScrollGuard() {
144
+ if (this.programmaticScrollTimer !== null) {
145
+ clearTimeout(this.programmaticScrollTimer);
146
+ this.programmaticScrollTimer = null;
147
+ }
148
+ this.isProgrammaticScroll = false;
149
+ }
150
+ setState(next) {
151
+ if (this.state() === next) {
152
+ return;
153
+ }
154
+ this.state.set(next);
155
+ this.ngZone.run(() => {
156
+ this.followStateChange.emit({ state: next });
157
+ });
158
+ }
159
+ teardown() {
160
+ if (this.scrollRafId !== null) {
161
+ cancelAnimationFrame(this.scrollRafId);
162
+ this.scrollRafId = null;
163
+ }
164
+ this.clearProgrammaticScrollGuard();
165
+ const element = this.host.nativeElement;
166
+ if (this.scrollListener) {
167
+ element.removeEventListener('scroll', this.scrollListener);
168
+ this.scrollListener = null;
169
+ }
170
+ }
171
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: AXAutoFollowScrollDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
172
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: AXAutoFollowScrollDirective, isStandalone: true, selector: "[axAutoFollowScroll]", inputs: { threshold: { classPropertyName: "threshold", publicName: "threshold", isSignal: true, isRequired: false, transformFunction: null }, smooth: { classPropertyName: "smooth", publicName: "smooth", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { followStateChange: "followStateChange", newMessages: "newMessages" }, exportAs: ["axautoFollowScroll"], ngImport: i0 }); }
173
+ }
174
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: AXAutoFollowScrollDirective, decorators: [{
175
+ type: Directive,
176
+ args: [{
177
+ selector: '[axAutoFollowScroll]',
178
+ exportAs: 'axautoFollowScroll',
179
+ }]
180
+ }], ctorParameters: () => [], propDecorators: { threshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "threshold", required: false }] }], smooth: [{ type: i0.Input, args: [{ isSignal: true, alias: "smooth", required: false }] }], followStateChange: [{ type: i0.Output, args: ["followStateChange"] }], newMessages: [{ type: i0.Output, args: ["newMessages"] }] } });
181
+
182
+ /**
183
+ * Generated bundle index. Do not edit.
184
+ */
185
+
186
+ export { AXAutoFollowScrollDirective };
187
+ //# sourceMappingURL=acorex-cdk-auto-follow-scroll.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acorex-cdk-auto-follow-scroll.mjs","sources":["../../../../packages/cdk/auto-follow-scroll/src/lib/auto-follow-scroll.directive.ts","../../../../packages/cdk/auto-follow-scroll/src/acorex-cdk-auto-follow-scroll.ts"],"sourcesContent":["import { isPlatformBrowser } from '@angular/common';\nimport {\n afterNextRender,\n DestroyRef,\n Directive,\n ElementRef,\n inject,\n input,\n NgZone,\n output,\n PLATFORM_ID,\n signal,\n} from '@angular/core';\n\nexport type ScrollState = 'following' | 'user-scrolling';\n\nexport interface FollowStateChangeEvent {\n state: ScrollState;\n}\n\nexport interface NewMessagesEvent {\n count: number;\n}\n\nconst PROGRAMMATIC_SCROLL_FALLBACK_MS = 400;\n\n/**\n * Manages chat-style auto-follow scroll: sticks to the bottom while the user is\n * \"following\", and preserves scroll position with unread counts when they scroll up.\n *\n * Apply to the element that has `overflow: auto` (the scroll container).\n *\n * @example\n * ```html\n * <div\n * autoFollowScroll\n * [threshold]=\"100\"\n * [smooth]=\"true\"\n * (followStateChange)=\"onFollowState($event)\"\n * (newMessages)=\"onNewMessages($event)\"\n * >\n * @for (message of messages; track message.id) {\n * <div>{{ message.text }}</div>\n * }\n * </div>\n * ```\n */\n@Directive({\n selector: '[axAutoFollowScroll]',\n exportAs: 'axautoFollowScroll',\n})\nexport class AXAutoFollowScrollDirective {\n private readonly host = inject(ElementRef<HTMLElement>);\n private readonly destroyRef = inject(DestroyRef);\n private readonly platformId = inject(PLATFORM_ID);\n private readonly ngZone = inject(NgZone);\n\n /** Distance from the bottom (px) at or below which the user is considered \"following\". */\n readonly threshold = input(100);\n\n /** Whether programmatic scroll uses smooth behavior. */\n readonly smooth = input(true);\n\n readonly followStateChange = output<FollowStateChangeEvent>();\n readonly newMessages = output<NewMessagesEvent>();\n\n readonly state = signal<ScrollState>('following');\n readonly unreadCount = signal(0);\n\n private isProgrammaticScroll = false;\n private scrollRafId: number | null = null;\n private programmaticScrollTimer: ReturnType<typeof setTimeout> | null = null;\n private scrollListener: (() => void) | null = null;\n\n constructor() {\n afterNextRender(() => {\n if (!isPlatformBrowser(this.platformId)) {\n return;\n }\n this.attachScrollListener();\n });\n\n this.destroyRef.onDestroy(() => this.teardown());\n }\n\n /**\n * Call after a new message is rendered in the list.\n */\n messageAdded(): void {\n requestAnimationFrame(() => {\n if (this.state() === 'following') {\n this.scrollToBottom();\n return;\n }\n\n this.unreadCount.update((count) => count + 1);\n this.newMessages.emit({ count: this.unreadCount() });\n });\n }\n\n /** Scrolls to the bottom and resumes following. */\n scrollToBottom(): void {\n this.unreadCount.set(0);\n this.setState('following');\n this.runProgrammaticScroll(this.smooth());\n }\n\n /** Resets unread count and following state without scrolling. */\n reset(): void {\n this.clearProgrammaticScrollGuard();\n this.unreadCount.set(0);\n this.setState('following');\n }\n\n private attachScrollListener(): void {\n const element = this.host.nativeElement;\n\n this.scrollListener = () => this.scheduleScrollHandling();\n element.addEventListener('scroll', this.scrollListener, { passive: true });\n }\n\n private scheduleScrollHandling(): void {\n if (this.scrollRafId !== null) {\n return;\n }\n\n this.scrollRafId = requestAnimationFrame(() => {\n this.scrollRafId = null;\n this.handleUserScroll();\n });\n }\n\n private handleUserScroll(): void {\n if (this.isProgrammaticScroll) {\n return;\n }\n\n const atBottom = this.isNearBottom();\n\n if (atBottom) {\n if (this.state() === 'user-scrolling') {\n this.unreadCount.set(0);\n this.setState('following');\n }\n return;\n }\n\n if (this.state() === 'following') {\n this.setState('user-scrolling');\n }\n }\n\n private isNearBottom(): boolean {\n const element = this.host.nativeElement;\n const distanceFromBottom = element.scrollHeight - element.clientHeight - element.scrollTop;\n return distanceFromBottom <= this.threshold();\n }\n\n private runProgrammaticScroll(smooth: boolean): void {\n const element = this.host.nativeElement;\n const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);\n\n this.beginProgrammaticScroll();\n\n element.scrollTo({\n top: maxScrollTop,\n behavior: smooth ? 'smooth' : 'auto',\n });\n\n if (typeof window !== 'undefined' && 'onscrollend' in window) {\n const onScrollEnd = (): void => {\n element.removeEventListener('scrollend', onScrollEnd);\n this.endProgrammaticScroll();\n };\n element.addEventListener('scrollend', onScrollEnd, { once: true });\n return;\n }\n\n this.programmaticScrollTimer = setTimeout(\n () => this.endProgrammaticScroll(),\n smooth ? PROGRAMMATIC_SCROLL_FALLBACK_MS : 0,\n );\n }\n\n private beginProgrammaticScroll(): void {\n this.clearProgrammaticScrollGuard();\n this.isProgrammaticScroll = true;\n }\n\n private endProgrammaticScroll(): void {\n this.isProgrammaticScroll = false;\n this.programmaticScrollTimer = null;\n }\n\n private clearProgrammaticScrollGuard(): void {\n if (this.programmaticScrollTimer !== null) {\n clearTimeout(this.programmaticScrollTimer);\n this.programmaticScrollTimer = null;\n }\n this.isProgrammaticScroll = false;\n }\n\n private setState(next: ScrollState): void {\n if (this.state() === next) {\n return;\n }\n\n this.state.set(next);\n this.ngZone.run(() => {\n this.followStateChange.emit({ state: next });\n });\n }\n\n private teardown(): void {\n if (this.scrollRafId !== null) {\n cancelAnimationFrame(this.scrollRafId);\n this.scrollRafId = null;\n }\n\n this.clearProgrammaticScrollGuard();\n\n const element = this.host.nativeElement;\n if (this.scrollListener) {\n element.removeEventListener('scroll', this.scrollListener);\n this.scrollListener = null;\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@acorex/cdk",
3
- "version": "22.3.16",
3
+ "version": "22.3.18",
4
4
  "peerDependencies": {
5
- "@acorex/core": "22.3.16",
5
+ "@acorex/core": "22.3.18",
6
6
  "quill": ">=2.0.2",
7
7
  "qrcode": ">=1.5.4",
8
8
  "swiper": ">=11.1.15",
@@ -24,6 +24,10 @@
24
24
  "types": "./types/acorex-cdk-accordion.d.ts",
25
25
  "default": "./fesm2022/acorex-cdk-accordion.mjs"
26
26
  },
27
+ "./auto-follow-scroll": {
28
+ "types": "./types/acorex-cdk-auto-follow-scroll.d.ts",
29
+ "default": "./fesm2022/acorex-cdk-auto-follow-scroll.mjs"
30
+ },
27
31
  "./carousel": {
28
32
  "types": "./types/acorex-cdk-carousel.d.ts",
29
33
  "default": "./fesm2022/acorex-cdk-carousel.mjs"
@@ -0,0 +1,72 @@
1
+ import * as _angular_core from '@angular/core';
2
+
3
+ type ScrollState = 'following' | 'user-scrolling';
4
+ interface FollowStateChangeEvent {
5
+ state: ScrollState;
6
+ }
7
+ interface NewMessagesEvent {
8
+ count: number;
9
+ }
10
+ /**
11
+ * Manages chat-style auto-follow scroll: sticks to the bottom while the user is
12
+ * "following", and preserves scroll position with unread counts when they scroll up.
13
+ *
14
+ * Apply to the element that has `overflow: auto` (the scroll container).
15
+ *
16
+ * @example
17
+ * ```html
18
+ * <div
19
+ * autoFollowScroll
20
+ * [threshold]="100"
21
+ * [smooth]="true"
22
+ * (followStateChange)="onFollowState($event)"
23
+ * (newMessages)="onNewMessages($event)"
24
+ * >
25
+ * @for (message of messages; track message.id) {
26
+ * <div>{{ message.text }}</div>
27
+ * }
28
+ * </div>
29
+ * ```
30
+ */
31
+ declare class AXAutoFollowScrollDirective {
32
+ private readonly host;
33
+ private readonly destroyRef;
34
+ private readonly platformId;
35
+ private readonly ngZone;
36
+ /** Distance from the bottom (px) at or below which the user is considered "following". */
37
+ readonly threshold: _angular_core.InputSignal<number>;
38
+ /** Whether programmatic scroll uses smooth behavior. */
39
+ readonly smooth: _angular_core.InputSignal<boolean>;
40
+ readonly followStateChange: _angular_core.OutputEmitterRef<FollowStateChangeEvent>;
41
+ readonly newMessages: _angular_core.OutputEmitterRef<NewMessagesEvent>;
42
+ readonly state: _angular_core.WritableSignal<ScrollState>;
43
+ readonly unreadCount: _angular_core.WritableSignal<number>;
44
+ private isProgrammaticScroll;
45
+ private scrollRafId;
46
+ private programmaticScrollTimer;
47
+ private scrollListener;
48
+ constructor();
49
+ /**
50
+ * Call after a new message is rendered in the list.
51
+ */
52
+ messageAdded(): void;
53
+ /** Scrolls to the bottom and resumes following. */
54
+ scrollToBottom(): void;
55
+ /** Resets unread count and following state without scrolling. */
56
+ reset(): void;
57
+ private attachScrollListener;
58
+ private scheduleScrollHandling;
59
+ private handleUserScroll;
60
+ private isNearBottom;
61
+ private runProgrammaticScroll;
62
+ private beginProgrammaticScroll;
63
+ private endProgrammaticScroll;
64
+ private clearProgrammaticScrollGuard;
65
+ private setState;
66
+ private teardown;
67
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<AXAutoFollowScrollDirective, never>;
68
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<AXAutoFollowScrollDirective, "[axAutoFollowScroll]", ["axautoFollowScroll"], { "threshold": { "alias": "threshold"; "required": false; "isSignal": true; }; "smooth": { "alias": "smooth"; "required": false; "isSignal": true; }; }, { "followStateChange": "followStateChange"; "newMessages": "newMessages"; }, never, never, true, never>;
69
+ }
70
+
71
+ export { AXAutoFollowScrollDirective };
72
+ export type { FollowStateChangeEvent, NewMessagesEvent, ScrollState };