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