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';
11 // Maps opac-style default tab names to local tab names.
12 const LEGACY_TAB_NAME_MAP = {
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\//)
25 selector: 'eg-catalog-search-form',
26 styleUrls: ['search-form.component.css'],
27 templateUrl: 'search-form.component.html'
29 export class SearchFormComponent implements OnInit, AfterViewInit {
31 context: CatalogSearchContext;
32 ccvmMap: {[ccvm: string]: IdlObject[]} = {};
33 cmfMap: {[cmf: string]: IdlObject} = {};
34 showSearchFilters = false;
35 copyLocations: IdlObject[];
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;
43 // Show the course search limit checkbox only if opted in to the
45 showCourseFilter = false;
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
57 this.copyLocations = [];
62 this.ccvmMap = this.cat.ccvmMap;
63 this.cmfMap = this.cat.cmfMap;
64 this.context = this.staffCat.searchContext;
66 // Start with advanced search options open
67 // if any filters are active.
68 this.showSearchFilters = this.filtersActive();
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
74 this.route.queryParams.subscribe(params => {
75 if (params.searchTab) {
76 this.searchTab = params.searchTab;
80 this.store.getItem('eg.catalog.search.form.open')
81 .then(value => this.showSearchFormSetting = value);
83 this.store.getItem('eg.staffcat.course_materials_selector')
84 .then(value => this.showCourseFilter = value);
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)) {
99 return this.canBeHidden() && !this.showSearchFormSetting;
102 toggleFormDisplay() {
103 this.showSearchFormSetting = !this.showSearchFormSetting;
104 this.store.setItem('eg.catalog.search.form.open', this.showSearchFormSetting);
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.
112 // Avoid changing the tab in the lifecycle hook thread.
115 if (this.context.identSearch.queryType === '') {
116 this.context.identSearch.queryType = 'identifier|isbn';
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';
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';
141 LEGACY_TAB_NAME_MAP[this.staffCat.defaultTab]
142 || this.staffCat.defaultTab || 'term';
146 if (this.searchTab === 'term') {
147 this.refreshCopyLocations();
151 this.focusTabInput();
155 onNavChange(evt: NgbNavChangeEvent) {
156 this.searchTab = evt.nextId;
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());
165 // Select a DOM node to focus when the tab changes.
166 let selector: string;
167 switch (this.searchTab) {
169 selector = '#ident-query-input';
172 selector = '#first-marc-tag';
175 selector = '#browse-term-input';
178 selector = '#cnbrowse-term-input';
181 this.refreshCopyLocations();
182 selector = '#first-query-input';
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 */ }
195 * Display the advanced/extended search options when asked to
196 * or if any advanced options are selected.
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();
206 this.showSearchFilters = !this.showSearchFilters;
207 this.refreshCopyLocations();
210 filtersActive(): boolean {
212 if (this.context.termSearch.copyLocations[0] !== '') { return true; }
214 // ccvm filters may be present without any filters applied.
215 // e.g. if filters were applied then removed.
217 Object.keys(this.context.termSearch.ccvmFilters).forEach(ccvm => {
218 if (this.context.termSearch.ccvmFilters[ccvm][0] !== '') {
226 orgOnChange = (org: IdlObject): void => {
227 this.context.searchOrg = org;
228 this.refreshCopyLocations();
231 refreshCopyLocations() {
232 if (!this.showFilters()) { return; }
234 this.cat.fetchCopyLocations(this.context.searchOrg).then(() =>
235 this.copyLocations = this.cat.copyLocations
239 orgName(orgId: number): string {
240 return this.org.get(orgId).shortname();
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');
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);
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, '');
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);
269 searchByForm(): void {
270 this.context.pager.offset = 0; // New search
272 // Form search overrides basket display
273 this.context.showBasket = false;
275 this.context.scrub(this.searchTab);
277 switch (this.searchTab) {
282 this.staffCat.search();
286 this.staffCat.browse();
290 this.staffCat.cnBrowse();
295 // https://stackoverflow.com/questions/42322968/angular2-dynamic-input-field-lose-focus-when-input-changes
296 trackByIdx(index: any, item: any) {
300 searchIsActive(): boolean {
301 return this.context.searchState === CatalogSearchState.SEARCHING;
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';
314 showBookplate(): boolean {
315 return this.staffCat.enableBookplates;
317 showExcludeElectronic(): boolean {
318 return this.staffCat.showExcludeElectronic;
320 searchFilters(): string[] {
321 return this.staffCat.searchFilters;
324 reserveComboboxChange(limiterStatus: string): void {
325 switch (limiterStatus) {
327 this.context.termSearch.onReserveFilter = false;
330 this.context.termSearch.onReserveFilter = true;
331 this.context.termSearch.onReserveFilterNegated = false;
334 this.context.termSearch.onReserveFilter = true;
335 this.context.termSearch.onReserveFilterNegated = true;