LP1885767 Staff catalog exclude electronic option
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / catalog / search-form.component.ts
1 import {Component, OnInit, AfterViewInit, Renderer2} from '@angular/core';
2 import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
3 import {IdlObject} from '@eg/core/idl.service';
4 import {OrgService} from '@eg/core/org.service';
5 import {ServerStoreService} from '@eg/core/server-store.service';
6 import {CatalogService} from '@eg/share/catalog/catalog.service';
7 import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
8 import {StaffCatalogService} from './catalog.service';
9 import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
10
11 // Maps opac-style default tab names to local tab names.
12 const LEGACY_TAB_NAME_MAP = {
13     expert: 'marc',
14     numeric: 'ident',
15     advanced: 'term'
16 };
17
18 @Component({
19   selector: 'eg-catalog-search-form',
20   styleUrls: ['search-form.component.css'],
21   templateUrl: 'search-form.component.html'
22 })
23 export class SearchFormComponent implements OnInit, AfterViewInit {
24
25     context: CatalogSearchContext;
26     ccvmMap: {[ccvm: string]: IdlObject[]} = {};
27     cmfMap: {[cmf: string]: IdlObject} = {};
28     showSearchFilters = false;
29     copyLocations: IdlObject[];
30     searchTab: string;
31
32     // Display the full form if true, otherwise display the expandy.
33     showThyself = true;
34
35     constructor(
36         private renderer: Renderer2,
37         private router: Router,
38         private route: ActivatedRoute,
39         private org: OrgService,
40         private cat: CatalogService,
41         private staffCat: StaffCatalogService
42     ) {
43         this.copyLocations = [];
44
45         // Some search scenarios, like rendering a search template,
46         // will not be searchable and thus not resovle to a specific
47         // search tab.  Check to see if a specific tab is requested
48         // via the URL.
49         this.route.queryParams.subscribe(params => {
50             if (params.searchTab) {
51                 this.searchTab = params.searchTab;
52             }
53         });
54
55         this.router.events.subscribe(routeEvent => {
56             if (routeEvent instanceof NavigationEnd) {
57                 if (routeEvent.url.match(/catalog\/record/)) {
58                     this.showThyself = false;
59                 } else {
60                     this.showThyself = true;
61                 }
62             }
63         });
64     }
65
66     ngOnInit() {
67         this.ccvmMap = this.cat.ccvmMap;
68         this.cmfMap = this.cat.cmfMap;
69         this.context = this.staffCat.searchContext;
70
71         // Start with advanced search options open
72         // if any filters are active.
73         this.showSearchFilters = this.filtersActive();
74     }
75
76     ngAfterViewInit() {
77         // Query inputs are generated from search context data,
78         // so they are not available until after the first render.
79         // Search context data is extracted synchronously from the URL.
80
81         // Avoid changing the tab in the lifecycle hook thread.
82         setTimeout(() => {
83
84             if (this.context.identSearch.queryType === '') {
85                 this.context.identSearch.queryType = 'identifier|isbn';
86             }
87
88             // Apply a tab if none was already specified
89             if (!this.searchTab) {
90                 // Assumes that only one type of search will be searchable
91                 // at any given time.
92                 if (this.context.marcSearch.isSearchable()) {
93                     this.searchTab = 'marc';
94                 } else if (this.context.identSearch.isSearchable()) {
95                     this.searchTab = 'ident';
96                 } else if (this.context.browseSearch.isSearchable()) {
97                     this.searchTab = 'browse';
98                 } else if (this.context.termSearch.isSearchable()) {
99                     this.searchTab = 'term';
100
101                 } else {
102
103                     this.searchTab =
104                         LEGACY_TAB_NAME_MAP[this.staffCat.defaultTab]
105                         || this.staffCat.defaultTab || 'term';
106
107                 }
108
109                 if (this.searchTab === 'term') {
110                     this.refreshCopyLocations();
111                 }
112             }
113
114             this.focusTabInput();
115         });
116     }
117
118     onTabChange(evt: NgbTabChangeEvent) {
119         this.searchTab = evt.nextId;
120
121         // Focus after tab-change event has a chance to complete
122         // or the tab body and its input won't exist yet and no
123         // elements will be focus-able.
124         setTimeout(() => this.focusTabInput());
125     }
126
127     focusTabInput() {
128         // Select a DOM node to focus when the tab changes.
129         let selector: string;
130         switch (this.searchTab) {
131             case 'ident':
132                 selector = '#ident-query-input';
133                 break;
134             case 'marc':
135                 selector = '#first-marc-tag';
136                 break;
137             case 'browse':
138                 selector = '#browse-term-input';
139                 break;
140             case 'cnbrowse':
141                 selector = '#cnbrowse-term-input';
142                 break;
143             default:
144                 this.refreshCopyLocations();
145                 selector = '#first-query-input';
146         }
147
148         try {
149             // TODO: sometime the selector is not available in the DOM
150             // until even later (even with setTimeouts).  Need to fix this.
151             // Note the error is thrown from selectRootElement(), not the
152             // call to .focus() on a null reference.
153             this.renderer.selectRootElement(selector).focus();
154         } catch (E) {}
155     }
156
157     /**
158      * Display the advanced/extended search options when asked to
159      * or if any advanced options are selected.
160      */
161     showFilters(): boolean {
162         // Note that filters may become active due to external
163         // actions on the search context.  Always show the filters
164         // if filter values are applied.
165         return this.showSearchFilters || this.filtersActive();
166     }
167
168     toggleFilters() {
169         this.showSearchFilters = !this.showSearchFilters;
170         this.refreshCopyLocations();
171     }
172
173     filtersActive(): boolean {
174
175         if (this.context.termSearch.copyLocations[0] !== '') { return true; }
176
177         // ccvm filters may be present without any filters applied.
178         // e.g. if filters were applied then removed.
179         let show = false;
180         Object.keys(this.context.termSearch.ccvmFilters).forEach(ccvm => {
181             if (this.context.termSearch.ccvmFilters[ccvm][0] !== '') {
182                 show = true;
183             }
184         });
185
186         return show;
187     }
188
189     orgOnChange = (org: IdlObject): void => {
190         this.context.searchOrg = org;
191         this.refreshCopyLocations();
192     }
193
194     refreshCopyLocations() {
195         if (!this.showFilters()) { return; }
196
197         // TODO: is this how we avoid displaying too many locations?
198         const org = this.context.searchOrg;
199         if (org.id() === this.org.root().id()) {
200             this.copyLocations = [];
201             return;
202         }
203
204         this.cat.fetchCopyLocations(org).then(() =>
205             this.copyLocations = this.cat.copyLocations
206         );
207     }
208
209     orgName(orgId: number): string {
210         return this.org.get(orgId).shortname();
211     }
212
213     addSearchRow(index: number): void {
214         this.context.termSearch.query.splice(index, 0, '');
215         this.context.termSearch.fieldClass.splice(index, 0, 'keyword');
216         this.context.termSearch.joinOp.splice(index, 0, '&&');
217         this.context.termSearch.matchOp.splice(index, 0, 'contains');
218     }
219
220     delSearchRow(index: number): void {
221         this.context.termSearch.query.splice(index, 1);
222         this.context.termSearch.fieldClass.splice(index, 1);
223         this.context.termSearch.joinOp.splice(index, 1);
224         this.context.termSearch.matchOp.splice(index, 1);
225     }
226
227     addMarcSearchRow(index: number): void {
228         this.context.marcSearch.tags.splice(index, 0, '');
229         this.context.marcSearch.subfields.splice(index, 0, '');
230         this.context.marcSearch.values.splice(index, 0, '');
231     }
232
233     delMarcSearchRow(index: number): void {
234         this.context.marcSearch.tags.splice(index, 1);
235         this.context.marcSearch.subfields.splice(index, 1);
236         this.context.marcSearch.values.splice(index, 1);
237     }
238
239     searchByForm(): void {
240         this.context.pager.offset = 0; // New search
241
242         // Form search overrides basket display
243         this.context.showBasket = false;
244
245         this.context.scrub(this.searchTab);
246
247         switch (this.searchTab) {
248
249             case 'term':
250             case 'ident':
251             case 'marc':
252                 this.staffCat.search();
253                 break;
254
255             case 'browse':
256                 this.staffCat.browse();
257                 break;
258
259             case 'cnbrowse':
260                 this.staffCat.cnBrowse();
261                 break;
262         }
263     }
264
265     // https://stackoverflow.com/questions/42322968/angular2-dynamic-input-field-lose-focus-when-input-changes
266     trackByIdx(index: any, item: any) {
267        return index;
268     }
269
270     searchIsActive(): boolean {
271         return this.context.searchState === CatalogSearchState.SEARCHING;
272     }
273
274     // It's possible to chose invalid combos depending on the order of selection
275     preventBogusCombos(idx: number) {
276         if (this.context.termSearch.fieldClass[idx] === 'keyword') {
277             const op = this.context.termSearch.matchOp[idx];
278             if (op === 'exact' || op === 'starts') {
279                 this.context.termSearch.matchOp[idx] = 'contains';
280             }
281         }
282     }
283
284     showBookplate(): boolean {
285         return this.staffCat.enableBookplates;
286     }
287     showExcludeElectronic(): boolean {
288         return this.staffCat.showExcludeElectronic;
289     }
290 }
291
292