Fix CSS for targets area.
[mkws-moved-to-github.git] / tools / htdocs / mkws.js
1 /*! MKWS, the MasterKey Widget Set.
2  *  Copyright (C) 2013-2014 Index Data
3  *  See the file LICENSE for details
4  */
5
6 "use strict"; // HTML5: disable for debug_level >= 2
7
8
9 // Handlebars helpers
10 Handlebars.registerHelper('json', function(obj) {
11     return $.toJSON(obj);
12 });
13
14
15 Handlebars.registerHelper('translate', function(s) {
16     return mkws.M(s);
17 });
18
19
20 // We need {{attr '@name'}} because Handlebars can't parse {{@name}}
21 Handlebars.registerHelper('attr', function(attrName) {
22     return this[attrName];
23 });
24
25
26 /*
27  * Use as follows: {{#if-any NAME1 having="NAME2"}}
28  * Applicable when NAME1 is the name of an array
29  * The guarded code runs only if at least one element of the NAME1
30  * array has a subelement called NAME2.
31  */
32 Handlebars.registerHelper('if-any', function(items, options) {
33     var having = options.hash.having;
34     for (var i in items) {
35         var item = items[i]
36         if (!having || item[having]) {
37             return options.fn(this);
38         }
39     }
40     return "";
41 });
42
43
44 Handlebars.registerHelper('first', function(items, options) {
45     var having = options.hash.having;
46     for (var i in items) {
47         var item = items[i]
48         if (!having || item[having]) {
49             return options.fn(item);
50         }
51     }
52     return "";
53 });
54
55
56 Handlebars.registerHelper('commaList', function(items, options) {
57     var out = "";
58
59     for (var i in items) {
60         if (i > 0) out += ", ";
61         out += options.fn(items[i])
62     }
63
64     return out;
65 });
66
67
68 Handlebars.registerHelper('index1', function(obj) {
69     return obj.data.index + 1;
70 });
71
72
73
74 // Set up global mkws object. Contains truly global state such as SP
75 // authentication, and a hash of team objects, indexed by team-name.
76 //
77 var mkws = {
78     authenticated: false,
79     debug_level: 1, // Will be overridden from mkws_config, but
80                     // initial value allows jQuery popup to use logging.
81     paz: undefined, // will be set up during initialisation
82     teams: {},
83     locale_lang: {
84         "de": {
85             "Authors": "Autoren",
86             "Subjects": "Schlagwörter",
87             "Sources": "Daten und Quellen",
88             "source": "datenquelle",
89             "Termlists": "Termlisten",
90             "Next": "Weiter",
91             "Prev": "Zurück",
92             "Search": "Suche",
93             "Sort by": "Sortieren nach",
94             "and show": "und zeige",
95             "per page": "pro Seite",
96             "Displaying": "Zeige",
97             "to": "von",
98             "of": "aus",
99             "found": "gefunden",
100             "Title": "Titel",
101             "Author": "Autor",
102             "author": "autor",
103             "Date": "Datum",
104             "Subject": "Schlagwort",
105             "subject": "schlagwort",
106             "Location": "Ort",
107             "Records": "Datensätze",
108             "Targets": "Datenbanken",
109
110             "dummy": "dummy"
111         },
112
113         "da": {
114             "Authors": "Forfattere",
115             "Subjects": "Emner",
116             "Sources": "Kilder",
117             "source": "kilder",
118             "Termlists": "Termlists",
119             "Next": "Næste",
120             "Prev": "Forrige",
121             "Search": "Søg",
122             "Sort by": "Sorter efter",
123             "and show": "og vis",
124             "per page": "per side",
125             "Displaying": "Viser",
126             "to": "til",
127             "of": "ud af",
128             "found": "fandt",
129             "Title": "Title",
130             "Author": "Forfatter",
131             "author": "forfatter",
132             "Date": "Dato",
133             "Subject": "Emneord",
134             "subject": "emneord",
135             "Location": "Lokation",
136             "Records": "Poster",
137             "Targets": "Baser",
138
139             "dummy": "dummy"
140         }
141     }
142 };
143
144
145 // The following PubSub code is modified from the jQuery manual:
146 // https://api.jquery.com/jQuery.Callbacks/
147 //
148 // Use as:
149 //      mkws.queue("eventName").subscribe(function(param1, param2 ...) { ... });
150 //      mkws.queue("eventName").publish(arg1, arg2, ...);
151
152 (function() {
153   var queues = {};
154   mkws.queue = function(id) {
155     if (!queues[id]) {
156       var callbacks = $.Callbacks();
157       queues[id] = {
158         publish: callbacks.fire,
159         subscribe: callbacks.add,
160         unsubscribe: callbacks.remove
161       };
162     }
163     return queues[id];
164   }
165 }());
166
167
168 // Define empty mkws_config for simple applications that don't define it.
169 if (mkws_config == null || typeof mkws_config != 'object') {
170     var mkws_config = {};
171 }
172
173
174 // Factory function for widget objects.
175 function widget($, team, type, node) {
176     var that = {
177         team: team,
178         type: type,
179         node: node
180     };
181
182     var M = mkws.M;
183
184     if (type === 'Targets') {
185         promoteTargets();
186     }
187
188     // ### More to do here, surely: e.g. wiring into the team
189     mkws.debug("made widget(team=" + team + ", type=" + type + ", node=" + node);
190
191     function promoteTargets() {
192         mkws.debug("promoting widget to type Targets");
193         mkws.queue("targets").subscribe(function(data) {
194             mkws.debug("notified that there are targets");
195
196             if (node.length === 0) alert("huh?!");
197
198             var table ='<table><thead><tr>' +
199                 '<td>' + M('Target ID') + '</td>' +
200                 '<td>' + M('Hits') + '</td>' +
201                 '<td>' + M('Diags') + '</td>' +
202                 '<td>' + M('Records') + '</td>' +
203                 '<td>' + M('State') + '</td>' +
204                 '</tr></thead><tbody>';
205
206             for (var i = 0; i < data.length; i++) {
207                 table += "<tr><td>" + data[i].id +
208                     "</td><td>" + data[i].hits +
209                     "</td><td>" + data[i].diagnostic +
210                     "</td><td>" + data[i].records +
211                     "</td><td>" + data[i].state + "</td></tr>";
212             }
213             
214             table += '</tbody></table>';
215             var subnode = $(node).children('.mkwsBytarget');
216             subnode.html(table);
217         });
218     }
219
220     return that;
221 }
222
223
224 // Factory function for team objects. As much as possible, this uses
225 // only member variables (prefixed "m_") and inner functions with
226 // private scope. Some functions are visibl as member-functions to be
227 // called from outside code -- specifically, from generated
228 // HTML. These functions are that.switchView(), showDetails(),
229 // limitTarget(), limitQuery(), delimitTarget(), delimitQuery(),
230 // pagerPrev(), pagerNext(), showPage().
231 //
232 function team($, teamName) {
233     var that = {};
234     var m_teamName = teamName;
235     var m_submitted = false;
236     var m_query; // initially undefined
237     var m_sort; // will be set below
238     var m_perpage; // will be set below
239     var m_filters = [];
240     var m_totalRec = 0;
241     var m_curPage = 1;
242     var m_curDetRecId = '';
243     var m_curDetRecData = null;
244     var m_debug_time = {
245         // Timestamps for logging
246         "start": $.now(),
247         "last": $.now()
248     };
249     var m_paz; // will be initialised below
250     var m_template = {};
251
252
253     var debug = function (s) {
254         var now = $.now();
255         var timestamp = ((now - m_debug_time.start)/1000).toFixed(3) + " (+" + ((now - m_debug_time.last)/1000).toFixed(3) + ") "
256         m_debug_time.last = now;
257
258         mkws.debug(m_teamName + ": " + timestamp + s);
259     }
260
261     debug("start running MKWS");
262
263     m_sort = mkws_config.sort_default;
264     m_perpage = mkws_config.perpage_default;
265
266     debug("Create main pz2 object");
267     // create a parameters array and pass it to the pz2's constructor
268     // then register the form submit event with the pz2.search function
269     // autoInit is set to true on default
270     m_paz = new pz2({ "windowid": teamName,
271                       "pazpar2path": mkws_config.pazpar2_url,
272                       "usesessions" : mkws_config.use_service_proxy ? false : true,
273                       "oninit": onInit,
274                       "onbytarget": onBytarget,
275                       "onstat": onStat,
276                       "onterm": (mkws_config.facets.length ? onTerm : undefined),
277                       "onshow": onShow,
278                       "onrecord": onRecord,
279                       "showtime": 500,            //each timer (show, stat, term, bytarget) can be specified this way
280                       "termlist": mkws_config.facets.join(',')
281                     });
282
283
284     //
285     // pz2.js event handlers:
286     //
287     function onInit(teamName) {
288         debug("init");
289         m_paz.stat();
290         m_paz.bytarget();
291     }
292
293
294     function onBytarget(data, teamName) {
295         debug("target");
296         mkws.queue("targets").publish(data);
297     }
298
299
300     function onStat(data, teamName) {
301         debug("stat");
302         var node = findnode('.mkwsStat');
303         if (node.length === 0) return;
304
305         node.html('<span class="head">' + M('Status info') + '</span>' +
306             ' -- ' +
307             '<span class="clients">' + M('Active clients') + ': ' + data.activeclients + '/' + data.clients + '</span>' +
308             ' -- ' +
309             '<span class="records">' + M('Retrieved records') + ': ' + data.records + '/' + data.hits + '</span>');
310     }
311
312
313     function onTerm(data, teamName) {
314         debug("term");
315         var node = findnode(".mkwsTermlists");
316         if (node.length == 0) return;
317
318         // no facets: this should never happen
319         if (!mkws_config.facets || mkws_config.facets.length == 0) {
320             alert("onTerm called even though we have no facets: " + $.toJSON(data));
321             node.hide();
322             return;
323         }
324
325         // display if we first got results
326         node.show();
327
328         var acc = [];
329         acc.push('<div class="title">' + M('Termlists') + '</div>');
330         var facets = mkws_config.facets;
331
332         for(var i = 0; i < facets.length; i++) {
333             if (facets[i] == "xtargets") {
334                 addSingleFacet(acc, "Sources",  data.xtargets, 16, null);
335             } else if (facets[i] == "subject") {
336                 addSingleFacet(acc, "Subjects", data.subject,  10, "subject");
337             } else if (facets[i] == "author") {
338                 addSingleFacet(acc, "Authors",  data.author,   10, "author");
339             } else {
340                 alert("bad facet configuration: '" + facets[i] + "'");
341             }
342         }
343
344         node.html(acc.join(''));
345     }
346
347
348     function onShow(data, teamName) {
349         debug("show");
350         m_totalRec = data.merged;
351
352         var pager = findnode(".mkwsPager");
353         if (pager.length) {
354             pager.html(drawPager(data))
355         }
356
357         var results = findnode(".mkwsRecords");
358         if (!results.length)
359             return;
360
361         var html = [];
362         for (var i = 0; i < data.hits.length; i++) {
363             var hit = data.hits[i];
364             html.push('<div class="record" id="mkwsRecdiv_' + teamName + '_' + hit.recid + '" >',
365                       renderSummary(hit),
366                       '</div>');
367             if (hit.recid == m_curDetRecId) {
368                 if (m_curDetRecData)
369                     html.push(renderDetails(m_curDetRecData));
370             }
371         }
372         results.html(html.join(''));
373     }
374
375
376     function onRecord(data, args, teamName) {
377         debug("record");
378         // FIXME: record is async!!
379         clearTimeout(m_paz.recordTimer);
380         // in case on_show was faster to redraw element
381         var detRecordDiv = document.getElementById('mkwsDet_' + teamName + '_' + data.recid);
382         if (detRecordDiv) return;
383         m_curDetRecData = data;
384         var recordDiv = document.getElementById('mkwsRecdiv_' + teamName + '_' + m_curDetRecData.recid);
385         var html = renderDetails(m_curDetRecData);
386         recordDiv.innerHTML += html;
387     }
388
389
390     function addSingleFacet(acc, caption, data, max, pzIndex) {
391         acc.push('<div class="facet mkwsFacet' + caption + ' mkwsTeam_' + m_teamName + '">');
392         acc.push('<div class="termtitle">' + M(caption) + '</div>');
393         for (var i = 0; i < data.length && i < max; i++) {
394             acc.push('<div class="term">');
395             acc.push('<a href="#" ');
396             var action = '';
397             if (!pzIndex) {
398                 // Special case: target selection
399                 acc.push('target_id='+data[i].id+' ');
400                 if (!targetFiltered(data[i].id)) {
401                     action = 'mkws.limitTarget(\'' + m_teamName + '\', this.getAttribute(\'target_id\'),this.firstChild.nodeValue)';
402                 }
403             } else {
404                 action = 'mkws.limitQuery(\'' + m_teamName + '\', \'' + pzIndex + '\', this.firstChild.nodeValue)';
405             }
406             acc.push('onclick="' + action + ';return false;">' + data[i].name + '</a>'
407                      + ' <span>' + data[i].freq + '</span>');
408             acc.push('</div>');
409         }
410         acc.push('</div>');
411     }
412
413
414     function targetFiltered(id) {
415         for (var i = 0; i < m_filters.length; i++) {
416             if (m_filters[i].id === id ||
417                 m_filters[i].id === 'pz:id=' + id) {
418                 return true;
419             }
420         }
421         return false;
422     }
423
424
425     function drawPager (data)
426     {
427         var s = '<div style="float: right">' + M('Displaying') + ': '
428             + (data.start + 1) + ' ' + M('to') + ' ' + (data.start + data.num) +
429             ' ' + M('of') + ' ' + data.merged + ' (' + M('found') + ': '
430             + data.total + ')</div>';
431
432         //client indexes pages from 1 but pz2 from 0
433         var onsides = 6;
434         var pages = Math.ceil(m_totalRec / m_perpage);
435
436         var firstClkbl = (m_curPage - onsides > 0)
437             ? m_curPage - onsides
438             : 1;
439
440         var lastClkbl = firstClkbl + 2*onsides < pages
441             ? firstClkbl + 2*onsides
442             : pages;
443
444         var prev = '<span class="mkwsPrev">&#60;&#60; ' + M('Prev') + '</span><b> | </b>';
445         if (m_curPage > 1)
446             prev = '<a href="#" class="mkwsPrev" onclick="mkws.pagerPrev(\'' + m_teamName + '\');">'
447             +'&#60;&#60; ' + M('Prev') + '</a><b> | </b>';
448
449         var middle = '';
450         for(var i = firstClkbl; i <= lastClkbl; i++) {
451             var numLabel = i;
452             if(i == m_curPage)
453                 numLabel = '<b>' + i + '</b>';
454
455             middle += '<a href="#" onclick="mkws.showPage(\'' + m_teamName + '\', ' + i + ')"> '
456                 + numLabel + ' </a>';
457         }
458
459         var next = '<b> | </b><span class="mkwsNext">' + M('Next') + ' &#62;&#62;</span>';
460         if (pages - m_curPage > 0)
461             next = '<b> | </b><a href="#" class="mkwsNext" onclick="mkws.pagerNext(\'' + m_teamName + '\')">'
462             + M('Next') + ' &#62;&#62;</a>';
463
464         var predots = '';
465         if (firstClkbl > 1)
466             predots = '...';
467
468         var postdots = '';
469         if (lastClkbl < pages)
470             postdots = '...';
471
472         s += '<div style="float: clear">'
473             + prev + predots + middle + postdots + next + '</div>';
474
475         return s;
476     }
477
478
479     ////////////////////////////////////////////////////////////////////////////////
480     ////////////////////////////////////////////////////////////////////////////////
481
482
483     // when search button pressed
484     function onFormSubmitEventHandler()
485     {
486         var val = findnode('.mkwsQuery').val();
487         newSearch(val);
488         return false;
489     }
490
491
492     function newSearch(query, sort, targets)
493     {
494         debug("newSearch: " + query);
495
496         if (mkws_config.use_service_proxy && !mkws.authenticated) {
497             alert("searching before authentication");
498             return;
499         }
500
501         m_filters = []
502         redrawNavi();
503         resetPage();
504         loadSelect();
505         triggerSearch(query, sort, targets);
506         switchView('records'); // In case it's configured to start off as hidden
507         m_submitted = true;
508     }
509
510
511     function onSelectDdChange()
512     {
513         if (!m_submitted) return false;
514         resetPage();
515         loadSelect();
516         m_paz.show(0, m_perpage, m_sort);
517         return false;
518     }
519
520
521     function redrawNavi ()
522     {
523         var navi = findnode('.mkwsNavi');
524         if (!navi) return;
525
526         var text = "";
527         for (var i in m_filters) {
528             if (text) {
529                 text += " | ";
530             }
531             var filter = m_filters[i];
532             if (filter.id) {
533                 text += M('source') + ': <a class="crossout" href="#" onclick="mkws.delimitTarget(\'' + m_teamName +
534                     "', '" + filter.id + "'" + ');return false;">' + filter.name + '</a>';
535             } else {
536                 text += M(filter.field) + ': <a class="crossout" href="#" onclick="mkws.delimitQuery(\'' + m_teamName +
537                     "', '" + filter.field + "', '" + filter.value + "'" +
538                     ');return false;">' + filter.value + '</a>';
539             }
540         }
541
542         navi.html(text);
543     }
544
545
546     function resetPage()
547     {
548         m_curPage = 1;
549         m_totalRec = 0;
550     }
551
552
553     function loadSelect ()
554     {
555         var node = findnode('.mkwsSort');
556         if (node.length && node.val() != m_sort) {
557             debug("changing m_sort from " + m_sort + " to " + node.val());
558             m_sort = node.val();
559         }
560         node = findnode('.mkwsPerpage');
561         if (node.length && node.val() != m_perpage) {
562             debug("changing m_perpage from " + m_perpage + " to " + node.val());
563             m_perpage = node.val();
564         }
565     }
566
567
568     function triggerSearch (query, sort, targets)
569     {
570         var pp2filter = "";
571         var pp2limit = "";
572
573         // Re-use previous query/sort if new ones are not specified
574         if (query) {
575             m_query = query;
576         }
577         if (sort) {
578             m_sort = sort;
579         }
580         if (targets) {
581             m_filters.push({ id: targets, name: targets });
582         }
583
584         for (var i in m_filters) {
585             var filter = m_filters[i];
586             if (filter.id) {
587                 if (pp2filter)
588                     pp2filter += ",";
589                 if (filter.id.match(/^[a-z:]+[=~]/)) {
590                     debug("filter '" + filter.id + "' already begins with SETTING OP");
591                 } else {
592                     filter.id = 'pz:id=' + filter.id;
593                 }
594                 pp2filter += filter.id;
595             } else {
596                 if (pp2limit)
597                     pp2limit += ",";
598                 pp2limit += filter.field + "=" + filter.value.replace(/[\\|,]/g, '\\$&');
599             }
600         }
601
602         var params = {};
603         if (pp2limit) {
604             params.limit = pp2limit;
605         }
606
607         debug("triggerSearch(" + m_query + "): filters = " + $.toJSON(m_filters) + ", pp2filter = " + pp2filter + ", params = " + $.toJSON(params));
608
609         // We can use: params.torusquery = "udb=NAME"
610         // Note: that won't work when running against raw pazpar2
611         m_paz.search(m_query, m_perpage, m_sort, pp2filter, undefined, params);
612     }
613
614
615     // limit by target functions
616     that.limitTarget  = function (id, name)
617     {
618         debug("limitTarget(id=" + id + ", name=" + name + ")");
619         m_filters.push({ id: id, name: name });
620         redrawNavi();
621         resetPage();
622         loadSelect();
623         triggerSearch();
624         return false;
625     }
626
627
628     // limit the query after clicking the facet
629     that.limitQuery = function (field, value)
630     {
631         debug("limitQuery(field=" + field + ", value=" + value + ")");
632         m_filters.push({ field: field, value: value });
633         redrawNavi();
634         resetPage();
635         loadSelect();
636         triggerSearch();
637         return false;
638     }
639
640
641     that.delimitTarget = function (id)
642     {
643         debug("delimitTarget(id=" + id + ")");
644         var newFilters = [];
645         for (var i in m_filters) {
646             var filter = m_filters[i];
647             if (filter.id) {
648                 debug("delimitTarget() removing filter " + $.toJSON(filter));
649             } else {
650                 debug("delimitTarget() keeping filter " + $.toJSON(filter));
651                 newFilters.push(filter);
652             }
653         }
654         m_filters = newFilters;
655
656         redrawNavi();
657         resetPage();
658         loadSelect();
659         triggerSearch();
660         return false;
661     }
662
663
664     that.delimitQuery = function (field, value)
665     {
666         debug("delimitQuery(field=" + field + ", value=" + value + ")");
667         var newFilters = [];
668         for (var i in m_filters) {
669             var filter = m_filters[i];
670             if (filter.field &&
671                 field == filter.field &&
672                 value == filter.value) {
673                 debug("delimitQuery() removing filter " + $.toJSON(filter));
674             } else {
675                 debug("delimitQuery() keeping filter " + $.toJSON(filter));
676                 newFilters.push(filter);
677             }
678         }
679         m_filters = newFilters;
680
681         redrawNavi();
682         resetPage();
683         loadSelect();
684         triggerSearch();
685         return false;
686     }
687
688
689     that.showPage = function (pageNum)
690     {
691         m_curPage = pageNum;
692         m_paz.showPage(m_curPage - 1);
693     }
694
695
696     // simple paging functions
697     that.pagerNext = function () {
698         if (m_totalRec - m_perpage*m_curPage > 0) {
699             m_paz.showNext();
700             m_curPage++;
701         }
702     }
703
704
705     that.pagerPrev = function () {
706         if (m_paz.showPrev() != false)
707             m_curPage--;
708     }
709
710
711     // switching view between targets and records
712     function switchView(view) {
713         var targets = findnode('.mkwsTargets');
714         var results = findnode('.mkwsResults,.mkwsRecords');
715         var blanket = findnode('.mkwsBlanket');
716         var motd    = findnode('.mkwsMOTD');
717
718         switch(view) {
719         case 'targets':
720             if (targets) targets.css('display', 'block');
721             if (results) results.css('display', 'none');
722             if (blanket) blanket.css('display', 'none');
723             if (motd) motd.css('display', 'none');
724             break;
725         case 'records':
726             if (targets) targets.css('display', 'none');
727             if (results) results.css('display', 'block');
728             if (blanket) blanket.css('display', 'block');
729             if (motd) motd.css('display', 'none');
730             break;
731         case 'none':
732             alert("mkws.switchView(" + m_teamName + ", 'none') shouldn't happen");
733             if (targets) targets.css('display', 'none');
734             if (results) results.css('display', 'none');
735             if (blanket) blanket.css('display', 'none');
736             if (motd) motd.css('display', 'none');
737             break;
738         default:
739             alert("Unknown view '" + view + "'");
740         }
741     }
742
743
744     that.switchView = switchView;
745
746
747     // detailed record drawing
748     that.showDetails = function (prefixRecId) {
749         var recId = prefixRecId.replace('mkwsRec_', '');
750         var oldRecId = m_curDetRecId;
751         m_curDetRecId = recId;
752
753         // remove current detailed view if any
754         var detRecordDiv = document.getElementById('mkwsDet_' + m_teamName + '_' + oldRecId);
755         // lovin DOM!
756         if (detRecordDiv)
757             detRecordDiv.parentNode.removeChild(detRecordDiv);
758
759         // if the same clicked, just hide
760         if (recId == oldRecId) {
761             m_curDetRecId = '';
762             m_curDetRecData = null;
763             return;
764         }
765         // request the record
766         debug("showDetails() requesting record '" + recId + "'");
767         m_paz.record(recId);
768     }
769
770
771     /*
772      * All the HTML stuff to render the search forms and
773      * result pages.
774      */
775     function mkwsHtmlAll() {
776         mkwsSetLang();
777         if (mkws_config.show_lang)
778             mkwsHtmlLang();
779
780         debug("HTML search form");
781         mkws.handleNodeWithTeam(findnode('.mkwsSearch'), function(tname) {
782             this.html('\
783 <form name="mkwsSearchForm" class="mkwsSearchForm mkwsTeam_' + tname + '" action="" >\
784   <input class="mkwsQuery mkwsTeam_' + tname + '" type="text" size="' + mkws_config.query_width + '" />\
785   <input class="mkwsButton mkwsTeam_' + tname + '" type="submit" value="' + M('Search') + '" />\
786 </form>');
787         });
788
789         debug("HTML records");
790         // If the team has a .mkwsResults, populate it in the usual
791         // way. If not, assume that it's a smarter application that
792         // defines its own subcomponents, some or all of the
793         // following:
794         //      .mkwsTermlists
795         //      .mkwsRanking
796         //      .mkwsPager
797         //      .mkwsNavi
798         //      .mkwsRecords
799         if (findnode(".mkwsResults").length) {
800             findnode(".mkwsResults").html('\
801 <table width="100%" border="0" cellpadding="6" cellspacing="0">\
802   <tr>\
803     <td class="mkwsTermlistContainer1 mkwsTeam_' + m_teamName + '" width="250" valign="top">\
804       <div class="mkwsTermlists mkwsTeam_' + m_teamName + '"></div>\
805     </td>\
806     <td class="mkwsMOTDContainer mkwsTeam_' + m_teamName + '" valign="top">\
807       <div class="mkwsRanking mkwsTeam_' + m_teamName + '"></div>\
808       <div class="mkwsPager mkwsTeam_' + m_teamName + '"></div>\
809       <div class="mkwsNavi mkwsTeam_' + m_teamName + '"></div>\
810       <div class="mkwsRecords mkwsTeam_' + m_teamName + '"></div>\
811     </td>\
812   </tr>\
813   <tr>\
814     <td colspan="2">\
815       <div class="mkwsTermlistContainer2 mkwsTeam_' + m_teamName + '"></div>\
816     </td>\
817   </tr>\
818 </table>');
819         }
820
821         var node = findnode(".mkwsRanking");
822         if (node.length) {
823             var ranking_data = '';
824             ranking_data += '<form name="mkwsSelect" class="mkwsSelect mkwsTeam_' + m_teamName + '" action="" >';
825             if (mkws_config.show_sort) {
826                 ranking_data +=  M('Sort by') + ' ' + mkwsHtmlSort() + ' ';
827             }
828             if (mkws_config.show_perpage) {
829                 ranking_data += M('and show') + ' ' + mkwsHtmlPerpage() + ' ' + M('per page') + '.';
830             }
831             ranking_data += '</form>';
832
833             node.html(ranking_data);
834         }
835
836         mkwsHtmlSwitch();
837
838         var node;
839         node = findnode('.mkwsSearchForm');
840         if (node.length)
841             node.submit(onFormSubmitEventHandler);
842         node = findnode('.mkwsSort');
843         if (node.length)
844             node.change(onSelectDdChange);
845         node = findnode('.mkwsPerpage');
846         if (node.length)
847             node.change(onSelectDdChange);
848
849         // on first page, hide the termlist
850         $(document).ready(function() { findnode(".mkwsTermlists").hide(); });
851         var motd = findnode(".mkwsMOTD");
852         var container = findnode(".mkwsMOTDContainer");
853         if (motd.length && container.length) {
854             // Move the MOTD from the provided element down into the container
855             motd.appendTo(container);
856         }
857     }
858
859
860     function mkwsSetLang()  {
861         var lang = parseQuerystring().lang || mkws_config.lang;
862         if (!lang || !mkws.locale_lang[lang]) {
863             mkws_config.lang = ""
864         } else {
865             mkws_config.lang = lang;
866         }
867
868         debug("Locale language: " + (mkws_config.lang ? mkws_config.lang : "none"));
869         return mkws_config.lang;
870     }
871
872
873     /* create locale language menu */
874     function mkwsHtmlLang() {
875         var lang_default = "en";
876         var lang = mkws_config.lang || lang_default;
877         var list = [];
878
879         /* display a list of configured languages, or all */
880         var lang_options = mkws_config.lang_options || [];
881         var toBeIncluded = {};
882         for (var i = 0; i < lang_options.length; i++) {
883             toBeIncluded[lang_options[i]] = true;
884         }
885
886         for (var k in mkws.locale_lang) {
887             if (toBeIncluded[k] || lang_options.length == 0)
888                 list.push(k);
889         }
890
891         // add english link
892         if (lang_options.length == 0 || toBeIncluded[lang_default])
893             list.push(lang_default);
894
895         debug("Language menu for: " + list.join(", "));
896
897         /* the HTML part */
898         var data = "";
899         for(var i = 0; i < list.length; i++) {
900             var l = list[i];
901
902             if (data)
903                 data += ' | ';
904
905             if (lang == l) {
906                 data += ' <span>' + l + '</span> ';
907             } else {
908                 data += ' <a href="?lang=' + l + '">' + l + '</a> '
909             }
910         }
911
912         findnode(".mkwsLang").html(data);
913     }
914
915
916     function mkwsHtmlSort() {
917         debug("HTML sort, m_sort = '" + m_sort + "'");
918         var sort_html = '<select class="mkwsSort mkwsTeam_' + m_teamName + '">';
919
920         for(var i = 0; i < mkws_config.sort_options.length; i++) {
921             var opt = mkws_config.sort_options[i];
922             var key = opt[0];
923             var val = opt.length == 1 ? opt[0] : opt[1];
924
925             sort_html += '<option value="' + key + '"';
926             if (m_sort == key || m_sort == val) {
927                 sort_html += ' selected="selected"';
928             }
929             sort_html += '>' + M(val) + '</option>';
930         }
931         sort_html += '</select>';
932
933         return sort_html;
934     }
935
936
937     function mkwsHtmlPerpage() {
938         debug("HTML perpage, m_perpage = " + m_perpage);
939         var perpage_html = '<select class="mkwsPerpage mkwsTeam_' + m_teamName + '">';
940
941         for(var i = 0; i < mkws_config.perpage_options.length; i++) {
942             var key = mkws_config.perpage_options[i];
943
944             perpage_html += '<option value="' + key + '"';
945             if (key == m_perpage) {
946                 perpage_html += ' selected="selected"';
947             }
948             perpage_html += '>' + key + '</option>';
949         }
950         perpage_html += '</select>';
951
952         return perpage_html;
953     }
954
955
956     function mkwsHtmlSwitch() {
957         debug("HTML switch for team " + m_teamName);
958
959         var node = findnode(".mkwsSwitch");
960         node.append($('<a href="#" onclick="mkws.switchView(\'' + m_teamName + '\', \'records\')">' + M('Records') + '</a>'));
961         node.append($("<span/>", { text: " | " }));
962         node.append($('<a href="#" onclick="mkws.switchView(\'' + m_teamName + '\', \'targets\')">' + M('Targets') + '</a>'));
963
964         debug("HTML targets");
965         var node = findnode(".mkwsTargets");
966         node.html('\
967 <div class="mkwsBytarget mkwsTeam_' + m_teamName + '">\
968   No information available yet.\
969 </div>');
970         node.css("display", "none");
971     }
972
973
974     that.runAutoSearch = function() {
975         // ### should check mkwsTermlist as well, for facet-only teams
976         var node = findnode('.mkwsRecords');
977         var query = node.attr('autosearch');
978         if (!query)
979             return;
980
981         if (query.match(/^!param!/)) {
982             var param = query.replace(/^!param!/, '');
983             query = getParameterByName(param);
984             debug("obtained query '" + query + "' from param '" + param + "'");
985             if (!query) {
986                 alert("This page has a MasterKey widget that needs a query specified by the '" + param + "' parameter");
987             }
988         } else if (query.match(/^!path!/)) {
989             var index = query.replace(/^!path!/, '');
990             var path = window.location.pathname.split('/');
991             query = path[path.length - index];
992             debug("obtained query '" + query + "' from path-component '" + index + "'");
993             if (!query) {
994                 alert("This page has a MasterKey widget that needs a query specified by the path-component " + index);
995             }
996         }
997
998         debug("node=" + node + ", class='" + node.className + "', query=" + query);
999
1000         var sort = node.attr('sort');
1001         var targets = node.attr('targets');
1002         var s = "running auto search: '" + query + "'";
1003         if (sort) s += " sorted by '" + sort + "'";
1004         if (targets) s += " in targets '" + targets + "'";
1005         debug(s);
1006
1007         newSearch(query, sort, targets);
1008     }
1009
1010
1011     // implement $.parseQuerystring() for parsing URL parameters
1012     function parseQuerystring() {
1013         var nvpair = {};
1014         var qs = window.location.search.replace('?', '');
1015         var pairs = qs.split('&');
1016         $.each(pairs, function(i, v){
1017             var pair = v.split('=');
1018             nvpair[pair[0]] = pair[1];
1019         });
1020         return nvpair;
1021     }
1022
1023
1024     // This function is taken from a StackOverflow answer
1025     // http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript/901144#901144
1026     // ### should we unify this and parseQuerystring()?
1027     function getParameterByName(name) {
1028         name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
1029         var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
1030             results = regex.exec(location.search);
1031         return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
1032     }
1033
1034
1035     /* locale */
1036     function M(word) {
1037         var lang = mkws_config.lang;
1038
1039         if (!lang || !mkws.locale_lang[lang])
1040             return word;
1041
1042         return mkws.locale_lang[lang][word] || word;
1043     }
1044     mkws.M = M; // so the Handlebars helper can use it
1045
1046
1047     // Finds the node of the specified class within the current team
1048     // Multiple OR-clauses separated by commas are handled
1049     // More complex cases may not work
1050     //
1051     function findnode(selector, teamName) {
1052         teamName = teamName || m_teamName;
1053
1054         selector = selector.split(',').map(function(s) {
1055             return s + '.mkwsTeam_' + teamName;
1056         }).join(',');
1057
1058         return $(selector);
1059     }
1060
1061
1062     function renderSummary(hit)
1063     {
1064         var template = loadTemplate("Summary");
1065         hit._id = "mkwsRec_" + hit.recid;
1066         hit._onclick = "mkws.showDetails('" + m_teamName + "', this.id);return false;"
1067         return template(hit);
1068     }
1069
1070
1071     function renderDetails(data, marker)
1072     {
1073         var template = loadTemplate("Record");
1074         var details = template(data);
1075         return '<div class="details" id="mkwsDet_' + m_teamName + '_' + data.recid + '">' + details + '</div>';
1076     }
1077
1078
1079     function loadTemplate(name)
1080     {
1081         var template = m_template[name];
1082
1083         if (template === undefined) {
1084             // Fall back to generic template if there is no team-specific one
1085             var node = findnode(".mkwsTemplate_" + name);
1086             if (!node.length) {
1087                 node = findnode(".mkwsTemplate_" + name, "ALL");
1088             }
1089
1090             var source = node.html();
1091             if (!source) {
1092                 source = defaultTemplate(name);
1093             }
1094
1095             template = Handlebars.compile(source);
1096             debug("compiled template '" + name + "'");
1097             m_template[name] = template;
1098         }
1099
1100         return template;
1101     }
1102
1103
1104     function defaultTemplate(name)
1105     {
1106         if (name === 'Record') {
1107             return '\
1108 <table>\
1109   <tr>\
1110     <th>{{translate "Title"}}</th>\
1111     <td>\
1112       {{md-title}}\
1113       {{#if md-title-remainder}}\
1114         ({{md-title-remainder}})\
1115       {{/if}}\
1116       {{#if md-title-responsibility}}\
1117         <i>{{md-title-responsibility}}</i>\
1118       {{/if}}\
1119     </td>\
1120   </tr>\
1121   {{#if md-date}}\
1122   <tr>\
1123     <th>{{translate "Date"}}</th>\
1124     <td>{{md-date}}</td>\
1125   </tr>\
1126   {{/if}}\
1127   {{#if md-author}}\
1128   <tr>\
1129     <th>{{translate "Author"}}</th>\
1130     <td>{{md-author}}</td>\
1131   </tr>\
1132   {{/if}}\
1133   {{#if md-electronic-url}}\
1134   <tr>\
1135     <th>{{translate "Links"}}</th>\
1136     <td>\
1137       {{#each md-electronic-url}}\
1138         <a href="{{this}}">Link{{index1}}</a>\
1139       {{/each}}\
1140     </td>\
1141   </tr>\
1142   {{/if}}\
1143   {{#if-any location having="md-subject"}}\
1144   <tr>\
1145     <th>{{translate "Subject"}}</th>\
1146     <td>\
1147       {{#first location having="md-subject"}}\
1148         {{#if md-subject}}\
1149           {{#commaList md-subject}}\
1150             {{this}}{{/commaList}}\
1151         {{/if}}\
1152       {{/first}}\
1153     </td>\
1154   </tr>\
1155   {{/if-any}}\
1156   <tr>\
1157     <th>{{translate "Locations"}}</th>\
1158     <td>\
1159       {{#commaList location}}\
1160         {{attr "@name"}}{{/commaList}}\
1161     </td>\
1162   </tr>\
1163 </table>\
1164 ';
1165         } else if (name === "Summary") {
1166             return '\
1167 <a href="#" id="{{_id}}" onclick="{{_onclick}}">\
1168   <b>{{md-title}}</b>\
1169 </a>\
1170 {{#if md-title-remainder}}\
1171   <span>{{md-title-remainder}}</span>\
1172 {{/if}}\
1173 {{#if md-title-responsibility}}\
1174   <span><i>{{md-title-responsibility}}</i></span>\
1175 {{/if}}\
1176 ';
1177         }
1178
1179         var s = "There is no default '" + name +"' template!";
1180         alert(s);
1181         return s;
1182     }
1183
1184
1185     // main
1186     (function() {
1187         try {
1188             mkwsHtmlAll()
1189         }
1190
1191         catch (e) {
1192             mkws_config.error = e.message;
1193             // alert(e.message);
1194         }
1195     })();
1196
1197     // Bizarrely, 'that' is just an empty hash. All its state is in
1198     // the closure variables defined earlier in this function.
1199     return that;
1200 };
1201
1202
1203 // wrapper to call team() after page load
1204 (function (j) {
1205     mkws.debug = function (string) {
1206         if (!mkws.debug_level)
1207             return;
1208
1209         if (typeof console === "undefined" || typeof console.log === "undefined") { /* ARGH!!! old IE */
1210             return;
1211         }
1212
1213         // you need to disable use strict at the top of the file!!!
1214         if (mkws.debug_level >= 3) {
1215             console.log(arguments.callee.caller);
1216         } else if (mkws.debug_level >= 2) {
1217             console.log(">>> called from function " + arguments.callee.caller.name + ' <<<');
1218         }
1219         console.log(string);
1220     }
1221     var debug = mkws.debug;
1222
1223
1224     mkws.handleNodeWithTeam = function(node, callback) {
1225         // First branch for DOM objects; second branch for jQuery objects
1226         var classes = node.className || node.attr('class');
1227         if (!classes) {
1228             // For some reason, if we try to proceed when classes is
1229             // undefined, we don't get an error message, but this
1230             // function and its callers, up several stack level,
1231             // silently return. What a crock.
1232             mkws.debug("handleNodeWithTeam() called on node with no classes");
1233             return;
1234         }
1235         var list = classes.split(/\s+/)
1236         var teamName, type;
1237
1238         for (var i = 0; i < list.length; i++) {
1239             var cname = list[i];
1240             if (cname.match(/^mkwsTeam_/)) {
1241                 teamName = cname.replace(/^mkwsTeam_/, '');
1242             } else if (cname.match(/^mkws/)) {
1243                 type = cname.replace(/^mkws/, '');
1244             }
1245         }
1246         callback.call(node, teamName, type);
1247     }
1248
1249
1250     mkws.resizePage = function () {
1251         var list = ["mkwsSwitch", "mkwsLang"];
1252
1253         var width = mkws_config.responsive_design_width;
1254         var parent = $(".mkwsTermlists").parent();
1255
1256         if ($(window).width() <= width &&
1257             parent.hasClass("mkwsTermlistContainer1")) {
1258             debug("changing from wide to narrow: " + $(window).width());
1259             $(".mkwsTermlistContainer1").hide();
1260             $(".mkwsTermlistContainer2").show();
1261             for (var tname in mkws.teams) {
1262                 $(".mkwsTermlists.mkwsTeam_" + tname).appendTo($(".mkwsTermlistContainer2.mkwsTeam_" + tname));
1263                 for(var i = 0; i < list.length; i++) {
1264                     $("." + list[i] + ".mkwsTeam_" + tname).hide();
1265                 }
1266             }
1267         } else if ($(window).width() > width &&
1268                    parent.hasClass("mkwsTermlistContainer2")) {
1269             debug("changing from narrow to wide: " + $(window).width());
1270             $(".mkwsTermlistContainer1").show();
1271             $(".mkwsTermlistContainer2").hide();
1272             for (var tname in mkws.teams) {
1273                 $(".mkwsTermlists.mkwsTeam_" + tname).appendTo($(".mkwsTermlistContainer1.mkwsTeam_" + tname));
1274                 for(var i = 0; i < list.length; i++) {
1275                     $("." + list[i] + ".mkwsTeam_" + tname).show();
1276                 }
1277             }
1278         }
1279     };
1280
1281
1282     mkws.switchView = function(tname, view) {
1283         mkws.teams[tname].switchView(view);
1284     }
1285
1286     mkws.showDetails = function (tname, prefixRecId) {
1287         mkws.teams[tname].showDetails(prefixRecId);
1288     }
1289
1290     mkws.limitTarget  = function (tname, id, name) {
1291         mkws.teams[tname].limitTarget(id, name);
1292     }
1293
1294     mkws.limitQuery  = function (tname, field, value) {
1295         mkws.teams[tname].limitQuery(field, value);
1296     }
1297
1298     mkws.delimitTarget = function (tname, id) {
1299         mkws.teams[tname].delimitTarget(id);
1300     }
1301
1302     mkws.delimitQuery = function (tname, field, value) {
1303         mkws.teams[tname].delimitQuery(field, value);
1304     }
1305
1306     mkws.showPage = function (tname, pageNum) {
1307         mkws.teams[tname].showPage(pageNum);
1308     }
1309
1310     mkws.pagerPrev = function (tname) {
1311         mkws.teams[tname].pagerPrev();
1312     }
1313
1314     mkws.pagerNext = function (tname) {
1315         mkws.teams[tname].pagerNext();
1316     }
1317
1318
1319     function defaultMkwsConfig() {
1320         /* default mkws config */
1321         var config_default = {
1322             use_service_proxy: true,
1323             pazpar2_url: "//mkws.indexdata.com/service-proxy/",
1324             service_proxy_auth: "//mkws.indexdata.com/service-proxy-auth",
1325             lang: "",
1326             sort_options: [["relevance"], ["title:1", "title"], ["date:0", "newest"], ["date:1", "oldest"]],
1327             perpage_options: [10, 20, 30, 50],
1328             sort_default: "relevance",
1329             perpage_default: 20,
1330             query_width: 50,
1331             show_lang: true,    /* show/hide language menu */
1332             show_sort: true,    /* show/hide sort menu */
1333             show_perpage: true,         /* show/hide perpage menu */
1334             lang_options: [],   /* display languages links for given languages, [] for all */
1335             facets: ["xtargets", "subject", "author"], /* display facets, in this order, [] for none */
1336             responsive_design_width: undefined, /* a page with less pixel width considered as narrow */
1337             debug_level: 1,     /* debug level for development: 0..2 */
1338
1339             dummy: "dummy"
1340         };
1341
1342         /* Set global debug_level flag early so that debug() works */
1343         if (typeof mkws_config.debug_level !== 'undefined') {
1344             mkws.debug_level = mkws_config.debug_level;
1345         } else if (typeof config_default.debug_level !== 'undefined') {
1346             mkws.debug_level = config_default.debug_level;
1347         }
1348
1349         // make sure the mkws_config is a valid hash
1350         if (!$.isPlainObject(mkws_config)) {
1351             debug("ERROR: mkws_config is not an JS object, ignore it....");
1352             mkws_config = {};
1353         }
1354
1355         /* override standard config values by function parameters */
1356         for (var k in config_default) {
1357             if (typeof mkws_config[k] === 'undefined')
1358                 mkws_config[k] = config_default[k];
1359             //debug("Set config: " + k + ' => ' + mkws_config[k]);
1360         }
1361     }
1362
1363
1364     /*
1365      * Run service-proxy authentication in background (after page load).
1366      * The username/password is configured in the apache config file
1367      * for the site.
1368      */
1369     function authenticateSession(auth_url, auth_domain, pp2_url) {
1370         debug("Run service proxy auth URL: " + auth_url);
1371
1372         if (!auth_domain) {
1373             auth_domain = pp2_url.replace(/^(https?:)?\/\/(.*?)\/.*/, '$2');
1374             debug("guessed auth_domain '" + auth_domain + "' from pp2_url '" + pp2_url + "'");
1375         }
1376
1377         var request = new pzHttpRequest(auth_url, function(err) {
1378             alert("HTTP call for authentication failed: " + err)
1379             return;
1380         }, auth_domain);
1381
1382         request.get(null, function(data) {
1383             if (!$.isXMLDoc(data)) {
1384                 alert("service proxy auth response document is not valid XML document, give up!");
1385                 return;
1386             }
1387             var status = $(data).find("status");
1388             if (status.text() != "OK") {
1389                 alert("service proxy auth repsonse status: " + status.text() + ", give up!");
1390                 return;
1391             }
1392
1393             debug("Service proxy auth successfully done");
1394             mkws.authenticated = true;
1395             runAutoSearches();
1396         });
1397     }
1398
1399
1400     function runAutoSearches() {
1401         debug("running auto searches");
1402
1403         for (var teamName in mkws.teams) {
1404             mkws.teams[teamName].runAutoSearch();
1405         }
1406     }
1407
1408
1409     $(document).ready(function() {
1410         debug("on load ready");
1411         defaultMkwsConfig();
1412
1413         if (mkws_config.query_width < 5 || mkws_config.query_width > 150) {
1414             debug("Reset query width: " + mkws_config.query_width);
1415             mkws_config.query_width = 50;
1416         }
1417
1418         for (var key in mkws_config) {
1419             if (mkws_config.hasOwnProperty(key)) {
1420                 if (key.match(/^language_/)) {
1421                     var lang = key.replace(/^language_/, "");
1422                     // Copy custom languages into list
1423                     mkws.locale_lang[lang] = mkws_config[key];
1424                     debug("Added locally configured language '" + lang + "'");
1425                 }
1426             }
1427         }
1428
1429         if (mkws_config.responsive_design_width) {
1430             // Responsive web design - change layout on the fly based on
1431             // current screen width. Required for mobile devices.
1432             $(window).resize(function(e) { mkws.resizePage() });
1433             // initial check after page load
1434             $(document).ready(function() { mkws.resizePage() });
1435         }
1436
1437         // protocol independent link for pazpar2: "//mkws/sp" -> "https://mkws/sp"
1438         if (mkws_config.pazpar2_url.match(/^\/\//)) {
1439             mkws_config.pazpar2_url = document.location.protocol + mkws_config.pazpar2_url;
1440             debug("adjust protocol independent links: " + mkws_config.pazpar2_url);
1441         }
1442
1443         // Backwards compatibility: set new magic class names on any
1444         // elements that have the old magic IDs.
1445         var ids = [ "Switch", "Lang", "Search", "Pager", "Navi",
1446                     "Results", "Records", "Targets", "Ranking",
1447                     "Termlists", "Stat", "MOTD" ];
1448         for (var i = 0; i < ids.length; i++) {
1449             var id = 'mkws' + ids[i];
1450             var node = $('#' + id);
1451             if (node.attr('id')) {
1452                 node.addClass(id);
1453                 debug("added magic class to '" + node.attr('id') + "'");
1454             }
1455         }
1456
1457         // For all MKWS-classed nodes that don't have a team
1458         // specified, set the team to AUTO.
1459         $('[class^="mkws"],[class*=" mkws"]').each(function () {
1460             if (!this.className.match(/mkwsTeam_/)) {
1461                 debug("adding AUTO team to node with class '" + this.className + "'");
1462                 $(this).addClass('mkwsTeam_AUTO');
1463             }
1464         });
1465
1466         // Find all nodes with an class, and determine their team from
1467         // the mkwsTeam_* class. Make all team objects.
1468         var then = $.now();
1469         $('[class^="mkws"],[class*=" mkws"]').each(function () {
1470             mkws.handleNodeWithTeam(this, function(tname, type) {
1471                 if (!mkws.teams[tname]) {
1472                     mkws.teams[tname] = team(j, tname);
1473                     debug("Made MKWS team '" + tname + "'");
1474                 }
1475                 var myTeam = mkws.teams[tname];
1476                 var myWidget = widget(j, myTeam, type, this);
1477             });
1478         });
1479         var now = $.now();
1480         debug("Walking MKWS nodes took " + (now-then) + " ms");
1481
1482         if (mkws_config.use_service_proxy) {
1483             authenticateSession(mkws_config.service_proxy_auth,
1484                                 mkws_config.service_proxy_auth_domain,
1485                                 mkws_config.pazpar2_url);
1486         } else {
1487             // raw pp2
1488             runAutoSearches();
1489         }
1490     });
1491 })(jQuery);