import {SearchTemplatesComponent} from './search-templates.component';
import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
import {PreferencesComponent} from './prefs.component';
+import {BrowsePagerComponent} from './result/browse-pager.component';
@NgModule({
declarations: [
CnBrowseComponent,
OpacViewComponent,
PreferencesComponent,
- CnBrowseResultsComponent
+ CnBrowseResultsComponent,
+ BrowsePagerComponent
],
imports: [
StaffCommonModule,
// Add digital bookplate to search options.
enableBookplates = false;
+ // Cache of browse results so the browse pager is not forced to
+ // re-run the browse search on each navigation.
+ browsePagerResults: any[];
+
constructor(
private router: Router,
private route: ActivatedRoute,
--- /dev/null
+
+<div class="row mb-2 p-2 border border-info rounded">
+
+ <ng-container *ngIf="browseLoading">
+ <div class="col-lg-6 offset-lg-3">
+ <eg-progress-inline></eg-progress-inline>
+ </div>
+ </ng-container>
+
+ <ng-container *ngIf="!browseLoading">
+ <div class="col-lg-4">
+ <div *ngIf="prevEntry" class="d-flex">
+ <div class="">
+ <button class="btn btn-sm btn-outline-dark mr-2"
+ (click)="goToBrowsePage(true)" i18n>Previous Heading</button>
+ </div>
+ <div class="flex-1 pt-1">
+ <span>{{prevEntry.value}}</span>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-4 d-flex justify-content-center">
+ <div class="pt-1">
+ Heading: <span class="font-weight-bold">
+ {{searchContext.termSearch.browseEntry.value()}}
+ </span>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div *ngIf="nextEntry" class="float-right">
+ <div class="d-flex">
+ <div class="flex-1 pt-1">
+ <span>{{nextEntry.value}}</span>
+ </div>
+ <div class="">
+ <button class="btn btn-sm btn-outline-dark ml-2"
+ (click)="goToBrowsePage(false)" i18n>Next Heading</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </ng-container>
+
+</div>
+
--- /dev/null
+import {Component, OnInit, OnDestroy, Input} from '@angular/core';
+import {Observable, Subscription} from 'rxjs';
+import {tap, map, switchMap, distinctUntilChanged} from 'rxjs/operators';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
+import {StaffCatalogService} from '../catalog.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {BasketService} from '@eg/share/catalog/basket.service';
+
+@Component({
+ selector: 'eg-catalog-browse-pager',
+ templateUrl: 'browse-pager.component.html'
+})
+export class BrowsePagerComponent implements OnInit {
+
+ searchContext: CatalogSearchContext;
+ browseLoading = false;
+ prevEntry: any;
+ nextEntry: any;
+
+ constructor(
+ private cat: CatalogService,
+ private staffCat: StaffCatalogService
+ ) {}
+
+ ngOnInit() {
+ this.searchContext = this.staffCat.searchContext;
+ this.fetchPageData().then(_ => this.setPrevNext());
+ }
+
+ fetchPageData(): Promise<any> {
+
+ let found = false;
+ const mbeId = this.pageEntryId();
+ this.staffCat.browsePagerResults.forEach(result => {
+ if (result.browse_entry === mbeId) {
+ found = true;
+ }
+ });
+
+ if (found) {
+ return Promise.resolve();
+ } else {
+ return this.fetchBrowseData(null);
+ }
+ }
+
+ // Grab a page of browse results
+ fetchBrowseData(prev: boolean): Promise<any> {
+ const ctx = this.searchContext.clone();
+ ctx.termSearch.hasBrowseEntry = null; // avoid term search
+
+ if (prev !== null) {
+ ctx.browseSearch.pivot = this.getBoundaryPivot(prev);
+ }
+
+ // Grab a few extras so we can last a bit longer before
+ // having to fetch more browse data. Could make this bigger.
+ ctx.pager.limit = 20;
+
+ const results = [];
+ this.browseLoading = true;
+
+ return this.cat.browse(ctx)
+ .pipe(tap(result => results.push(result)))
+ .toPromise().then(_ => {
+ if (prev) {
+ this.staffCat.browsePagerResults =
+ results.concat(this.staffCat.browsePagerResults);
+ } else {
+ this.staffCat.browsePagerResults =
+ this.staffCat.browsePagerResults.concat(results);
+ }
+ this.browseLoading = false;
+ });
+ }
+
+ pageEntryId(): number {
+ return Number(
+ this.searchContext.termSearch.hasBrowseEntry.split(',')[0]
+ );
+ }
+
+ // Collect enough browse data to display previous, current, and
+ // next heading. This can mean fetching an additional page of data.
+ setPrevNext(): Promise<any> {
+ let previous: any;
+ const mbeId = this.pageEntryId();
+
+ this.staffCat.browsePagerResults.forEach(result => {
+ // ignore pivot and authority-only entries
+ if (!result.sources) { return; }
+
+ if (previous) {
+ if (result.browse_entry === mbeId) {
+ this.prevEntry = previous;
+ }
+ if (previous.browse_entry === mbeId) {
+ this.nextEntry = result;
+ }
+ }
+ previous = result;
+ });
+
+ if (!this.prevEntry) {
+ return this.fetchBrowseData(true);
+ } else if (!this.nextEntry) {
+ return this.fetchBrowseData(false);
+ } else {
+ return Promise.resolve();
+ }
+ }
+
+ getBoundaryPivot(prev?: boolean): number {
+ const results = this.staffCat.browsePagerResults;
+ return prev ? results[0].pivot_point :
+ results[results.length - 1].pivot_point;
+ }
+
+ setSearchPivot(prev?: boolean) {
+ // When traversing browse result page boundaries, modify the
+ // search pivot to keep up.
+
+ const mbeId = Number( // for this page
+ this.searchContext.termSearch.hasBrowseEntry.split(',')[0]);
+
+ const targetMbe = prev ?
+ this.prevEntry.browse_entry : this.nextEntry.browse_entry;
+
+ let results = [].concat(this.staffCat.browsePagerResults);
+ if (prev) { results.reverse(); }
+
+ let current;
+ let done = false;
+ results.forEach(result => {
+ if (done) { return; }
+
+ if (current) {
+
+ if (result.browse_entry === targetMbe) {
+ // The target entry is on the current page of results
+ // No pivot movement required.
+ done = true;
+
+ } else if (result.pivot) {
+ // We have crossed a pivot boundary.
+ this.searchContext.browseSearch.pivot = result.pivot;
+ done = true;
+ }
+
+ } else if (result.browse_entry === mbeId) {
+ current = result;
+ }
+ });
+ }
+
+ // Find the browse entry for the next/prev page and navigate there
+ // if possible. Returns false if not enough data is available.
+ goToBrowsePage(prev: boolean): boolean {
+ const ctx = this.searchContext;
+ const target = prev ? this.prevEntry : this.nextEntry;
+
+ if (!target) { return false; }
+
+ this.setSearchPivot();
+
+ // Jump to the selected browse entry's page.
+ ctx.termSearch.hasBrowseEntry = target.browse_entry + ',' + target.fields;
+ ctx.pager.offset = 0; // this is a new records-for-browse-entry search
+ this.staffCat.search();
+
+ return true;
+ }
+}
+
+
<!-- header, pager, and list of records -->
<div id="staff-catalog-results-container" *ngIf="searchHasResults()">
- <div class="row d-flex mb-2" *ngIf="searchContext.termSearch.browseEntry">
- <div class="col-lg-10 offset-lg-2">
- <h3 i18n>Results for browse entry "{{searchContext.termSearch.browseEntry.value()}}"</h3>
- </div>
- </div>
+
+ <eg-catalog-browse-pager *ngIf="searchContext.termSearch.browseEntry">
+ </eg-catalog-browse-pager>
+
<div class="row">
<div class="col-lg-2" *ngIf="!searchContext.basket">
<ng-container *ngIf="!searchContext.termSearch.browseEntry">
</span>
</label>
</div>
-
- <ng-container *ngIf="searchContext.browseSearch.value">
- <div class="col-lg-3">
- <ng-container *ngIf="browseLoading">
- <eg-progress-inline></eg-progress-inline>
- </ng-container>
- <ng-container *ngIf="!browseLoading">
- <button class="btn btn-sm btn-outline-dark"
- (click)="browseNav(true)" i18n>Previous Heading</button>
- <button class="btn btn-sm btn-outline-dark ml-2"
- (click)="browseNav()" i18n>Next Heading</button>
- </ng-container>
+ <div class="col-lg-8">
+ <div class="float-right">
+ <eg-catalog-result-pagination></eg-catalog-result-pagination>
</div>
- <div class="col-lg-5">
- <div class="float-right">
- <eg-catalog-result-pagination></eg-catalog-result-pagination>
- </div>
- </div>
- </ng-container>
- <ng-container *ngIf="!searchContext.browseSearch.value">
- <div class="col-lg-8">
- <div class="float-right">
- <eg-catalog-result-pagination></eg-catalog-result-pagination>
- </div>
- </div>
- </ng-container>
+ </div>
</div>
<div>
<div class="row mt-2">
searchSub: Subscription;
routeSub: Subscription;
basketSub: Subscription;
- browseResults: any[] = [];
- browseLoading = false;
constructor(
private route: ActivatedRoute,
ngOnInit() {
this.searchContext = this.staffCat.searchContext;
+ this.staffCat.browsePagerResults = [];
// Our search context is initialized on page load. Once
// ResultsComponent is active, it will not be reinitialized,
// Watch for basket changes applied by other components.
this.basketSub = this.basket.onChange.subscribe(
() => this.applyRecordSelection());
-
-
- // Load browse data to support browse paging.
- /*
- if (this.searchContext.termSearch.hasBrowseEntry &&
- this.searchContext.browseSearch.value) {
- this.fetchBrowseData().toPromise();
- }
- */
}
ngOnDestroy() {
this.basket.removeRecordIds(ids);
}
}
-
- // Grab a page of browse results
- fetchBrowseData(prev?: boolean): Promise<any> {
- const ctx = this.searchContext.clone();
- ctx.termSearch.hasBrowseEntry = null; // avoid term search
-
- // Grab a few extras so we can last a bit longer before
- // having to fetch more browse data. Could make this bigger.
- ctx.pager.limit = 20;
-
- const results = [];
-
- return this.cat.browse(ctx)
- .pipe(tap(result => results.push(result)))
- .toPromise().then(_ => {
- if (prev) {
- this.browseResults = results.concat(this.browseResults);
- } else {
- this.browseResults = this.browseResults.concat(results);
- }
- });
- }
-
- // Find the browse entry for the next/prev page and navigate there
- // if possible. Returns false if not enough data is available.
- goToBrowsePage(prev?: boolean): boolean {
- const ctx = this.searchContext;
-
- // See if we have enough info to direct to the selected page.
-
- const mbeId = Number( // for this page
- this.searchContext.termSearch.hasBrowseEntry.split(',')[0]);
-
- let target, previous;
-
- this.browseResults.forEach(result => {
- // ignore pivot and authority-only entries
- if (!result.sources) { return; }
-
- if (previous) {
- if (prev) {
- if (result.browse_entry === mbeId) {
- target = previous;
- }
- } else {
- if (previous.browse_entry === mbeId) {
- target = result;
- }
- }
- }
- previous = result;
- });
-
- if (!target) { return false; }
-
- // Jump to the selected browse entry's page.
- ctx.termSearch.hasBrowseEntry = target.browse_entry + ',' + target.fields;
- ctx.pager.offset = 0; // this is a brand new search
- this.staffCat.search();
-
- return true;
- }
-
-
- // Navigate to next or previous browse entry page.
- // take1: navigate without fetching data if possible
- // take2: fetch data and navigate if possible.
- // take3: data fetched in take2 contains data for this page, but
- // not the requested page. This happens when the user navigates
- // cold to a browse page boundary and the current page of data
- // does not yet exist.
- browseNav(prev?: boolean, take?: number) {
- if (!take) { take = 1; }
-
- this.browseLoading = true;
-
- if (take > 3) {
- // We tried our best, but could not find the requested
- // browse data Could happen if we reach the end of the data set.
- this.browseLoading = false;
- return;
- }
-
- if (this.goToBrowsePage(prev)) {
- // Navigation successful
- this.browseLoading = false;
- return;
- }
-
- // We need another page of browse data before we can direct
- // the user to the requested page.
-
- const results = this.browseResults;
- if (results.length > 0) {
- this.searchContext.browseSearch.pivot = prev ?
- results[0].pivot_point :
- results[results.length - 1].pivot_point;
- }
-
- this.fetchBrowseData(prev).then(_ => this.browseNav(prev, take + 1));
- }
}