]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/share/catalog/search-context.ts
e4e64b2d0e5b8453185c2e607f13079688067893
[Evergreen.git] / Open-ILS / src / eg2 / src / app / share / catalog / search-context.ts
1 import {OrgService} from '@eg/core/org.service';
2 import {IdlObject} from '@eg/core/idl.service';
3 import {Pager} from '@eg/share/util/pager';
4 import {Params} from '@angular/router';
5
6 export enum CatalogSearchState {
7     PENDING,
8     SEARCHING,
9     COMPLETE
10 }
11
12 export class FacetFilter {
13     facetClass: string;
14     facetName: string;
15     facetValue: string;
16
17     constructor(cls: string, name: string, value: string) {
18         this.facetClass = cls;
19         this.facetName  = name;
20         this.facetValue = value;
21     }
22
23     equals(filter: FacetFilter): boolean {
24         return (
25             this.facetClass === filter.facetClass &&
26             this.facetName  === filter.facetName &&
27             this.facetValue === filter.facetValue
28         );
29     }
30 }
31
32 // Not an angular service.
33 // It's conceviable there could be multiple contexts.
34 export class CatalogSearchContext {
35
36     // Search options and filters
37     available = false;
38     global = false;
39     sort: string;
40     fieldClass: string[];
41     query: string[];
42     identQuery: string;
43     identQueryType: string; // isbn, issn, etc.
44     joinOp: string[];
45     matchOp: string[];
46     format: string;
47     searchOrg: IdlObject;
48     ccvmFilters: {[ccvmCode: string]: string[]};
49     facetFilters: FacetFilter[];
50     isStaff: boolean;
51
52     // Result from most recent search.
53     result: any = {};
54     searchState: CatalogSearchState = CatalogSearchState.PENDING;
55
56     // List of IDs in page/offset context.
57     resultIds: number[] = [];
58
59     // Utility stuff
60     pager: Pager;
61     org: OrgService;
62
63     constructor() {
64         this.pager = new Pager();
65         this.reset();
66     }
67
68     // List of result IDs for the current page of data.
69     currentResultIds(): number[] {
70         const ids = [];
71         const max = Math.min(
72             this.pager.offset + this.pager.limit,
73             this.pager.resultCount
74         );
75         for (let idx = this.pager.offset; idx < max; idx++) {
76             ids.push(this.resultIds[idx]);
77         }
78         return ids;
79     }
80
81     addResultId(id: number, resultIdx: number ): void {
82         this.resultIds[resultIdx + this.pager.offset] = id;
83     }
84
85     // Return the record at the requested index.
86     resultIdAt(index: number): number {
87         return this.resultIds[index] || null;
88     }
89
90     // Return the index of the requested record
91     indexForResult(id: number): number {
92         for (let i = 0; i < this.resultIds.length; i++) {
93             if (this.resultIds[i] === id) {
94                 return i;
95             }
96         }
97         return null;
98     }
99
100     /**
101      * Return search context to its default state, resetting search
102      * parameters and clearing any cached result data.
103      * This does not reset global filters like limit-to-available
104      * search-global, or search-org.
105      */
106     reset(): void {
107         this.pager.offset = 0;
108         this.format = '';
109         this.sort = '';
110         this.query = [''];
111         this.identQuery = null;
112         this.identQueryType = 'identifier|isbn';
113         this.fieldClass  = ['keyword'];
114         this.matchOp = ['contains'];
115         this.joinOp = [''];
116         this.ccvmFilters = {};
117         this.facetFilters = [];
118         this.result = {};
119         this.resultIds = [];
120         this.searchState = CatalogSearchState.PENDING;
121     }
122
123     isSearchable(): boolean {
124
125         if (this.identQuery && this.identQueryType) {
126             return true;
127         }
128
129         return this.query.length
130             && this.query[0] !== ''
131             && this.searchOrg !== null;
132     }
133
134     compileSearch(): string {
135         let str = '';
136
137         if (this.available) {
138             str += '#available';
139         }
140
141         if (this.sort) {
142             // e.g. title, title.descending
143             const parts = this.sort.split(/\./);
144             if (parts[1]) { str += ' #descending'; }
145             str += ' sort(' + parts[0] + ')';
146         }
147
148         if (this.identQuery && this.identQueryType) {
149             if (str) { str += ' '; }
150             str += this.identQueryType + ':' + this.identQuery;
151
152         } else {
153
154             // -------
155             // Compile boolean sub-query components
156             if (str.length) { str += ' '; }
157             const qcount = this.query.length;
158
159             // if we multiple boolean query components, wrap them in parens.
160             if (qcount > 1) { str += '('; }
161             this.query.forEach((q, idx) => {
162                 str += this.compileBoolQuerySet(idx);
163             });
164             if (qcount > 1) { str += ')'; }
165             // -------
166         }
167
168         if (this.format) {
169             str += ' format(' + this.format + ')';
170         }
171
172         if (this.global) {
173             str += ' depth(' +
174                 this.org.root().ou_type().depth() + ')';
175         }
176
177         str += ' site(' + this.searchOrg.shortname() + ')';
178
179         Object.keys(this.ccvmFilters).forEach(field => {
180             if (this.ccvmFilters[field][0] !== '') {
181                 str += ' ' + field + '(' + this.ccvmFilters[field] + ')';
182             }
183         });
184
185         this.facetFilters.forEach(f => {
186             str += ' ' + f.facetClass + '|'
187                 + f.facetName + '[' + f.facetValue + ']';
188         });
189
190         return str;
191     }
192
193     stripQuotes(query: string): string {
194         return query.replace(/"/g, '');
195     }
196
197     stripAnchors(query: string): string {
198         return query.replace(/[\^\$]/g, '');
199     }
200
201     addQuotes(query: string): string {
202         if (query.match(/ /)) {
203             return '"' + query + '"';
204         }
205         return query;
206     }
207
208     compileBoolQuerySet(idx: number): string {
209         let query = this.query[idx];
210         const joinOp = this.joinOp[idx];
211         const matchOp = this.matchOp[idx];
212         const fieldClass = this.fieldClass[idx];
213
214         let str = '';
215         if (!query) { return str; }
216
217         if (idx > 0) { str += ' ' + joinOp + ' '; }
218
219         str += '(';
220         if (fieldClass) { str += fieldClass + ':'; }
221
222         switch (matchOp) {
223             case 'phrase':
224                 query = this.addQuotes(this.stripQuotes(query));
225                 break;
226             case 'nocontains':
227                 query = '-' + this.addQuotes(this.stripQuotes(query));
228                 break;
229             case 'exact':
230                 query = '^' + this.stripAnchors(query) + '$';
231                 break;
232             case 'starts':
233                 query = this.addQuotes('^' +
234                     this.stripAnchors(this.stripQuotes(query)));
235                 break;
236         }
237
238         return str + query + ')';
239     }
240
241     hasFacet(facet: FacetFilter): boolean {
242         return Boolean(
243             this.facetFilters.filter(f => f.equals(facet))[0]
244         );
245     }
246
247     removeFacet(facet: FacetFilter): void {
248         this.facetFilters = this.facetFilters.filter(f => !f.equals(facet));
249     }
250
251     addFacet(facet: FacetFilter): void {
252         if (!this.hasFacet(facet)) {
253             this.facetFilters.push(facet);
254         }
255     }
256
257     toggleFacet(facet: FacetFilter): void {
258         if (this.hasFacet(facet)) {
259             this.removeFacet(facet);
260         } else {
261             this.facetFilters.push(facet);
262         }
263     }
264 }
265
266