]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/dojo/openils/AutoSuggestStore.js
LP#1350371 PO name on create w/ dupe detect
[working/Evergreen.git] / Open-ILS / web / js / dojo / openils / AutoSuggestStore.js
1 if (!dojo._hasResource["openils.AutoSuggestStore"]) {
2     dojo._hasResource["openils.AutoSuggestStore"] = true;
3
4     dojo.provide("openils.AutoSuggestStore");
5
6     dojo.require("dojo.cookie");
7     dojo.require("DojoSRF");
8     dojo.require("openils.Util");
9
10     /* Here's an exception class specific to openils.AutoSuggestStore */
11     function AutoSuggestStoreError(message) { this.message = message; }
12     AutoSuggestStoreError.prototype.toString = function() {
13         return "openils.AutoSuggestStore: " + this.message;
14     };
15
16     function TermString(str, field) { this.str = str; this.field = field; }
17     /* It doesn't seem to be possible to subclass builtins like String, but
18      * these are the only methods of String we should actually need */
19     TermString.prototype.toString=function(){return this.str;};
20     TermString.prototype.toLowerCase=function(){return this.str.toLowerCase();};
21     TermString.prototype.substr=function(){return this.str.substr(arguments);};
22
23     var _autosuggest_fields = ["id", "match", "term", "field"];
24
25     dojo.declare(
26         "openils.AutoSuggestStore", null, {
27
28         "_last_fetch": null,        /* used internally */
29
30         /* Everything between here and the constructor can be specified in
31          * the constructor's args object. */
32
33         "type_selector": null,      /* HTMLSelect object w/ options whose values
34                                        are search_classes (required) */
35         "org_unit_getter": null,    /* function that returns int (OU ID) */
36
37         "limit": 10,                /* number of suggestions at once */
38         "highlight_max": null,      /* TS_HEADLINE()'s MaxWords option */
39         "highlight_min": null,      /* TS_HEADLINE()'s MinWords option */
40         "short_word_length": null,  /* TS_HEADLINE()'s ShortWord option */
41         "normalization": null,      /* TS_RANK_CD()'s normalization argument */
42
43         "constructor": function(/* object */ args) {
44             dojo.mixin(this, args); /* XXX very sloppy */
45             this._current_items = {};
46             this._setup_config_metabib_caches();
47         },
48
49         "_setup_config_metabib_cache": function(key, field_list, oncomplete) {
50             var self = this;
51
52             if (this.cm_cache[key]) return;
53
54             /* Try to get cache of cmc's or cmf's from
55              * openils.widget.Searcher */
56             try {
57                 /* openils.widget.Searcher may not even be loaded;
58                  * that's ok; just try. */
59                 this.cm_cache[key] =
60                     openils.widget.Searcher._cache.obj[key];
61                 /* Don't try to set a cookie here; o.w.Searcher has
62                  * tried and failed. */
63             } catch (E) {
64                 void(0);
65             }
66
67             if (this.cm_cache[key]) return oncomplete();
68
69             /* now try talking to fielder ourselves, and cache the result */
70             var pkey = field_list[0];
71             var query = {};
72             query[pkey] = {"!=": null};
73
74             OpenSRF.CachedClientSession("open-ils.fielder").request({
75                 "method": "open-ils.fielder." + key + ".atomic",
76                 "params": [{"query": query, "fields": field_list}],
77                 "async": true,
78                 "cache": true,
79                 "oncomplete": function(r) {
80                     var result_arr = openils.Util.readResponse(r);
81
82                     self.cm_cache[key] = {};
83                     dojo.forEach(
84                         result_arr,
85                         function(o) { self.cm_cache[key][o[pkey]] = o; }
86                     );
87                     oncomplete();
88                 }
89             }).send();
90         },
91
92         "_setup_config_metabib_caches": function() {
93             var self = this;
94
95             this.cm_cache = {};
96
97             var field_lists = {
98                 "cmf": ["id", "field_class", "name", "label"],
99                 "cmc": ["name", "label"]
100             };
101             var class_list = openils.Util.objectProperties(field_lists);
102
103             var is_done = function(k) { return Boolean(self.cm_cache[k]); };
104
105             dojo.forEach(
106                 class_list, function(key) {
107                     self._setup_config_metabib_cache(
108                         key, field_lists[key], function() {
109                             if (dojo.every(class_list, is_done))
110                                 self.cm_cache.is_done = true;
111                         }
112                     );
113                 }
114             );
115         },
116
117         "_prepare_match_for_display": function(match, field) {
118             return (
119                 "<div class='oils_AS_match'><div class='oils_AS_match_term'>" +
120                 match + "</div><div class='oils_AS_match_field'>" +
121                 this.get_field_label(field) + "</div></div>"
122             );
123         },
124
125         "_prepare_autosuggest_url": function(req) {
126             var term = req.query.term;  /* affected by searchAttr on widget */
127             var limit = (!isNaN(req.count) && req.count != Infinity) ?
128                 req.count : this.limit;
129
130             if (!term || term.length < 1 || term == "*") return null;
131             if (term.match(/[^\s*]$/)) term += " ";
132             term = term.replace(/\*$/, "");
133
134             var params = [
135                 "query=" + encodeURIComponent(term),
136                 "search_class=" + this.type_selector.value,
137                 "limit=" + limit
138             ];
139
140             if (typeof this.org_unit_getter == "function")
141                 params.push("org_unit=" + this.org_unit_getter());
142
143             dojo.forEach(
144                 ["highlight_max", "highlight_min",
145                     "short_word_length", "normalization"],
146                 dojo.hitch(this, function(arg) {
147                     if (this[arg] != null)
148                         params.push(arg + "=" + this[arg]);
149                 })
150             );
151
152             return "/opac/extras/autosuggest?" + params.join("&");
153         },
154
155         "get_field_label": function(field_id) {
156             var mfield = this.cm_cache.cmf[field_id];
157             var mclass = this.cm_cache.cmc[mfield.field_class];
158             return mfield.label + " (" + mclass.label + ")";
159         },
160
161         /* *** Begin dojo.data.api.Read methods *** */
162
163         "getValue": function(
164             /* object */ item,
165             /* string */ attribute,
166             /* anything */ defaultValue) {
167             if (!this.isItem(item))
168                 throw new AutoSuggestStoreError("getValue(): bad item " + item);
169             else if (typeof attribute != "string")
170                 throw new AutoSuggestStoreError("getValue(): bad attribute");
171
172             var value = item[attribute];
173             return (typeof value == "undefined") ? defaultValue : value;
174         },
175
176         "getValues": function(/* object */ item, /* string */ attribute) {
177             if (!this.isItem(item) || typeof attribute != "string")
178                 throw new AutoSuggestStoreError("bad arguments");
179
180             var result = this.getValue(item, attribute, []);
181             return dojo.isArray(result) ? result : [result];
182         },
183
184         "getAttributes": function(/* object */ item) {
185             if (!this.isItem(item))
186                 throw new AutoSuggestStoreError("getAttributes(): bad args");
187             else
188                 return _autosuggest_fields;
189         },
190
191         "hasAttribute": function(/* object */ item, /* string */ attribute) {
192             if (!this.isItem(item) || typeof attribute != "string") {
193                 throw new AutoSuggestStoreError("hasAttribute(): bad args");
194             } else {
195                 return (dojo.indexOf(_autosuggest_fields, attribute) >= 0);
196             }
197         },
198
199         "containsValue": function(
200             /* object */ item,
201             /* string */ attribute,
202             /* anything */ value) {
203             if (!this.isItem(item) || typeof attribute != "string")
204                 throw new AutoSuggestStoreError("bad data");
205             else
206                 return (
207                     dojo.indexOf(this.getValues(item, attribute), value) != -1
208                 );
209         },
210
211         "isItem": function(/* anything */ something) {
212             if (typeof something != "object" || something === null)
213                 return false;
214
215             for (var i = 0; i < _autosuggest_fields.length; i++) {
216                 var cur = _autosuggest_fields[i];
217                 if (typeof something[cur] == "undefined")
218                     return false;
219             }
220             return true;
221         },
222
223         "isItemLoaded": function(/* anything */ something) {
224             return this.isItem(something);  /* for this store,
225                                                items are always loaded */
226         },
227
228         "close": function(/* object */ request) { /* no-op */ return; },
229         "getLabel": function(/* object */ item) { return "match"; },
230         "getLabelAttributes": function(/* object */ item) { return ["match"]; },
231
232         "loadItem": function(/* object */ keywordArgs) {
233             if (!this.isItem(keywordArgs.item))
234                 throw new AutoSuggestStoreError("not an item; can't load it");
235
236             keywordArgs.identity = this.getIdentity(item);
237             return this.fetchItemByIdentity(keywordArgs);
238         },
239
240         "fetch": function(/* request-object */ req) {
241             //  Respect the following properties of the *req* object:
242             //
243             //      query    a dojo-style query, which will need modest
244             //                  translation for our server-side service
245             //      count    an int
246             //      onBegin  a callback that takes the number of items
247             //                  that this call to fetch() will return, but
248             //                  we always give it -1 (i.e. unknown)
249             //      onItem   a callback that takes each item as we get it
250             //      onComplete  a callback that takes the list of items
251             //                      after they're all fetched
252             //
253             //  The onError callback is ignored for now (haven't thought
254             //  of anything useful to do with it yet).
255             //
256             //  The Read API also charges this method with adding an abort
257             //  callback to the *req* object for the caller's use, but
258             //  the one we provide does nothing but issue an alert().
259
260             if (!this.cm_cache.is_done) {
261                 if (typeof req.onComplete == "function")
262                     req.onComplete.call(callback_scope, [], req);
263                 return;
264             }
265             this._current_items = {};
266
267             var callback_scope = req.scope || dojo.global;
268             var url = this._prepare_autosuggest_url(req);
269
270             if (!url) {
271                 if (typeof req.onComplete == "function")
272                     req.onComplete.call(callback_scope, [], req);
273                 return;
274             }
275
276             var self = this;
277             var process_fetch = function(obj, when) {
278                 if (when < self._last_fetch) /* Stale response. Discard. */
279                     return;
280
281                 dojo.forEach(
282                     obj.val,
283                     function(item) {
284                         item.id = item.field + "_" + item.term;
285                         item.term = new TermString(item.term, item.field);
286
287                         item.match = self._prepare_match_for_display(
288                             item.match, item.field
289                         );
290                         self._current_items[item.id] = item;
291
292                         if (typeof req.onItem == "function")
293                             req.onItem.call(callback_scope, item, req);
294                     }
295                 );
296
297                 if (typeof req.onComplete == "function") {
298                     req.onComplete.call(
299                         callback_scope,
300                         openils.Util.objectValues(self._current_items),
301                         req
302                     );
303                 }
304             };
305
306             req.abort = function() {
307                 alert("The 'abort' operation is not supported");
308             };
309
310             if (typeof req.onBegin == "function")
311                 req.onBegin.call(callback_scope, -1, req);
312
313             var fetch_time = this._last_fetch = (new Date().getTime());
314
315             dojo.xhrGet({
316                 "url": url,
317                 "handleAs": "json",
318                 "sync": false,
319                 "preventCache": true,
320                 "headers": {"Accept": "application/json"},
321                 "load": function(obj) { process_fetch(obj, fetch_time); }
322             });
323
324             /* as for onError: what to do? */
325
326             return req;
327         },
328
329         /* *** Begin dojo.data.api.Identity methods *** */
330
331         "getIdentity": function(/* object */ item) {
332             if (!this.isItem(item))
333                 throw new AutoSuggestStoreError("not an item");
334
335             return item.id;
336         },
337
338         "getIdentityAttributes": function(/* object */ item) { return ["id"]; },
339
340         "fetchItemByIdentity": function(/* object */ keywordArgs) {
341             if (keywordArgs.identity == undefined)
342                 return null; // Identity API spec unclear whether error callback
343                              // would need to be run, so we won't.
344             var callback_scope = keywordArgs.scope || dojo.global;
345
346             var item;
347             if (item = this._current_items[keywordArgs.identity]) {
348                 if (typeof keywordArgs.onItem == "function")
349                     keywordArgs.onItem.call(callback_scope, item);
350
351                 return item;
352             } else {
353                 if (typeof keywordArgs.onError == "function")
354                     keywordArgs.onError.call(callback_scope, E);
355
356                 return null;
357             }
358         },
359
360         /* *** Classes implementing any Dojo APIs do this to list which
361          *     APIs they're implementing. *** */
362
363         "getFeatures": function() {
364             return {
365                 "dojo.data.api.Read": true,
366                 "dojo.data.api.Identity": true
367             };
368         }
369     });
370 }