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 {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 {NgbTabset, NgbTabChangeEvent} 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(/catalog\/record\//),
21 new RegExp(/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 // Display the full form if true, otherwise display the expandy.
42 private renderer: Renderer2,
43 private router: Router,
44 private route: ActivatedRoute,
45 private org: OrgService,
46 private cat: CatalogService,
47 private staffCat: StaffCatalogService
49 this.copyLocations = [];
51 // Some search scenarios, like rendering a search template,
52 // will not be searchable and thus not resovle to a specific
53 // search tab. Check to see if a specific tab is requested
55 this.route.queryParams.subscribe(params => {
56 if (params.searchTab) {
57 this.searchTab = params.searchTab;
61 this.router.events.subscribe(routeEvent => {
62 if (routeEvent instanceof NavigationEnd) {
63 this.showThyself = true;
64 COLLAPSE_ON_PAGES.forEach(pageRegex => {
65 if (routeEvent.url.match(pageRegex)) {
66 this.showThyself = false;
74 this.ccvmMap = this.cat.ccvmMap;
75 this.cmfMap = this.cat.cmfMap;
76 this.context = this.staffCat.searchContext;
78 // Start with advanced search options open
79 // if any filters are active.
80 this.showSearchFilters = this.filtersActive();
84 // Query inputs are generated from search context data,
85 // so they are not available until after the first render.
86 // Search context data is extracted synchronously from the URL.
88 // Avoid changing the tab in the lifecycle hook thread.
91 if (this.context.identSearch.queryType === '') {
92 this.context.identSearch.queryType = 'identifier|isbn';
95 // Apply a tab if none was already specified
96 if (!this.searchTab) {
97 // Assumes that only one type of search will be searchable
99 if (this.context.marcSearch.isSearchable()) {
100 this.searchTab = 'marc';
101 } else if (this.context.identSearch.isSearchable()) {
102 this.searchTab = 'ident';
103 } else if (this.context.browseSearch.isSearchable()) {
104 this.searchTab = 'browse';
105 } else if (this.context.termSearch.isSearchable()) {
106 this.searchTab = 'term';
111 LEGACY_TAB_NAME_MAP[this.staffCat.defaultTab]
112 || this.staffCat.defaultTab || 'term';
116 if (this.searchTab === 'term') {
117 this.refreshCopyLocations();
121 this.focusTabInput();
125 onTabChange(evt: NgbTabChangeEvent) {
126 this.searchTab = evt.nextId;
128 // Focus after tab-change event has a chance to complete
129 // or the tab body and its input won't exist yet and no
130 // elements will be focus-able.
131 setTimeout(() => this.focusTabInput());
135 // Select a DOM node to focus when the tab changes.
136 let selector: string;
137 switch (this.searchTab) {
139 selector = '#ident-query-input';
142 selector = '#first-marc-tag';
145 selector = '#browse-term-input';
148 selector = '#cnbrowse-term-input';
151 this.refreshCopyLocations();
152 selector = '#first-query-input';
156 // TODO: sometime the selector is not available in the DOM
157 // until even later (even with setTimeouts). Need to fix this.
158 // Note the error is thrown from selectRootElement(), not the
159 // call to .focus() on a null reference.
160 this.renderer.selectRootElement(selector).focus();
165 * Display the advanced/extended search options when asked to
166 * or if any advanced options are selected.
168 showFilters(): boolean {
169 // Note that filters may become active due to external
170 // actions on the search context. Always show the filters
171 // if filter values are applied.
172 return this.showSearchFilters || this.filtersActive();
176 this.showSearchFilters = !this.showSearchFilters;
177 this.refreshCopyLocations();
180 filtersActive(): boolean {
182 if (this.context.termSearch.copyLocations[0] !== '') { return true; }
184 // ccvm filters may be present without any filters applied.
185 // e.g. if filters were applied then removed.
187 Object.keys(this.context.termSearch.ccvmFilters).forEach(ccvm => {
188 if (this.context.termSearch.ccvmFilters[ccvm][0] !== '') {
196 orgOnChange = (org: IdlObject): void => {
197 this.context.searchOrg = org;
198 this.refreshCopyLocations();
201 refreshCopyLocations() {
202 if (!this.showFilters()) { return; }
204 // TODO: is this how we avoid displaying too many locations?
205 const org = this.context.searchOrg;
206 if (org.id() === this.org.root().id()) {
207 this.copyLocations = [];
211 this.cat.fetchCopyLocations(org).then(() =>
212 this.copyLocations = this.cat.copyLocations
216 orgName(orgId: number): string {
217 return this.org.get(orgId).shortname();
220 addSearchRow(index: number): void {
221 this.context.termSearch.query.splice(index, 0, '');
222 this.context.termSearch.fieldClass.splice(index, 0, 'keyword');
223 this.context.termSearch.joinOp.splice(index, 0, '&&');
224 this.context.termSearch.matchOp.splice(index, 0, 'contains');
227 delSearchRow(index: number): void {
228 this.context.termSearch.query.splice(index, 1);
229 this.context.termSearch.fieldClass.splice(index, 1);
230 this.context.termSearch.joinOp.splice(index, 1);
231 this.context.termSearch.matchOp.splice(index, 1);
234 addMarcSearchRow(index: number): void {
235 this.context.marcSearch.tags.splice(index, 0, '');
236 this.context.marcSearch.subfields.splice(index, 0, '');
237 this.context.marcSearch.values.splice(index, 0, '');
240 delMarcSearchRow(index: number): void {
241 this.context.marcSearch.tags.splice(index, 1);
242 this.context.marcSearch.subfields.splice(index, 1);
243 this.context.marcSearch.values.splice(index, 1);
246 searchByForm(): void {
247 this.context.pager.offset = 0; // New search
249 // Form search overrides basket display
250 this.context.showBasket = false;
252 this.context.scrub(this.searchTab);
254 switch (this.searchTab) {
259 this.staffCat.search();
263 this.staffCat.browse();
267 this.staffCat.cnBrowse();
272 // https://stackoverflow.com/questions/42322968/angular2-dynamic-input-field-lose-focus-when-input-changes
273 trackByIdx(index: any, item: any) {
277 searchIsActive(): boolean {
278 return this.context.searchState === CatalogSearchState.SEARCHING;
281 // It's possible to chose invalid combos depending on the order of selection
282 preventBogusCombos(idx: number) {
283 if (this.context.termSearch.fieldClass[idx] === 'keyword') {
284 const op = this.context.termSearch.matchOp[idx];
285 if (op === 'exact' || op === 'starts') {
286 this.context.termSearch.matchOp[idx] = 'contains';
291 showBookplate(): boolean {
292 return this.staffCat.enableBookplates;
294 showExcludeElectronic(): boolean {
295 return this.staffCat.showExcludeElectronic;