33import {
44 AfterViewInit ,
55 ChangeDetectionStrategy ,
6+ ChangeDetectorRef ,
67 Component ,
78 ElementRef ,
89 inject ,
@@ -58,10 +59,12 @@ import { OpenVacancyComponent } from "./shared/open-vacancy/open-vacancy.compone
5859 styleUrl : "./feed.component.scss" ,
5960} )
6061export class FeedComponent implements OnInit , AfterViewInit , OnDestroy {
61- route = inject ( ActivatedRoute ) ;
62- projectNewsService = inject ( ProjectNewsService ) ;
63- profileNewsService = inject ( ProfileNewsService ) ;
64- feedService = inject ( FeedService ) ;
62+ private readonly route = inject ( ActivatedRoute ) ;
63+ private readonly projectNewsService = inject ( ProjectNewsService ) ;
64+ private readonly profileNewsService = inject ( ProfileNewsService ) ;
65+ private readonly feedService = inject ( FeedService ) ;
66+ private readonly cdref = inject ( ChangeDetectorRef ) ;
67+ private observer ?: IntersectionObserver ;
6568
6669 /**
6770 * ИНИЦИАЛИЗАЦИЯ КОМПОНЕНТА
@@ -71,49 +74,36 @@ export class FeedComponent implements OnInit, AfterViewInit, OnDestroy {
7174 * - Настраивает наблюдение за изменениями фильтров
7275 * - Инициализирует отслеживание просмотров элементов
7376 */
77+ /**
78+ * ИНИЦИАЛИЗАЦИЯ КОМПОНЕНТА
79+ */
7480 ngOnInit ( ) {
75- // Получаем предзагруженные данные из резолвера
7681 const routeData$ = this . route . data
7782 . pipe ( map ( r => r [ "data" ] ) )
7883 . subscribe ( ( feed : ApiPagination < FeedItem > ) => {
7984 this . feedItems . set ( feed . results ) ;
8085 this . totalItemsCount . set ( feed . count ) ;
8186
82- // Настраиваем отслеживание просмотров элементов
83- setTimeout ( ( ) => {
84- const observer = new IntersectionObserver ( this . onFeedItemView . bind ( this ) , {
85- root : document . querySelector ( ".office__body" ) ,
86- rootMargin : "0px 0px 0px 0px" ,
87- threshold : 0 ,
88- } ) ;
89-
90- document . querySelectorAll ( ".page__item" ) . forEach ( e => {
91- observer . observe ( e ) ;
92- } ) ;
93- } ) ;
87+ this . initObserver ( ) ;
9488 } ) ;
9589 this . subscriptions$ ( ) . push ( routeData$ ) ;
9690
97- // Отслеживаем изменения параметров фильтрации
9891 const queryParams$ = this . route . queryParams
9992 . pipe (
100- map ( params => params [ "includes" ] ) ,
101- tap ( includes => {
102- this . includes . set ( includes ) ;
103- } ) ,
93+ map ( p => p [ "includes" ] ) ,
94+ tap ( includes => this . includes . set ( includes ) ) ,
10495 skip ( 1 ) ,
10596 concatMap ( includes => {
10697 this . totalItemsCount . set ( 0 ) ;
107- this . feedPage . set ( 0 ) ;
108-
98+ this . feedPage . set ( 1 ) ;
10999 return this . onFetch ( 0 , this . perFetchTake ( ) , includes ?? [ "vacancy" , "projects" , "news" ] ) ;
110100 } )
111101 )
112102 . subscribe ( feed => {
113103 this . feedItems . set ( feed ) ;
114-
115104 setTimeout ( ( ) => {
116- this . feedRoot ?. nativeElement . children [ 0 ] . scrollIntoView ( { behavior : "smooth" } ) ;
105+ this . feedRoot ?. nativeElement . scrollTo ( { top : 0 } ) ;
106+ this . observeNewElements ( ) ;
117107 } ) ;
118108 } ) ;
119109 this . subscriptions$ ( ) . push ( queryParams$ ) ;
@@ -132,7 +122,7 @@ export class FeedComponent implements OnInit, AfterViewInit, OnDestroy {
132122 const scrollEvents$ = fromEvent ( target , "scroll" )
133123 . pipe (
134124 concatMap ( ( ) => this . onScroll ( ) ) ,
135- throttleTime ( 500 ) // Ограничиваем частоту запросов
125+ throttleTime ( 500 )
136126 )
137127 . subscribe ( noop ) ;
138128
@@ -141,7 +131,8 @@ export class FeedComponent implements OnInit, AfterViewInit, OnDestroy {
141131 }
142132
143133 ngOnDestroy ( ) {
144- this . subscriptions$ ( ) . forEach ( $ => $ . unsubscribe ( ) ) ;
134+ this . subscriptions$ ( ) . forEach ( s => s . unsubscribe ( ) ) ;
135+ this . observer ?. disconnect ( ) ;
145136 }
146137
147138 @ViewChild ( "feedRoot" ) feedRoot ?: ElementRef < HTMLElement > ;
@@ -224,7 +215,7 @@ export class FeedComponent implements OnInit, AfterViewInit, OnDestroy {
224215 * - Отмечает новости как прочитанные при попадании в область видимости
225216 * - Различает новости проектов и профилей
226217 */
227- onFeedItemView ( entries : IntersectionObserverEntry [ ] ) : void {
218+ private onFeedItemView ( entries : IntersectionObserverEntry [ ] ) : void {
228219 const items = entries
229220 . map ( e => {
230221 return Number ( ( e . target as HTMLElement ) . dataset [ "id" ] ) ;
@@ -266,33 +257,41 @@ export class FeedComponent implements OnInit, AfterViewInit, OnDestroy {
266257 * - Проверяет, достигнут ли конец списка
267258 * - Загружает следующую порцию элементов при необходимости
268259 */
269- onScroll ( ) {
270- // Проверяем, загружены ли все элементы
271- if ( this . totalItemsCount ( ) && this . feedItems ( ) . length >= this . totalItemsCount ( ) ) return of ( { } ) ;
260+ /**
261+ * ОБРАБОТКА ПРОКРУТКИ ДЛЯ БЕСКОНЕЧНОЙ ЗАГРУЗКИ
262+ */
263+ private onScroll ( ) {
264+ const container = document . querySelector ( ".office__body" ) as HTMLElement ;
265+ if ( ! container ) return of ( { } ) ;
272266
273- const target = document . querySelector ( ".office__body" ) ;
274- if ( ! target || ! this . feedRoot ) return of ( { } ) ;
267+ const isNearBottom =
268+ container . scrollHeight - container . scrollTop - container . clientHeight < 100 ;
275269
276- // Вычисляем, нужно ли загружать новые элементы
277- const diff =
278- target . scrollTop -
279- this . feedRoot . nativeElement . getBoundingClientRect ( ) . height +
280- window . innerHeight ;
270+ if ( ! isNearBottom ) return of ( { } ) ;
281271
282- if ( diff > 0 ) {
283- return this . onFetch (
284- this . feedPage ( ) * this . perFetchTake ( ) ,
285- this . perFetchTake ( ) ,
286- this . includes ( )
287- ) . pipe (
288- tap ( ( feedChunk : FeedItem [ ] ) => {
289- this . feedPage . update ( page => page + 1 ) ;
290- this . feedItems . update ( items => [ ...items , ...feedChunk ] ) ;
291- } )
292- ) ;
272+ // Предотвращаем множественные запросы
273+ if ( this . feedItems ( ) . length >= this . totalItemsCount ( ) ) {
274+ return of ( [ ] ) ;
293275 }
294276
295- return of ( { } ) ;
277+ return this . onFetch (
278+ this . feedPage ( ) * this . perFetchTake ( ) ,
279+ this . perFetchTake ( ) ,
280+ this . includes ( )
281+ ) . pipe (
282+ tap ( ( feedChunk : FeedItem [ ] ) => {
283+ if ( feedChunk . length > 0 ) {
284+ this . feedPage . update ( p => p + 1 ) ;
285+ this . feedItems . update ( items => [ ...items , ...feedChunk ] ) ;
286+
287+ // ВАЖНО: обновляем observer после добавления новых элементов
288+ setTimeout ( ( ) => {
289+ this . observeNewElements ( ) ;
290+ this . cdref . detectChanges ( ) ;
291+ } , 0 ) ;
292+ }
293+ } )
294+ ) ;
296295 }
297296
298297 /**
@@ -306,7 +305,7 @@ export class FeedComponent implements OnInit, AfterViewInit, OnDestroy {
306305 * ЧТО ВОЗВРАЩАЕТ:
307306 * @returns Observable<FeedItem[]> - массив элементов ленты
308307 */
309- onFetch (
308+ private onFetch (
310309 offset : number ,
311310 limit : number ,
312311 includes : FeedItemType [ ] = [ "project" , "vacancy" , "news" ]
@@ -318,4 +317,33 @@ export class FeedComponent implements OnInit, AfterViewInit, OnDestroy {
318317 map ( res => res . results )
319318 ) ;
320319 }
320+
321+ private initObserver ( ) {
322+ if ( this . observer ) {
323+ this . observer . disconnect ( ) ;
324+ }
325+
326+ this . observer = new IntersectionObserver ( this . onFeedItemView . bind ( this ) , {
327+ root : null ,
328+ rootMargin : "0px" ,
329+ threshold : 0.1 ,
330+ } ) ;
331+
332+ this . observeNewElements ( ) ;
333+ }
334+
335+ /**
336+ * ДОБАВЛЕНИЕ НОВЫХ ЭЛЕМЕНТОВ
337+ */
338+ private observeNewElements ( ) {
339+ // Небольшая задержка для рендеринга DOM
340+ setTimeout ( ( ) => {
341+ document . querySelectorAll ( ".page__item" ) . forEach ( element => {
342+ if ( element && ! element . hasAttribute ( "data-observed" ) ) {
343+ this . observer ?. observe ( element ) ;
344+ element . setAttribute ( "data-observed" , "true" ) ;
345+ }
346+ } ) ;
347+ } , 0 ) ;
348+ }
321349}
0 commit comments