Compare commits

...

4 Commits

5 changed files with 121 additions and 117 deletions

View File

@@ -16,7 +16,6 @@ export interface ArticleSearchState {
hits: number; hits: number;
selectedBranch: BranchDTO; selectedBranch: BranchDTO;
selectedItemIds: number[]; selectedItemIds: number[];
scrollPosition: number;
defaultSettings?: UISettingsDTO; defaultSettings?: UISettingsDTO;
} }
@@ -44,12 +43,6 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
return this.get((s) => s.items); return this.get((s) => s.items);
} }
scrollPosition$ = this.select((s) => s.scrollPosition);
get scrollPosition() {
return this.get((s) => s.scrollPosition);
}
selectedBranch$ = this.select((s) => s.selectedBranch); selectedBranch$ = this.select((s) => s.selectedBranch);
get selectedBranch() { get selectedBranch() {
@@ -92,7 +85,6 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
searchState: '', searchState: '',
selectedItemIds: [], selectedItemIds: [],
selectedBranch: undefined, selectedBranch: undefined,
scrollPosition: 0,
}); });
this.setDefaultFilter(); this.setDefaultFilter();
} }
@@ -113,10 +105,6 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
this.patchState({ selectedBranch }); this.patchState({ selectedBranch });
} }
setScrollPosition(scrollPosition: number) {
this.patchState({ scrollPosition });
}
async setDefaultFilter(defaultQueryParams?: Record<string, string>) { async setDefaultFilter(defaultQueryParams?: Record<string, string>) {
const defaultSettings = await this.catalog.getSettings().toPromise(); const defaultSettings = await this.catalog.getSettings().toPromise();

View File

@@ -1,10 +1,7 @@
<a <div
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 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-primary]="primaryOutletActive" [class.page-search-result-item__item-card-primary]="primaryOutletActive"
[routerLink]="detailsPath" [class.active]="isActive"
[routerLinkActive]="!isTablet && !primaryOutletActive ? 'active' : ''"
queryParamsHandling="preserve"
(click)="isDesktopLarge ? scrollIntoView() : ''"
> >
<div class="page-search-result-item__item-thumbnail text-center mr-4 w-[3.125rem] h-[4.9375rem]"> <div class="page-search-result-item__item-thumbnail text-center mr-4 w-[3.125rem] h-[4.9375rem]">
<img <img
@@ -122,4 +119,4 @@
</ng-container> </ng-container>
</div> </div>
</div> </div>
</a> </div>

View File

@@ -1,5 +1,5 @@
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostBinding, ElementRef } from '@angular/core'; import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostBinding } from '@angular/core';
import { ApplicationService } from '@core/application'; import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment'; import { EnvironmentService } from '@core/environment';
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability'; import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
@@ -54,6 +54,8 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
@Input() @Input()
primaryOutletActive?: boolean = false; primaryOutletActive?: boolean = false;
@Input() isActive: boolean;
@Output() @Output()
selectedChange = new EventEmitter<ItemDTO>(); selectedChange = new EventEmitter<ItemDTO>();
@@ -82,11 +84,6 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
return this._environment.matchDesktopLarge(); return this._environment.matchDesktopLarge();
} }
get detailsPath() {
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: this.item?.id })
.path;
}
get resultsPath() { get resultsPath() {
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId).path; return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId).path;
} }
@@ -141,7 +138,6 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
private _availability: DomainAvailabilityService, private _availability: DomainAvailabilityService,
private _environment: EnvironmentService, private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService, private _navigationService: ProductCatalogNavigationService,
private _elRef: ElementRef<HTMLElement>,
private _store: Store private _store: Store
) { ) {
super({ super({
@@ -150,10 +146,6 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
}); });
} }
scrollIntoView() {
this._elRef.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
setSelected() { setSelected() {
const isSelected = this._articleSearchService.selectedItemIds.includes(this.item?.id); const isSelected = this._articleSearchService.selectedItemIds.includes(this.item?.id);
this._articleSearchService.setSelected({ selected: !isSelected, itemId: this.item?.id }); this._articleSearchService.setSelected({ selected: !isSelected, itemId: this.item?.id });

View File

@@ -46,37 +46,74 @@
</shared-order-by-filter> </shared-order-by-filter>
</div> </div>
<div class="h-full relative"> <ng-container *ngIf="primaryOutletActive$ | async; else sideOutlet">
<cdk-virtual-scroll-viewport <div class="h-full relative">
#scrollContainer <cdk-virtual-scroll-viewport class="product-list h-full" itemSize="98" (scrolledIndexChange)="scrolledIndexChange($event)">
class="product-list h-full" <a
[itemSize]="(primaryOutletActive$ | async) ? 98 : 181" *cdkVirtualFor="let item of results$ | async; let i = index; trackBy: trackByItemId"
[maxBufferPx]="maxBufferCdkScrollContainer$ | async" [routerLink]="getDetailsPath(item.id)"
(scrolledIndexChange)="scrolledIndexChange($event)" routerLinkActive
> #rla="routerLinkActive"
<search-result-item queryParamsHandling="preserve"
class="page-search-results__result-item" (click)="scrollToItem(i)"
[class.page-search-results__result-item-primary]="primaryOutletActive$ | async" >
*cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId" <search-result-item
(selectedChange)="addToCart($event)" class="page-search-results__result-item page-search-results__result-item-primary"
[selected]="isSelected(item)" (selectedChange)="addToCart($event)"
[selectable]="isSelectable(item)" [selected]="isSelected(item)"
[item]="item" [selectable]="isSelectable(item)"
[primaryOutletActive]="primaryOutletActive$ | async" [item]="item"
></search-result-item> [primaryOutletActive]="true"
<page-search-result-item-loading [isActive]="rla.isActive"
[primaryOutletActive]="primaryOutletActive$ | async" ></search-result-item>
*ngIf="fetching$ | async" </a>
></page-search-result-item-loading> <page-search-result-item-loading [primaryOutletActive]="true" *ngIf="fetching$ | async"></page-search-result-item-loading>
</cdk-virtual-scroll-viewport> </cdk-virtual-scroll-viewport>
<div class="actions z-sticky h-0"> <div class="actions z-sticky h-0">
<button <button
[disabled]="loading$ | async" [disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0" *ngIf="(selectedItemIds$ | async)?.length > 0"
class="cta-cart cta-action-primary" class="cta-cart cta-action-primary"
(click)="addToCart()" (click)="addToCart()"
> >
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner> <ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button> </button>
</div>
</div> </div>
</div> </ng-container>
<ng-template #sideOutlet>
<div class="h-full relative">
<cdk-virtual-scroll-viewport class="product-list h-full" itemSize="181" (scrolledIndexChange)="scrolledIndexChange($event)">
<a
*cdkVirtualFor="let item of results$ | async; let i = index; trackBy: trackByItemId"
[routerLink]="getDetailsPath(item.id)"
routerLinkActive
#rla="routerLinkActive"
queryParamsHandling="preserve"
(click)="scrollToItem(i)"
>
<search-result-item
class="page-search-results__result-item"
(selectedChange)="addToCart($event)"
[selected]="isSelected(item)"
[selectable]="isSelectable(item)"
[item]="item"
[primaryOutletActive]="false"
[isActive]="rla.isActive"
></search-result-item>
</a>
<page-search-result-item-loading [primaryOutletActive]="false" *ngIf="fetching$ | async"></page-search-result-item-loading>
</cdk-virtual-scroll-viewport>
<div class="actions z-sticky h-0">
<button
[disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0"
class="cta-cart cta-action-primary"
(click)="addToCart()"
>
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button>
</div>
</div>
</ng-template>

View File

@@ -28,6 +28,7 @@ import { SearchResultItemComponent } from './search-result-item.component';
import { ProductCatalogNavigationService } from '@shared/services'; import { ProductCatalogNavigationService } from '@shared/services';
import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib'; import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib';
import { DomainAvailabilityService, ItemData } from '@domain/availability'; import { DomainAvailabilityService, ItemData } from '@domain/availability';
import { asapScheduler } from 'rxjs';
@Component({ @Component({
selector: 'page-search-results', selector: 'page-search-results',
@@ -37,7 +38,7 @@ import { DomainAvailabilityService, ItemData } from '@domain/availability';
}) })
export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterViewInit { export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChildren(SearchResultItemComponent) listItems: QueryList<SearchResultItemComponent>; @ViewChildren(SearchResultItemComponent) listItems: QueryList<SearchResultItemComponent>;
@ViewChild('scrollContainer', { static: true }) @ViewChild(CdkVirtualScrollViewport, { static: false })
scrollContainer: CdkVirtualScrollViewport; scrollContainer: CdkVirtualScrollViewport;
@ViewChild(FilterInputGroupMainComponent, { static: false }) @ViewChild(FilterInputGroupMainComponent, { static: false })
@@ -59,6 +60,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
}) })
); );
getProcessId(): number {
return this.application.activatedProcessId;
}
loading$ = new BehaviorSubject<boolean>(false); loading$ = new BehaviorSubject<boolean>(false);
private subscriptions = new Subscription(); private subscriptions = new Subscription();
@@ -92,21 +97,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
return this._environment.matchDesktop$.pipe(map((matches) => matches && this.route.outlet === 'primary')); return this._environment.matchDesktop$.pipe(map((matches) => matches && this.route.outlet === 'primary'));
} }
// Ticket #4169 Splitscreen private readonly SCROLL_INDEX_TOKEN = 'CATALOG_RESULTS_LIST_SCROLL_INDEX';
// Render genug Artikel um bei Navigation auf Trefferliste | PDP zum angewählten Artikel zu Scrollen
maxBufferCdkScrollContainer$ = this.results$.pipe(
map((results) => {
if (this._environment.matchDesktop() && this.route.outlet === 'side' && results?.length > 0) {
// Splitscreen mode: Items Length * Item Pixel Height
const maxBufferSize = results.length * 181;
return maxBufferSize >= 1200 ? maxBufferSize : 1200;
} else if (this._environment.matchTablet()) {
return 500;
} else {
return 1200;
}
})
);
constructor( constructor(
public searchService: ArticleSearchService, public searchService: ArticleSearchService,
@@ -157,9 +148,8 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
const cleanQueryParams = this.cleanupQueryParams(queryParams); const cleanQueryParams = this.cleanupQueryParams(queryParams);
// Scroll to scroll_position in great result list if (this.route.outlet === 'primary' && processChanged) {
if (!!queryParams?.scroll_position && this.route.outlet === 'primary') { this.scrollToItem(this._getScrollIndexFromCache());
this.scrollTop(Number(queryParams.scroll_position ?? 0));
} }
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) { if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
@@ -175,11 +165,6 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
) { ) {
this.search({ clear: true }); this.search({ clear: true });
} else { } else {
if (!this.isDesktopLarge || this.route.outlet === 'primary') {
this.scrollTop(Number(queryParams.scroll_position ?? 0));
} else {
this.scrollItemIntoView();
}
const selectedItemIds: Array<string> = queryParams?.selected_item_ids?.split(',') ?? []; const selectedItemIds: Array<string> = queryParams?.selected_item_ids?.split(',') ?? [];
for (const id of selectedItemIds) { for (const id of selectedItemIds) {
if (id) { if (id) {
@@ -272,7 +257,39 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.scrollItemIntoView(); this.scrollToItem(this._getScrollIndexFromCache());
}
private _addScrollIndexToCache(index: number): void {
this.cache.set<number>({ processId: this.getProcessId(), token: this.SCROLL_INDEX_TOKEN }, index);
}
private _getScrollIndexFromCache(): number {
return this.cache.get<number>({ processId: this.getProcessId(), token: this.SCROLL_INDEX_TOKEN });
}
scrollToItem(i?: number) {
let index = i;
if (!index) {
index = this._getScrollIndexFromCache();
} else {
this._addScrollIndexToCache(index);
}
asapScheduler.schedule(() => {
this.scrollContainer.scrollToIndex(index, 'smooth');
}, 150);
}
scrolledIndexChange(index: number) {
if (index && this.searchService.items.length <= this.scrollContainer?.getRenderedRange()?.end) {
this.search({ clear: false });
}
if (this.getProcessId() === this.searchService.processId) {
this._addScrollIndexToCache(index);
}
} }
async ngOnDestroy() { async ngOnDestroy() {
@@ -306,40 +323,14 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
this.searchService.search({ clear, orderBy, doNotTrack: true }); this.searchService.search({ clear, orderBy, doNotTrack: true });
} }
scrollTop(scrollPos: number) { getDetailsPath(itemId: number) {
setTimeout(() => this.scrollContainer.scrollTo({ top: scrollPos }), 0); return this._navigationService.getArticleDetailsPath({ processId: this.application.activatedProcessId, itemId }).path;
}
scrollItemIntoView() {
setTimeout(() => {
const item = this.listItems?.find((item) => item.item.id === Number(this.route?.snapshot?.params?.id));
item?.scrollIntoView();
}, 0);
}
async scrolledIndexChange(index: number) {
alert('scrolledIndexChange');
const results = await this.results$.pipe(first()).toPromise();
const hits = await this.hits$.pipe(first()).toPromise();
if (results.length >= hits) {
return;
}
if (this.route.outlet === 'primary') {
this.searchService.setScrollPosition(this.scrollContainer.measureScrollOffset('top'));
}
if (index >= results.length - 20 && results.length - 20 > 0) {
this.search({ clear: false });
}
} }
async updateBreadcrumbs( async updateBreadcrumbs(
processId: number = this.searchService.processId, processId: number = this.searchService.processId,
queryParams: Record<string, string> = this.searchService.filter?.getQueryParams() queryParams: Record<string, string> = this.searchService.filter?.getQueryParams()
) { ) {
const scroll_position = this.searchService.scrollPosition;
const selected_item_ids = this.searchService?.selectedItemIds?.toString(); const selected_item_ids = this.searchService?.selectedItemIds?.toString();
if (queryParams) { if (queryParams) {
@@ -349,7 +340,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
.toPromise(); .toPromise();
const name = queryParams.main_qs ? queryParams.main_qs : 'Alle Artikel'; const name = queryParams.main_qs ? queryParams.main_qs : 'Alle Artikel';
const params = { ...queryParams, scroll_position, selected_item_ids }; const params = { ...queryParams, selected_item_ids };
for (const crumb of crumbs) { for (const crumb of crumbs) {
this.breadcrumb.patchBreadcrumb(crumb.id, { this.breadcrumb.patchBreadcrumb(crumb.id, {
@@ -402,7 +393,6 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
cleanupQueryParams(params: Record<string, string> = {}) { cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params }; const clean = { ...params };
delete clean['scroll_position'];
delete clean['selected_item_ids']; delete clean['selected_item_ids'];
for (const key in clean) { for (const key in clean) {