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