]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts
LP1615805 No inputs after submit in patron search (AngularJS)
[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} 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 {NgbNavChangeEvent} 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 // Automatically collapse the search form on these pages
19 const COLLAPSE_ON_PAGES = [
20     new RegExp(/staff\/catalog\/record\//),
21     new RegExp(/staff\/catalog\/hold\//)
22 ];
23
24 @Component({
25     selector: 'eg-catalog-search-form',
26     styleUrls: ['search-form.component.css'],
27     templateUrl: 'search-form.component.html'
28 })
29 export class SearchFormComponent implements OnInit, AfterViewInit {
30
31     context: CatalogSearchContext;
32     ccvmMap: {[ccvm: string]: IdlObject[]} = {};
33     cmfMap: {[cmf: string]: IdlObject} = {};
34     showSearchFilters = false;
35     copyLocations: IdlObject[];
36     searchTab: string;
37
38     // What does the user want us to do?
39     // On pages where we can be hidded, start out hidden, unless the
40     // user has opted to show us.
41     showSearchFormSetting = false;
42
43     // Show the course search limit checkbox only if opted in to the
44     // course module
45     showCourseFilter = false;
46
47
48     constructor(
49         private renderer: Renderer2,
50         private router: Router,
51         private route: ActivatedRoute,
52         private org: OrgService,
53         private cat: CatalogService,
54         private store: ServerStoreService,
55         private staffCat: StaffCatalogService
56     ) {
57         this.copyLocations = [];
58
59     }
60
61     ngOnInit() {
62         this.ccvmMap = this.cat.ccvmMap;
63         this.cmfMap = this.cat.cmfMap;
64         this.context = this.staffCat.searchContext;
65
66         // Start with advanced search options open
67         // if any filters are active.
68         this.showSearchFilters = this.filtersActive();
69
70         // Some search scenarios, like rendering a search template,
71         // will not be searchable and thus not resovle to a specific
72         // search tab.  Check to see if a specific tab is requested
73         // via the URL.
74         this.route.queryParams.subscribe(params => {
75             if (params.searchTab) {
76                 this.searchTab = params.searchTab;
77             }
78         });
79
80         this.store.getItem('eg.catalog.search.form.open')
81             .then(value => this.showSearchFormSetting = value);
82
83         this.store.getItem('eg.staffcat.course_materials_selector')
84             .then(value => this.showCourseFilter = value);
85     }
86
87     // Are we on a page where the form is allowed to be collapsed.
88     canBeHidden(): boolean {
89         for (let idx = 0; idx < COLLAPSE_ON_PAGES.length; idx++) {
90             const pageRegex = COLLAPSE_ON_PAGES[idx];
91             if (this.router.url.match(pageRegex)) {
92                 return true;
93             }
94         }
95         return false;
96     }
97
98     hideForm(): boolean {
99         return this.canBeHidden() && !this.showSearchFormSetting;
100     }
101
102     toggleFormDisplay() {
103         this.showSearchFormSetting = !this.showSearchFormSetting;
104         this.store.setItem('eg.catalog.search.form.open', this.showSearchFormSetting);
105     }
106
107     ngAfterViewInit() {
108         // Query inputs are generated from search context data,
109         // so they are not available until after the first render.
110         // Search context data is extracted synchronously from the URL.
111
112         // Avoid changing the tab in the lifecycle hook thread.
113         setTimeout(() => {
114
115             if (this.context.identSearch.queryType === '') {
116                 this.context.identSearch.queryType = 'identifier|isbn';
117             }
118
119             // Apply a tab if none was already specified
120             if (!this.searchTab) {
121                 // Assumes that only one type of search will be searchable
122                 // at any given time.
123                 if (this.context.marcSearch.isSearchable()) {
124                     this.searchTab = 'marc';
125                 } else if (this.context.identSearch.isSearchable()) {
126                     this.searchTab = 'ident';
127
128                 // Browse search may remain 'searchable' even though we
129                 // are displaying bibs linked to a browse entry.
130                 // This is so browse search paging can be added to
131                 // the record list page.
132                 } else if (this.context.browseSearch.isSearchable()
133                     && !this.context.termSearch.hasBrowseEntry) {
134                     this.searchTab = 'browse';
135                 } else if (this.context.termSearch.isSearchable()) {
136                     this.searchTab = 'term';
137
138                 } else {
139
140                     this.searchTab =
141                         LEGACY_TAB_NAME_MAP[this.staffCat.defaultTab]
142                         || this.staffCat.defaultTab || 'term';
143
144                 }
145
146                 if (this.searchTab === 'term') {
147                     this.refreshCopyLocations();
148                 }
149             }
150
151             this.focusTabInput();
152         });
153     }
154
155     onNavChange(evt: NgbNavChangeEvent) {
156         this.searchTab = evt.nextId;
157
158         // Focus after tab-change event has a chance to complete
159         // or the tab body and its input won't exist yet and no
160         // elements will be focus-able.
161         setTimeout(() => this.focusTabInput());
162     }
163
164     focusTabInput() {
165         // Select a DOM node to focus when the tab changes.
166         let selector: string;
167         switch (this.searchTab) {
168             case 'ident':
169                 selector = '#ident-query-input';
170                 break;
171             case 'marc':
172                 selector = '#first-marc-tag';
173                 break;
174             case 'browse':
175                 selector = '#browse-term-input';
176                 break;
177             case 'cnbrowse':
178                 selector = '#cnbrowse-term-input';
179                 break;
180             default:
181                 this.refreshCopyLocations();
182                 selector = '#first-query-input';
183         }
184
185         try {
186             // TODO: sometime the selector is not available in the DOM
187             // until even later (even with setTimeouts).  Need to fix this.
188             // Note the error is thrown from selectRootElement(), not the
189             // call to .focus() on a null reference.
190             this.renderer.selectRootElement(selector).focus();
191         } catch (E) { /* empty */ }
192     }
193
194     /**
195      * Display the advanced/extended search options when asked to
196      * or if any advanced options are selected.
197      */
198     showFilters(): boolean {
199         // Note that filters may become active due to external
200         // actions on the search context.  Always show the filters
201         // if filter values are applied.
202         return this.showSearchFilters || this.filtersActive();
203     }
204
205     toggleFilters() {
206         this.showSearchFilters = !this.showSearchFilters;
207         this.refreshCopyLocations();
208     }
209
210     filtersActive(): boolean {
211
212         if (this.context.termSearch.copyLocations[0] !== '') { return true; }
213
214         // ccvm filters may be present without any filters applied.
215         // e.g. if filters were applied then removed.
216         let show = false;
217         Object.keys(this.context.termSearch.ccvmFilters).forEach(ccvm => {
218             if (this.context.termSearch.ccvmFilters[ccvm][0] !== '') {
219                 show = true;
220             }
221         });
222
223         return show;
224     }
225
226     orgOnChange = (org: IdlObject): void => {
227         this.context.searchOrg = org;
228         this.refreshCopyLocations();
229     };
230
231     refreshCopyLocations() {
232         if (!this.showFilters()) { return; }
233
234         this.cat.fetchCopyLocations(this.context.searchOrg).then(() =>
235             this.copyLocations = this.cat.copyLocations
236         );
237     }
238
239     orgName(orgId: number): string {
240         return this.org.get(orgId).shortname();
241     }
242
243     addSearchRow(index: number): void {
244         this.context.termSearch.query.splice(index, 0, '');
245         this.context.termSearch.fieldClass.splice(index, 0, 'keyword');
246         this.context.termSearch.joinOp.splice(index, 0, '&&');
247         this.context.termSearch.matchOp.splice(index, 0, 'contains');
248     }
249
250     delSearchRow(index: number): void {
251         this.context.termSearch.query.splice(index, 1);
252         this.context.termSearch.fieldClass.splice(index, 1);
253         this.context.termSearch.joinOp.splice(index, 1);
254         this.context.termSearch.matchOp.splice(index, 1);
255     }
256
257     addMarcSearchRow(index: number): void {
258         this.context.marcSearch.tags.splice(index, 0, '');
259         this.context.marcSearch.subfields.splice(index, 0, '');
260         this.context.marcSearch.values.splice(index, 0, '');
261     }
262
263     delMarcSearchRow(index: number): void {
264         this.context.marcSearch.tags.splice(index, 1);
265         this.context.marcSearch.subfields.splice(index, 1);
266         this.context.marcSearch.values.splice(index, 1);
267     }
268
269     searchByForm(): void {
270         this.context.pager.offset = 0; // New search
271
272         // Form search overrides basket display
273         this.context.showBasket = false;
274
275         this.context.scrub(this.searchTab);
276
277         switch (this.searchTab) {
278
279             case 'term':
280             case 'ident':
281             case 'marc':
282                 this.staffCat.search();
283                 break;
284
285             case 'browse':
286                 this.staffCat.browse();
287                 break;
288
289             case 'cnbrowse':
290                 this.staffCat.cnBrowse();
291                 break;
292         }
293     }
294
295     // https://stackoverflow.com/questions/42322968/angular2-dynamic-input-field-lose-focus-when-input-changes
296     trackByIdx(index: any, item: any) {
297         return index;
298     }
299
300     searchIsActive(): boolean {
301         return this.context.searchState === CatalogSearchState.SEARCHING;
302     }
303
304     // It's possible to chose invalid combos depending on the order of selection
305     preventBogusCombos(idx: number) {
306         if (this.context.termSearch.fieldClass[idx] === 'keyword') {
307             const op = this.context.termSearch.matchOp[idx];
308             if (op === 'exact' || op === 'starts') {
309                 this.context.termSearch.matchOp[idx] = 'contains';
310             }
311         }
312     }
313
314     showBookplate(): boolean {
315         return this.staffCat.enableBookplates;
316     }
317     showExcludeElectronic(): boolean {
318         return this.staffCat.showExcludeElectronic;
319     }
320     searchFilters(): string[] {
321         return this.staffCat.searchFilters;
322     }
323
324     reserveComboboxChange(limiterStatus: string): void {
325         switch (limiterStatus) {
326             case 'any':
327                 this.context.termSearch.onReserveFilter = false;
328                 break;
329             case 'limit':
330                 this.context.termSearch.onReserveFilter = true;
331                 this.context.termSearch.onReserveFilterNegated = false;
332                 break;
333             case 'negated':
334                 this.context.termSearch.onReserveFilter = true;
335                 this.context.termSearch.onReserveFilterNegated = true;
336         }
337     }
338 }
339
340