Merged PR 1556: #4067 RD Artikelsuche Ergebnisliste Performance und Scrollposition Update

#4067 RD Artikelsuche Ergebnisliste Performance und Scrollposition Update
This commit is contained in:
Nino Righi
2023-06-09 09:11:10 +00:00
committed by Andreas Schickinger
parent d86f595b1f
commit be1a9e8f7e
11 changed files with 80 additions and 58 deletions

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { Platform } from '@angular/cdk/platform';
import { NativeContainerService } from 'native-container';
import { BreakpointObserver } from '@angular/cdk/layout';
import { shareReplay } from 'rxjs/operators';
const MATCH_TABLET = '(max-width: 1024px)';
@@ -23,19 +24,19 @@ export class EnvironmentService {
return this._breakpointObserver.isMatched(MATCH_TABLET);
}
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET);
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET).pipe(shareReplay());
matchDesktopSmall(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_SMALL);
}
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL);
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL).pipe(shareReplay());
matchDesktop(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP);
}
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP);
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP).pipe(shareReplay());
/**
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.

View File

@@ -8,7 +8,6 @@ import { FocusSearchboxEvent } from './focus-searchbox.event';
import { ArticleSearchMainAutocompleteProvider } from './providers';
import { ProductCatalogNavigationService } from '@shared/services';
import { FilterAutocompleteProvider } from 'apps/shared/components/filter/src/lib';
import { isEqual } from 'lodash';
import { EnvironmentService } from '@core/environment';
@Component({

View File

@@ -8,7 +8,7 @@
[routerLink]="closeFilterRoute"
queryParamsHandling="preserve"
>
<ui-icon icon="close" size="15px"></ui-icon>
<ui-svg-icon icon="close" [size]="25"></ui-svg-icon>
</a>
</div>

View File

@@ -1,4 +1,4 @@
<ng-container *ngIf="!(mainOutletActive$ | async); else mainOutlet">
<ng-container *ngIf="!mainOutletActive; else mainOutlet">
<div class="bg-ucla-blue rounded w-[4.375rem] h-[5.625rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-col flex-grow">
<div class="h-4 bg-ucla-blue ml-4 mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>

View File

@@ -1,7 +1,4 @@
import { Component, ChangeDetectionStrategy, HostBinding } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductCatalogNavigationService } from '@shared/services';
import { shareReplay } from 'rxjs/operators';
import { Component, ChangeDetectionStrategy, HostBinding, Input } from '@angular/core';
@Component({
selector: 'page-search-result-item-loading',
@@ -10,13 +7,12 @@ import { shareReplay } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchResultItemLoadingComponent {
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
}
@Input()
mainOutletActive?: boolean = false;
constructor(private _navigationService: ProductCatalogNavigationService, private _activatedRoute: ActivatedRoute) {}
constructor() {}
@HostBinding('style') get class() {
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
return this.mainOutletActive ? { height: '6.125rem' } : '';
}
}

View File

@@ -1,9 +1,10 @@
<a
class="page-search-result-item__item-card hover p-5 desktop-small:px-4 desktop-small:py-[0.625rem] h-[13.25rem] desktop-small:h-[11.3125rem] bg-white border border-solid border-transparent rounded"
[class.page-search-result-item__item-card-main]="mainOutletActive$ | async"
[class.page-search-result-item__item-card-main]="mainOutletActive"
[routerLink]="detailsPath"
[routerLinkActive]="!isTablet && !(mainOutletActive$ | async) ? 'active' : ''"
[routerLinkActive]="!isTablet && !mainOutletActive ? 'active' : ''"
[queryParamsHandling]="!isTablet ? 'preserve' : ''"
(click)="isDesktop ? scrollIntoView() : ''"
>
<div class="page-search-result-item__item-thumbnail text-center mr-4 w-[50px] h-[79px]">
<img
@@ -15,10 +16,7 @@
/>
</div>
<div
class="page-search-result-item__item-grid-container"
[class.page-search-result-item__item-grid-container-main]="mainOutletActive$ | async"
>
<div class="page-search-result-item__item-grid-container" [class.page-search-result-item__item-grid-container-main]="mainOutletActive">
<div
class="page-search-result-item__item-contributors desktop-small:text-p3 font-bold text-[#0556B4] text-ellipsis overflow-hidden max-w-[24rem] whitespace-nowrap"
>
@@ -67,7 +65,7 @@
<div
class="page-search-result-item__item-price desktop-small:text-p3 font-bold justify-self-end"
[class.page-search-result-item__item-price-main]="mainOutletActive$ | async"
[class.page-search-result-item__item-price-main]="mainOutletActive"
>
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
</div>
@@ -85,7 +83,7 @@
<div
class="page-search-result-item__item-stock desktop-small:text-p3 font-bold z-dropdown justify-self-start"
[class.justify-self-end]="!(mainOutletActive$ | async)"
[class.justify-self-end]="!mainOutletActive"
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
>
@@ -115,9 +113,9 @@
<div
class="page-search-result-item__item-ssc desktop-small:text-p3 w-full text-right overflow-hidden text-ellipsis whitespace-nowrap"
[class.page-search-result-item__item-ssc-main]="mainOutletActive$ | async"
[class.page-search-result-item__item-ssc-main]="mainOutletActive"
>
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive$ | async">
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive">
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
</div>
<strong>{{ item?.catalogAvailability?.ssc }}</strong> - {{ item?.catalogAvailability?.sscText }}

View File

@@ -1,5 +1,5 @@
import { DatePipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostListener, HostBinding } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostBinding, ElementRef } from '@angular/core';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
@@ -8,10 +8,9 @@ import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { isEqual } from 'lodash';
import { combineLatest } from 'rxjs';
import { debounceTime, switchMap, map, shareReplay, filter, first } from 'rxjs/operators';
import { debounceTime, switchMap, map, shareReplay, filter } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { ProductCatalogNavigationService } from '@shared/services';
import { ActivatedRoute } from '@angular/router';
export interface SearchResultItemComponentState {
item?: ItemDTO;
@@ -51,6 +50,9 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
}
}
@Input()
mainOutletActive?: boolean = false;
@Output()
selectedChange = new EventEmitter<ItemDTO>();
@@ -75,6 +77,10 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
return this._environment.matchTablet();
}
get isDesktop() {
return this._environment.matchDesktop();
}
get detailsPath() {
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: this.item?.id });
}
@@ -83,10 +89,6 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
}
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
}
defaultBranch$ = this._availability.getDefaultBranch();
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
@@ -136,7 +138,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
private _availability: DomainAvailabilityService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService,
private _activatedRoute: ActivatedRoute
private _elRef: ElementRef<HTMLElement>
) {
super({
selected: false,
@@ -144,6 +146,10 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
});
}
scrollIntoView() {
this._elRef.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
setSelected() {
const isSelected = this._articleSearchService.selectedItemIds.includes(this.item?.id);
this._articleSearchService.setSelected({ selected: !isSelected, itemId: this.item?.id });
@@ -154,6 +160,6 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
}
@HostBinding('style') get class() {
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
return this.mainOutletActive ? { height: '6.125rem' } : '';
}
}

View File

@@ -51,8 +51,8 @@
#scrollContainer
class="product-list"
[itemSize]="(mainOutletActive$ | async) ? 98 : 187"
minBufferPx="2800"
maxBufferPx="2800"
minBufferPx="935"
maxBufferPx="1200"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<search-result-item
@@ -62,8 +62,12 @@
(selectedChange)="addToCart($event)"
[selectable]="isSelectable(item)"
[item]="item"
[mainOutletActive]="mainOutletActive$ | async"
></search-result-item>
<page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading>
<page-search-result-item-loading
[mainOutletActive]="mainOutletActive$ | async"
*ngIf="fetching$ | async"
></page-search-result-item-loading>
</cdk-virtual-scroll-viewport>
<div *ngIf="isTablet" class="actions z-fixed">

View File

@@ -1,5 +1,15 @@
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, ViewChildren, QueryList, TrackByFunction } from '@angular/core';
import {
Component,
ChangeDetectionStrategy,
OnInit,
OnDestroy,
ViewChild,
ViewChildren,
QueryList,
TrackByFunction,
AfterViewInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
@@ -11,7 +21,7 @@ import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { CacheService } from 'apps/core/cache/src/public-api';
import { isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, first, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
import { debounceTime, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
import { SearchResultItemComponent } from './search-result-item.component';
@@ -24,7 +34,7 @@ import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/fi
styleUrls: ['search-results.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChildren(SearchResultItemComponent) listItems: QueryList<SearchResultItemComponent>;
@ViewChild('scrollContainer', { static: true })
scrollContainer: CdkVirtualScrollViewport;
@@ -58,6 +68,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
return this._environment.matchTablet();
}
get isDesktop() {
return this._environment.matchDesktop();
}
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
map(([filter, defaultFilter]) => {
const filterQueryParams = filter?.getQueryParams();
@@ -74,7 +88,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
}
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this.route).pipe(shareReplay());
return this._environment.matchTablet$.pipe(map((state) => this._navigationService.mainOutletActive(this.route, state?.matches)));
}
constructor(
@@ -134,7 +148,11 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
if (data.items?.length === 0) {
this.search();
} else {
this.scrollTop(Number(queryParams.scroll_position ?? 0));
if (!this.isDesktop || this._navigationService.mainOutletActive(this.route)) {
this.scrollTop(Number(queryParams.scroll_position ?? 0));
} else {
this.scrollItemIntoView();
}
const selectedItemIds: Array<string> = queryParams?.selected_item_ids?.split(',') ?? [];
for (const id of selectedItemIds) {
if (id) {
@@ -158,9 +176,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
const params = state.filter.getQueryParams();
if ((state.hits === 1 && this.isTablet) || (!this.isTablet && !this._navigationService.mainOutletActive(this.route))) {
const item = state.items.find((f) => f);
const itemId = this.route?.snapshot?.params?.id ? Number(this.route?.snapshot?.params?.id) : item.id; // Nicht zum ersten Item der Liste springen wenn bereits eines selektiert ist
this._navigationService.navigateToDetails({
processId,
itemId: item.id,
itemId,
queryParams: this.isTablet ? undefined : params,
});
} else if (this.isTablet || this._navigationService.mainOutletActive(this.route)) {
@@ -174,6 +193,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
);
}
ngAfterViewInit(): void {
this.scrollItemIntoView();
}
ngOnDestroy() {
this.subscriptions?.unsubscribe();
@@ -217,6 +240,13 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
setTimeout(() => this.scrollContainer.scrollTo({ top: scrollPos }), 0);
}
scrollItemIntoView() {
setTimeout(() => {
const item = this.listItems?.find((item) => item.item.id === Number(this.route?.snapshot?.params?.id));
item?.scrollIntoView();
}, 0);
}
async scrolledIndexChange(index: number) {
const results = await this.results$.pipe(first()).toPromise();
const hits = await this.hits$.pipe(first()).toPromise();

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core';
import { NavigationDetails, OutletLocations, OutletParams } from './defs';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export abstract class BaseNavigationService {
@@ -9,7 +8,6 @@ export abstract class BaseNavigationService {
abstract getOutletLocations(activatedRoute: ActivatedRoute): OutletLocations;
abstract getOutletParams(activatedRoute: ActivatedRoute): OutletParams;
abstract mainOutletActive$(activatedRoute: ActivatedRoute): Observable<boolean>;
abstract mainOutletActive(activatedRoute: ActivatedRoute): boolean;
protected abstract _navigateTo(navigation: NavigationDetails): Promise<boolean>;
}

View File

@@ -3,8 +3,6 @@ import { ActivatedRoute, Router } from '@angular/router';
import { EnvironmentService } from '@core/environment';
import { NavigationDetails, OutletLocations, OutletParams } from './defs';
import { BaseNavigationService } from './base-navigation.service';
import { Observable, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class NavigationService extends BaseNavigationService {
@@ -28,17 +26,9 @@ export class NavigationService extends BaseNavigationService {
};
}
mainOutletActive$(activatedRoute: ActivatedRoute): Observable<boolean> {
return combineLatest([this._environment.matchDesktopSmall$, this._environment.matchDesktop$]).pipe(
map(
([desktopSmallState, desktopState]) =>
!!this.getOutletLocations(activatedRoute)?.main && (desktopSmallState.matches || desktopState.matches)
)
);
}
mainOutletActive(activatedRoute: ActivatedRoute): boolean {
return !!this.getOutletLocations(activatedRoute)?.main && (this._environment.matchDesktopSmall() || this._environment.matchDesktop());
mainOutletActive(activatedRoute: ActivatedRoute, isTablet?: boolean): boolean {
const matchesTablet = isTablet ?? this._environment.matchTablet();
return !!this.getOutletLocations(activatedRoute)?.main && !matchesTablet;
}
protected async _navigateTo(navigation: NavigationDetails): Promise<boolean> {