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