Only deserialize state when appropriate
[mkdru-moved-to-drupal.org.git] / mkdru.client.js
1 // Set up namespace and some state.
2 var mkdru = {
3   // Settings to pass to pz2.js
4   useSessions: Drupal.settings.mkdru.use_sessions === '1',
5   // Variables
6   active: false,
7   pz2: null,
8   totalRec: 0,
9   pagerRange: 6,
10   pazpar2Path: Drupal.settings.mkdru.pz2_path,
11   facetContainerSelector: Drupal.settings.mkdru.facetContainerSelector,
12   // Facets
13   facets: {
14     source: {
15       displayName: Drupal.settings.mkdru.source_dname || "Source",
16       orderWeight: Drupal.settings.mkdru.source_weight || 1,
17       pz2Name: 'xtargets',
18       max: Drupal.settings.mkdru.source_max || 10
19     },
20     subject: {
21       displayName: Drupal.settings.mkdru.subject_dname || "Subject",
22       orderWeight: Drupal.settings.mkdru.source_weight || 2,
23       pz2Name: 'subject',
24       max: Drupal.settings.mkdru.subject_max || 10,
25       multiLimit: true,
26       limiter: 'su'
27     },
28     author: {
29       displayName: Drupal.settings.mkdru.author_dname || "Author",
30       orderWeight: Drupal.settings.mkdru.source_weight || 3,
31       pz2Name: 'author',
32       max: Drupal.settings.mkdru.author_max || 10,
33       multiLimit: false,
34       limiter: 'au'
35     }
36   },
37   // State; Keys limit_{facet key} will also be initialised
38   defaultState: {
39     page: 1,
40     perpage: 20,
41     sort: 'relevance',
42     query:'',
43     recid:null
44   },
45   state: {},
46   realm: ''
47 };
48
49
50
51 // Wrapper for jQuery
52 (function ($) {
53
54 // So we can use jQuery BBQ with Drupal 6 and its 1.2.6 jQuery
55 if (!$.isArray) $.isArray = function(obj) {
56   return Object.prototype.toString.call(obj) === "[object Array]";
57 };
58
59 // BBQ has no handy way to remove params without changing the hash.
60 // This takes an object to add and an array of keys to delete.
61 mkdru.hashAddDelMany = function (add, del) {
62   var newHash = $.deparam.fragment();
63   if (typeof(add) === 'object')
64     $.extend(newHash, add);
65   if ($.isArray(del))
66     for (var i=0; i < del.length; i++)
67       if (newHash[del[i]] !== 'undefined')
68         delete newHash[del[i]];
69   return $.param.fragment("#", newHash);
70 }
71
72 // It's sometimes cumbersome that object literals can't take variable keys.
73 mkdru.hashAddDelOne = function (key, value, del) {
74   var toAdd;
75   var toDel;
76   if (key && value) {
77     var toAdd = {};
78     toAdd[key] = value;
79   }
80   if (del) {
81     var toDel = [];
82     toDel.push(del);
83   }
84   return mkdru.hashAddDelMany(toAdd, toDel);
85 }
86
87
88
89 // pz2.js event handlers:
90 mkdru.pz2Init = function () {
91   if (mkdru.state.query) {
92     mkdru.search();
93   }
94   mkdru.pz2.stat();
95   //mkdru.pz2.bytarget();
96 };
97
98 mkdru.pz2Show = function (data) {
99   mkdru.totalRec = data.merged;
100   $('.mkdru-pager').html(mkdru.generatePager());
101   $('.mkdru-counts').html(Drupal.theme('mkdruCounts', data.start + 1,
102                                       data.num, data.merged, data.total));
103   var html = "";
104   for (var i = 0; i < data.hits.length; i++) {
105     html += Drupal.theme('mkdruResult', data.hits[i], 
106       i + 1 + mkdru.state.perpage * (mkdru.state.page - 1),
107       "#" + $.param.fragment($.param.fragment(
108         window.location.href, {recid: data.hits[i].recid})) + "\n"
109     );
110   }
111   $('.mkdru-result-list').html(html);
112   if (mkdru.state.recid) {
113     mkdru.pz2.record(mkdru.state.recid);
114   }
115   else {
116     $('.mkdru-results').show();
117   }
118 };
119
120 mkdru.pz2Status = function (data) {
121   $('.mkdru-status').html(Drupal.theme('mkdruStatus', data.activeclients, data.clients));
122 };
123
124 mkdru.contains = function (hash, key, value) {
125   if (hash[key]) {
126     if (hash[key].length) {
127       for (var i=0; i<hash[key].length; i++) {
128         if (hash[key][i] === value) {
129           return true;
130         }
131       }
132     } else {
133       return hash[key] === value;
134     }
135   }
136   return false;
137 }
138
139 mkdru.pz2Term = function (data) {
140   // map all facets against selected, for simple rendering 
141   var hash = $.deparam.fragment();
142   for (var key in hash) {
143     if (key.indexOf('limit') == 0 && hash[key]) 
144       //always wrap in array
145       hash[key.substr(6)] = hash[key].split(/;+/);
146     delete hash[key];
147   }
148   for (var facet in mkdru.facets) {
149     var terms = data[mkdru.facets[facet].pz2Name];
150     for (var i=0; i<terms.length; i++) {
151       var term = terms[i];
152       var value = facet == "source" ? term.id : term.name;
153       if (mkdru.contains(hash, facet, value)) { //enabled
154         term.toggleLink = mkdru.removeLimit(facet, value);
155         term.selected = true;
156       } else { //disabled
157         term.toggleLink = mkdru.addLimit(facet, value);
158         term.selected = false;
159       }
160     }
161     $('.mkdru-facet-' + facet).html(
162         Drupal.theme('mkdruFacet', terms, facet, mkdru.facets[facet].max, 
163           hash[facet]));
164   }
165 };
166
167 mkdru.pz2ByTarget = function (data) {
168   
169 };
170
171 mkdru.pz2Record = function (data) {
172   clearTimeout(mkdru.pz2.showTimer);
173   $('.mkdru-results').hide();
174   $('.mkdru-detail').html(Drupal.theme('mkdruDetail', data,
175                                        mkdru.hashAddDelOne(null, null, 'recid')));
176   $('.mkdru-detail').show();
177   clearTimeout(mkdru.pz2.recordTimer);
178 };
179
180
181
182 // State and URL handling 
183
184 // populate state from an object and fill in the blanks with defaults
185 mkdru.stateFromObject = function (obj) {
186   mkdru.state = $.extend({}, mkdru.defaultState);
187   for (var key in mkdru.defaultState)
188     if (typeof(obj[key]) != "undefined")
189       mkdru.state[key] = obj[key];
190 };
191
192 // populate state from current window's hash string
193 mkdru.stateFromHash = function () {
194   mkdru.stateFromObject($.deparam.fragment());
195 };
196
197 // set current window's hash string from state
198 mkdru.hashFromState = function () {
199   // only include non-default settings in the URL
200   var alteredState = {};
201   for (var key in mkdru.defaultState) {
202     if (mkdru.state[key] != mkdru.defaultState[key]) {
203       alteredState[key] = mkdru.state[key];
204     }
205   }
206   $.bbq.pushState(alteredState, 2);
207 };
208
209 // update mkdru_form theme's ui to match state
210 mkdru.uiFromState = function () {
211   for (var key in mkdru.state) {
212     switch(key) {
213     case 'query':
214       $('.mkdru-search input:text').attr('value', mkdru.state[key]);
215       break;
216     case 'perpage':
217       $('.mkdru-perpage').attr('value', mkdru.state[key]);
218       break;
219     case 'sort':
220       $('.mkdru-sort').attr('value', mkdru.state[key]);
221       break;
222     }
223   }
224 };
225
226 mkdru.hashChange = function () {
227   // TING hack, switching tabs resets the hash but does not re-load the page
228   // simply ignore new hash and set it to the old state
229   var hash = $.param.fragment();
230   if (hash.indexOf('addon-result') == 0 || hash.indexOf('content-result') == 0
231       || hash.indexOf('ting-result') == 0) {
232     mkdru.hashFromState();
233     return;
234   }
235   // do we need to restart the search?
236   var searchTrigger = false;
237   // shallow copy of state so we can see what changed.
238   var oldState = $.extend({}, mkdru.state);
239   mkdru.stateFromHash();
240   // only have to compare values since all keys are initialised
241   for (key in mkdru.state) {
242     var changed = (mkdru.state[key] != oldState[key]);
243     if (key.substring(0,5) === 'limit' && changed)
244       searchTrigger = true;
245     if (key === 'page' && changed)
246       mkdru.pz2.showPage(mkdru.state.page-1);
247     if (key === 'query' && changed)
248       searchTrigger = true;
249   }
250   if (searchTrigger)
251     mkdru.search();
252   // request for record detail
253   if (mkdru.state.recid && (mkdru.state.recid != oldState.recid)) {
254     mkdru.pz2.record(mkdru.state.recid);
255   }
256   else {
257     $('.mkdru-detail').hide();
258     $('.mkdru-results').show();
259   }
260 };
261
262 // return link to limit facet
263 mkdru.addLimit = function (facet, limit) {
264   var newHash = $.deparam.fragment();
265   delete newHash['page'];
266   if ((typeof(newHash['limit_' + facet]) === 'undefined')
267        || !mkdru.facets[facet].multiLimit) {
268     newHash['limit_' + facet] = limit;
269   }
270   else {
271     newHash['limit_' + facet] += ';' + limit;
272   }
273   return $.param.fragment("#", newHash);
274 };
275
276 // return link to remove limit from facet
277 mkdru.removeLimit = function (facet, limit) {
278   var newHash = $.deparam.fragment();
279   delete newHash['page'];
280   if (!newHash['limit_' + facet].indexOf(';')
281       || !mkdru.facets[facet].multiLimit) {
282     delete newHash['limit_' + facet];
283   }
284   else {
285     var limits = newHash['limit_' + facet].split(';');
286     for (var i = 0; i < limits.length; i++) {
287       if (limits[i] == limit) {
288         limits.splice(i, 1);
289         if (limits.length < 1)
290           delete newHash['limit_' + facet];
291         else
292           newHash['limit_' + facet] = limits.join(';');
293         break;
294       }
295     }
296   }
297   return $.param.fragment("#", newHash);
298 };
299
300
301
302 // form submit handler
303 mkdru.submitQuery = function () {
304   // new query, back to defaults (shallow copy)
305   mkdru.state = $.extend({}, mkdru.defaultState);
306   mkdru.state.query = $('.mkdru-search input:text').attr('value');
307   mkdru.pollDropDowns();
308   mkdru.hashFromState();
309   mkdru.search();
310   mkdru.active = true;
311   return false;
312 };
313
314 // criteria drop-down (perpage, sort) handler
315 mkdru.submitCriteria = function () {
316   mkdru.pollDropDowns();
317   //search is not ON, do nothing
318   if (!mkdru.active) return false;
319   // pages mean different things now
320   mkdru.state.page = 1;
321   mkdru.hashFromState();
322   mkdru.pz2.show(0, mkdru.state.perpage, mkdru.state.sort);
323   return false;
324 }
325
326 mkdru.search = function () {
327   var filter = null;
328   var query = mkdru.state.query;
329
330   // facet limit implementation
331   for (var facet in mkdru.facets) {
332     // facet is limited
333     if (mkdru.state['limit_' + facet]) {
334       if (facet == "source") {
335         filter = 'pz:id=' + mkdru.state.limit_source;
336       }
337       else {
338         var limits = mkdru.state['limit_' + facet].split(/;+/);
339         for (var i = 0; i < limits.length; i++) {
340           // ex. query + and au="{limit_author}"
341           if (limits[i]) 
342             query += ' and ' + mkdru.facets[facet]['limiter'] + '="'
343                   + limits[i] + '"';
344         }
345       }
346     }
347   }
348
349   mkdru.pz2.search(query, mkdru.state.perpage, mkdru.state.sort, filter);
350   mkdru.active = true;
351 };
352
353 mkdru.pollDropDowns = function () {
354   mkdru.state.perpage = $('.mkdru-perpage').attr('value');
355   mkdru.state.sort = $('.mkdru-sort').attr('value');
356 };
357
358 mkdru.generatePager = function () {
359   // cast page parameter to numeric so we can add to it
360   if (typeof mkdru.state.page == "string") {
361     mkdru.state.page = Number(mkdru.state.page);
362   }
363   var total = Math.ceil(mkdru.totalRec / mkdru.state.perpage);
364   var first = (mkdru.state.page - mkdru.pagerRange > 0)
365       ? mkdru.state.page - mkdru.pagerRange : 1;
366   var last = first + 2 * mkdru.pagerRange < total
367       ? first + 2 * mkdru.pagerRange : total;
368   var prev = null;
369   var next = null;
370   var pages = [];
371
372   if ((mkdru.state.page - 1) >= first) {
373     prev = "#" + $.param.fragment($.param.fragment(
374                window.location.href, {page: mkdru.state.page - 1}))
375   }
376   if ((mkdru.state.page + 1) <= total) {
377     next = "#" + $.param.fragment($.param.fragment(
378                window.location.href, {page: mkdru.state.page + 1}))
379   }
380
381   for (var i = first; i <= last; i++) {
382     pages.push("#" + $.param.fragment($.param.fragment(
383                window.location.href, {page: i})));
384   }
385
386   return Drupal.theme('mkdruPager', pages, first, mkdru.state.page,
387                       total, prev, next);
388 };
389
390
391
392 // wait until the DOM is ready, bind events
393 // and instantiate pz2 library
394 $(document).ready(function () {
395   $(window).bind( 'hashchange', mkdru.hashChange);
396   $('.mkdru-search').bind('submit', mkdru.submitQuery);
397   $('.mkdru-search input:text').attr('value', '');
398   $('.mkdru-perpage').bind('change', mkdru.submitCriteria);
399   $('.mkdru-sort').bind('change', mkdru.submitCriteria);
400
401   // generate termlist for pz2.js and populate facet limit state
402   var termlist = [];
403   for (var key in mkdru.facets) {
404     termlist.push(mkdru.facets[key].pz2Name);
405     mkdru.defaultState['limit_' + key] = null;
406   }
407
408   // if signaled, prepare facet container client-side
409   if (mkdru.facetContainerSelector) {
410     $(mkdru.facetContainerSelector).html(Drupal.theme('mkdruFacetContainer',
411       mkdru.facets));
412   }
413
414   mkdru.pz2 = new pz2( { "onshow": mkdru.pz2Show,
415               "showtime": 500, //each timer (show, stat, term, bytarget) can be specified this way
416               "pazpar2path": mkdru.pazpar2Path,
417               "oninit": mkdru.pz2Init,
418               "onstat": mkdru.pz2Status,
419               "onterm": mkdru.pz2Term,
420               "termlist": termlist.join(','),
421               "onbytarget": mkdru.pz2ByTarget,
422               "usesessions" : mkdru.useSessions,
423               "showResponseType": mkdru.showResponseType,
424               "onrecord": mkdru.pz2Record,
425               "autoInit": false } );
426   mkdru.pz2.showFastCount = 1;
427
428   // initialise state to hash string or defaults
429   mkdru.stateFromHash();
430   // and update UI to match
431   mkdru.uiFromState();
432
433   // ting thing
434   if (typeof(Drupal.settings.mkdru.query) !== "undefined") {
435     mkdru.state.query = Drupal.settings.mkdru.query
436     //force the hash to contain query
437     mkdru.hashFromState();
438   }
439
440   //not running against SP? init, otherwise authenticate
441   if (mkdru.useSessions) {
442     mkdru.pz2.init();
443   } else {
444     //runnin against SP
445     var user = Drupal.settings.mkdru.sp_user;
446     var pass = Drupal.settings.mkdru.sp_pass;
447     var params = {};
448     params['command'] = 'auth';
449     if (user && pass) {
450       params['action'] = 'login';
451       params['username'] = user;
452       params['password'] = pass;
453     } else {
454       params['action'] = 'ipauth';
455     }
456     var authReq = new pzHttpRequest(mkdru.pazpar2Path, 
457       function (err) {
458         alert("Authentication against metasearch gateway failed: " +err);
459       }
460     );
461     authReq.get(params,
462       function (data) {
463         var s = data.getElementsByTagName('status');
464         if (s.length && Element_getTextContent(s[0]) == "OK") {
465           mkdru.realm = data.getElementsByTagName('realm');
466           mkdru.pz2Init();
467         } else {
468           alert("Malformed response when authenticating against the metasearch"
469             + " gateway");
470         }
471       }
472     );
473   }
474 });
475 })(jQuery);