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