More explicit logging for widget-creation
[mkws-moved-to-github.git] / tools / htdocs / mkws.js
index f320111..c506ccf 100644 (file)
@@ -142,29 +142,6 @@ var mkws = {
 };
 
 
-// The following PubSub code is modified from the jQuery manual:
-// https://api.jquery.com/jQuery.Callbacks/
-//
-// Use as:
-//     mkws.queue("eventName").subscribe(function(param1, param2 ...) { ... });
-//     mkws.queue("eventName").publish(arg1, arg2, ...);
-
-(function() {
-  var queues = {};
-  mkws.queue = function(id) {
-    if (!queues[id]) {
-      var callbacks = $.Callbacks();
-      queues[id] = {
-       publish: callbacks.fire,
-       subscribe: callbacks.add,
-       unsubscribe: callbacks.remove
-      };
-    }
-    return queues[id];
-  }
-}());
-
-
 // Define empty mkws_config for simple applications that don't define it.
 if (mkws_config == null || typeof mkws_config != 'object') {
     var mkws_config = {};
@@ -183,16 +160,33 @@ function widget($, team, type, node) {
 
     if (type === 'Targets') {
        promoteTargets();
+       team.debug("made targets widget(node=" + node + ")");
+    } else if (type === 'Stat') {
+       promoteStat();
+       team.debug("made stat widget(node=" + node + ")");
+    } else if (type === 'Termlists') {
+       promoteTermlists();
+       team.debug("made termlists widget(node=" + node + ")");
+    } else if (type === 'Pager') {
+       promotePager();
+       team.debug("made pager widget(node=" + node + ")");
+    } else if (type === 'Records') {
+       promoteRecords();
+       team.debug("made records widget(node=" + node + ")");
+    } else {
+       // ### Handle other types here
+       team.debug("made unencapsulated widget(type=" + type + ", node=" + node + ")");
     }
 
-    // ### More to do here, surely: e.g. wiring into the team
-    mkws.debug("made widget(team=" + team + ", type=" + type + ", node=" + node);
+    return that;
 
-    function promoteTargets() {
-       mkws.debug("promoting widget to type Targets");
-       mkws.queue("targets").subscribe(function(data) {
-           mkws.debug("notified that there are targets");
 
+    // Functions follow for promoting the regular widget object into
+    // widgets of specific types. These could be moved outside of the
+    // widget object, or even into their own source files.
+
+    function promoteTargets() {
+       team.queue("targets").subscribe(function(data) {
            if (node.length === 0) alert("huh?!");
 
            var table ='<table><thead><tr>' +
@@ -217,7 +211,169 @@ function widget($, team, type, node) {
        });
     }
 
-    return that;
+
+    function promoteStat() {
+       team.queue("stat").subscribe(function(data) {
+           if (node.length === 0)  alert("huh?!");
+
+           $(node).html('<span class="head">' + M('Status info') + '</span>' +
+               ' -- ' +
+               '<span class="clients">' + M('Active clients') + ': ' + data.activeclients + '/' + data.clients + '</span>' +
+               ' -- ' +
+               '<span class="records">' + M('Retrieved records') + ': ' + data.records + '/' + data.hits + '</span>');
+       });
+    }
+
+
+    function promoteTermlists() {
+       team.queue("termlists").subscribe(function(data) {
+           mkws.debug("in termlist consumer");
+           if (!node) {
+               alert("termlists event when there are no termlists");
+               return;
+           }
+
+           // no facets: this should never happen
+           if (!mkws_config.facets || mkws_config.facets.length == 0) {
+               alert("onTerm called even though we have no facets: " + $.toJSON(data));
+               $(node).hide();
+               return;
+           }
+
+           // display if we first got results
+           $(node).show();
+
+           var acc = [];
+           acc.push('<div class="title">' + M('Termlists') + '</div>');
+           var facets = mkws_config.facets;
+
+           for (var i = 0; i < facets.length; i++) {
+               if (facets[i] == "xtargets") {
+                   addSingleFacet(acc, "Sources",  data.xtargets, 16, null);
+               } else if (facets[i] == "subject") {
+                   addSingleFacet(acc, "Subjects", data.subject,  10, "subject");
+               } else if (facets[i] == "author") {
+                   addSingleFacet(acc, "Authors",  data.author,   10, "author");
+               } else {
+                   alert("bad facet configuration: '" + facets[i] + "'");
+               }
+           }
+
+           $(node).html(acc.join(''));
+
+           function addSingleFacet(acc, caption, data, max, pzIndex) {
+               acc.push('<div class="facet mkwsFacet' + caption + ' mkwsTeam_' + team.name() + '">');
+               acc.push('<div class="termtitle">' + M(caption) + '</div>');
+               for (var i = 0; i < data.length && i < max; i++) {
+                   acc.push('<div class="term">');
+                   acc.push('<a href="#" ');
+                   var action = '';
+                   if (!pzIndex) {
+                       // Special case: target selection
+                       acc.push('target_id='+data[i].id+' ');
+                       if (!team.targetFiltered(data[i].id)) {
+                           action = 'mkws.limitTarget(\'' + team.name() + '\', this.getAttribute(\'target_id\'),this.firstChild.nodeValue)';
+                       }
+                   } else {
+                       action = 'mkws.limitQuery(\'' + team.name() + '\', \'' + pzIndex + '\', this.firstChild.nodeValue)';
+                   }
+                   acc.push('onclick="' + action + ';return false;">' + data[i].name + '</a>'
+                            + ' <span>' + data[i].freq + '</span>');
+                   acc.push('</div>');
+               }
+               acc.push('</div>');
+           }
+       });
+    }
+
+
+    function promotePager() {
+       team.queue("pager").subscribe(function(data) {
+           $(node).html(drawPager(data))
+
+           function drawPager(data) {
+               var s = '<div style="float: right">' + M('Displaying') + ': '
+                   + (data.start + 1) + ' ' + M('to') + ' ' + (data.start + data.num) +
+                   ' ' + M('of') + ' ' + data.merged + ' (' + M('found') + ': '
+                   + data.total + ')</div>';
+
+               //client indexes pages from 1 but pz2 from 0
+               var onsides = 6;
+               var pages = Math.ceil(team.totalRecordCount() / team.perpage());
+               var currentPage = team.currentPage();
+
+               var firstClkbl = (currentPage - onsides > 0)
+                   ? currentPage - onsides
+                   : 1;
+
+               var lastClkbl = firstClkbl + 2*onsides < pages
+                   ? firstClkbl + 2*onsides
+                   : pages;
+
+               var prev = '<span class="mkwsPrev">&#60;&#60; ' + M('Prev') + '</span><b> | </b>';
+               if (currentPage > 1)
+                   prev = '<a href="#" class="mkwsPrev" onclick="mkws.pagerPrev(\'' + team.name() + '\');">'
+                   +'&#60;&#60; ' + M('Prev') + '</a><b> | </b>';
+
+               var middle = '';
+               for(var i = firstClkbl; i <= lastClkbl; i++) {
+                   var numLabel = i;
+                   if(i == currentPage)
+                       numLabel = '<b>' + i + '</b>';
+
+                   middle += '<a href="#" onclick="mkws.showPage(\'' + team.name() + '\', ' + i + ')"> '
+                       + numLabel + ' </a>';
+               }
+
+               var next = '<b> | </b><span class="mkwsNext">' + M('Next') + ' &#62;&#62;</span>';
+               if (pages - currentPage > 0)
+                   next = '<b> | </b><a href="#" class="mkwsNext" onclick="mkws.pagerNext(\'' + team.name() + '\')">'
+                   + M('Next') + ' &#62;&#62;</a>';
+
+               var predots = '';
+               if (firstClkbl > 1)
+                   predots = '...';
+
+               var postdots = '';
+               if (lastClkbl < pages)
+                   postdots = '...';
+
+               s += '<div style="float: clear">'
+                   + prev + predots + middle + postdots + next + '</div>';
+
+               return s;
+           }
+       });
+    }                       
+
+
+    function promoteRecords() {
+       team.queue("records").subscribe(function(data) {
+           var html = [];
+           for (var i = 0; i < data.hits.length; i++) {
+               var hit = data.hits[i];
+               html.push('<div class="record" id="mkwsRecdiv_' + team.name() + '_' + hit.recid + '" >',
+                         renderSummary(hit),
+                         '</div>');
+               // ### At some point, we may be able to move the
+               // m_currentRecordId and m_currentRecordData members
+               // from the team object into this widget.
+               if (hit.recid == team.currentRecordId()) {
+                   if (team.currentRecordData())
+                       html.push(renderDetails(team.currentRecordData()));
+               }
+           }
+           $(node).html(html.join(''));
+
+           function renderSummary(hit)
+           {
+               var template = team.loadTemplate("Summary");
+               hit._id = "mkwsRec_" + hit.recid;
+               hit._onclick = "mkws.showDetails('" + team.name() + "', this.id);return false;"
+               return template(hit);
+           }
+       });
+    }
 }
 
 
@@ -234,14 +390,14 @@ function team($, teamName) {
     var m_teamName = teamName;
     var m_submitted = false;
     var m_query; // initially undefined
-    var m_sort; // will be set below
+    var m_sortOrder; // will be set below
     var m_perpage; // will be set below
     var m_filters = [];
-    var m_totalRec = 0;
-    var m_curPage = 1;
-    var m_curDetRecId = '';
-    var m_curDetRecData = null;
-    var m_debug_time = {
+    var m_totalRecordCount = 0;
+    var m_currentPage = 1;
+    var m_currentRecordId = '';
+    var m_currentRecordData = null;
+    var m_debugTime = {
        // Timestamps for logging
        "start": $.now(),
        "last": $.now()
@@ -249,18 +405,25 @@ function team($, teamName) {
     var m_paz; // will be initialised below
     var m_template = {};
 
+    that.name = function() { return m_teamName; }
+    that.perpage = function() { return m_perpage; }
+    that.totalRecordCount = function() { return m_totalRecordCount; }
+    that.currentPage = function() { return m_currentPage; }
+    that.currentRecordId = function() { return m_currentRecordId; }
+    that.currentRecordData = function() { return m_currentRecordData; }
 
     var debug = function (s) {
        var now = $.now();
-       var timestamp = ((now - m_debug_time.start)/1000).toFixed(3) + " (+" + ((now - m_debug_time.last)/1000).toFixed(3) + ") "
-       m_debug_time.last = now;
+       var timestamp = ((now - m_debugTime.start)/1000).toFixed(3) + " (+" + ((now - m_debugTime.last)/1000).toFixed(3) + ") "
+       m_debugTime.last = now;
 
        mkws.debug(m_teamName + ": " + timestamp + s);
     }
+    that.debug = debug;
 
     debug("start running MKWS");
 
-    m_sort = mkws_config.sort_default;
+    m_sortOrder = mkws_config.sort_default;
     m_perpage = mkws_config.perpage_default;
 
     debug("Create main pz2 object");
@@ -284,92 +447,36 @@ function team($, teamName) {
     //
     // pz2.js event handlers:
     //
-    function onInit(teamName) {
+    function onInit() {
        debug("init");
        m_paz.stat();
        m_paz.bytarget();
     }
 
 
-    function onBytarget(data, teamName) {
+    function onBytarget(data) {
        debug("target");
-       mkws.queue("targets").publish(data);
+       queue("targets").publish(data);
     }
 
 
-    function onStat(data, teamName) {
+    function onStat(data) {
        debug("stat");
-       var node = findnode('.mkwsStat');
-       if (node.length === 0) return;
-
-       node.html('<span class="head">' + M('Status info') + '</span>' +
-           ' -- ' +
-           '<span class="clients">' + M('Active clients') + ': ' + data.activeclients + '/' + data.clients + '</span>' +
-           ' -- ' +
-           '<span class="records">' + M('Retrieved records') + ': ' + data.records + '/' + data.hits + '</span>');
+       queue("stat").publish(data);
     }
 
 
-    function onTerm(data, teamName) {
+    function onTerm(data) {
        debug("term");
-       var node = findnode(".mkwsTermlists");
-       if (node.length == 0) return;
-
-       // no facets: this should never happen
-       if (!mkws_config.facets || mkws_config.facets.length == 0) {
-           alert("onTerm called even though we have no facets: " + $.toJSON(data));
-           node.hide();
-           return;
-       }
-
-       // display if we first got results
-       node.show();
-
-       var acc = [];
-       acc.push('<div class="title">' + M('Termlists') + '</div>');
-       var facets = mkws_config.facets;
-
-       for(var i = 0; i < facets.length; i++) {
-           if (facets[i] == "xtargets") {
-               addSingleFacet(acc, "Sources",  data.xtargets, 16, null);
-           } else if (facets[i] == "subject") {
-               addSingleFacet(acc, "Subjects", data.subject,  10, "subject");
-           } else if (facets[i] == "author") {
-               addSingleFacet(acc, "Authors",  data.author,   10, "author");
-           } else {
-               alert("bad facet configuration: '" + facets[i] + "'");
-           }
-       }
-
-       node.html(acc.join(''));
+       queue("termlists").publish(data);
     }
 
 
     function onShow(data, teamName) {
        debug("show");
-       m_totalRec = data.merged;
-
-       var pager = findnode(".mkwsPager");
-       if (pager.length) {
-           pager.html(drawPager(data))
-       }
-
-       var results = findnode(".mkwsRecords");
-       if (!results.length)
-           return;
-
-       var html = [];
-       for (var i = 0; i < data.hits.length; i++) {
-            var hit = data.hits[i];
-           html.push('<div class="record" id="mkwsRecdiv_' + teamName + '_' + hit.recid + '" >',
-                     renderSummary(hit),
-                     '</div>');
-           if (hit.recid == m_curDetRecId) {
-               if (m_curDetRecData)
-                   html.push(renderDetails(m_curDetRecData));
-           }
-       }
-       results.html(html.join(''));
+       m_totalRecordCount = data.merged;
+       queue("pager").publish(data);
+       queue("records").publish(data);
     }
 
 
@@ -380,38 +487,15 @@ function team($, teamName) {
        // in case on_show was faster to redraw element
        var detRecordDiv = document.getElementById('mkwsDet_' + teamName + '_' + data.recid);
        if (detRecordDiv) return;
-       m_curDetRecData = data;
-       var recordDiv = document.getElementById('mkwsRecdiv_' + teamName + '_' + m_curDetRecData.recid);
-       var html = renderDetails(m_curDetRecData);
-       recordDiv.innerHTML += html;
+       m_currentRecordData = data;
+       // Can't use jQuery's $('#x') syntax to find this ID, because it contains spaces.
+       var recordDiv = document.getElementById('mkwsRecdiv_' + teamName + '_' + m_currentRecordData.recid);
+       var html = renderDetails(m_currentRecordData);
+       $(recordDiv).append(html);
     }
 
 
-    function addSingleFacet(acc, caption, data, max, pzIndex) {
-       acc.push('<div class="facet mkwsFacet' + caption + ' mkwsTeam_' + m_teamName + '">');
-       acc.push('<div class="termtitle">' + M(caption) + '</div>');
-       for (var i = 0; i < data.length && i < max; i++) {
-           acc.push('<div class="term">');
-            acc.push('<a href="#" ');
-           var action = '';
-           if (!pzIndex) {
-               // Special case: target selection
-               acc.push('target_id='+data[i].id+' ');
-               if (!targetFiltered(data[i].id)) {
-                   action = 'mkws.limitTarget(\'' + m_teamName + '\', this.getAttribute(\'target_id\'),this.firstChild.nodeValue)';
-               }
-           } else {
-               action = 'mkws.limitQuery(\'' + m_teamName + '\', \'' + pzIndex + '\', this.firstChild.nodeValue)';
-           }
-           acc.push('onclick="' + action + ';return false;">' + data[i].name + '</a>'
-                    + ' <span>' + data[i].freq + '</span>');
-           acc.push('</div>');
-       }
-       acc.push('</div>');
-    }
-
-
-    function targetFiltered(id) {
+    that.targetFiltered = function(id) {
        for (var i = 0; i < m_filters.length; i++) {
            if (m_filters[i].id === id ||
                m_filters[i].id === 'pz:id=' + id) {
@@ -422,60 +506,6 @@ function team($, teamName) {
     }
 
 
-    function drawPager (data)
-    {
-       var s = '<div style="float: right">' + M('Displaying') + ': '
-           + (data.start + 1) + ' ' + M('to') + ' ' + (data.start + data.num) +
-           ' ' + M('of') + ' ' + data.merged + ' (' + M('found') + ': '
-           + data.total + ')</div>';
-
-       //client indexes pages from 1 but pz2 from 0
-       var onsides = 6;
-       var pages = Math.ceil(m_totalRec / m_perpage);
-
-       var firstClkbl = (m_curPage - onsides > 0)
-            ? m_curPage - onsides
-            : 1;
-
-       var lastClkbl = firstClkbl + 2*onsides < pages
-            ? firstClkbl + 2*onsides
-            : pages;
-
-       var prev = '<span class="mkwsPrev">&#60;&#60; ' + M('Prev') + '</span><b> | </b>';
-       if (m_curPage > 1)
-            prev = '<a href="#" class="mkwsPrev" onclick="mkws.pagerPrev(\'' + m_teamName + '\');">'
-            +'&#60;&#60; ' + M('Prev') + '</a><b> | </b>';
-
-       var middle = '';
-       for(var i = firstClkbl; i <= lastClkbl; i++) {
-            var numLabel = i;
-            if(i == m_curPage)
-               numLabel = '<b>' + i + '</b>';
-
-            middle += '<a href="#" onclick="mkws.showPage(\'' + m_teamName + '\', ' + i + ')"> '
-               + numLabel + ' </a>';
-       }
-
-       var next = '<b> | </b><span class="mkwsNext">' + M('Next') + ' &#62;&#62;</span>';
-       if (pages - m_curPage > 0)
-            next = '<b> | </b><a href="#" class="mkwsNext" onclick="mkws.pagerNext(\'' + m_teamName + '\')">'
-            + M('Next') + ' &#62;&#62;</a>';
-
-       var predots = '';
-       if (firstClkbl > 1)
-            predots = '...';
-
-       var postdots = '';
-       if (lastClkbl < pages)
-            postdots = '...';
-
-       s += '<div style="float: clear">'
-            + prev + predots + middle + postdots + next + '</div>';
-
-       return s;
-    }
-
-
     ////////////////////////////////////////////////////////////////////////////////
     ////////////////////////////////////////////////////////////////////////////////
 
@@ -489,7 +519,7 @@ function team($, teamName) {
     }
 
 
-    function newSearch(query, sort, targets)
+    function newSearch(query, sortOrder, targets)
     {
        debug("newSearch: " + query);
 
@@ -502,7 +532,7 @@ function team($, teamName) {
        redrawNavi();
        resetPage();
        loadSelect();
-       triggerSearch(query, sort, targets);
+       triggerSearch(query, sortOrder, targets);
        switchView('records'); // In case it's configured to start off as hidden
        m_submitted = true;
     }
@@ -513,7 +543,7 @@ function team($, teamName) {
        if (!m_submitted) return false;
        resetPage();
        loadSelect();
-       m_paz.show(0, m_perpage, m_sort);
+       m_paz.show(0, m_perpage, m_sortOrder);
        return false;
     }
 
@@ -545,17 +575,17 @@ function team($, teamName) {
 
     function resetPage()
     {
-       m_curPage = 1;
-       m_totalRec = 0;
+       m_currentPage = 1;
+       m_totalRecordCount = 0;
     }
 
 
     function loadSelect ()
     {
        var node = findnode('.mkwsSort');
-       if (node.length && node.val() != m_sort) {
-           debug("changing m_sort from " + m_sort + " to " + node.val());
-           m_sort = node.val();
+       if (node.length && node.val() != m_sortOrder) {
+           debug("changing m_sortOrder from " + m_sortOrder + " to " + node.val());
+           m_sortOrder = node.val();
        }
        node = findnode('.mkwsPerpage');
        if (node.length && node.val() != m_perpage) {
@@ -565,17 +595,17 @@ function team($, teamName) {
     }
 
 
-    function triggerSearch (query, sort, targets)
+    function triggerSearch (query, sortOrder, targets)
     {
        var pp2filter = "";
        var pp2limit = "";
 
-       // Re-use previous query/sort if new ones are not specified
+       // Continue to use previous query/sort-order unless new ones are specified
        if (query) {
            m_query = query;
        }
-       if (sort) {
-           m_sort = sort;
+       if (sortOrder) {
+           m_sortOrder = sortOrder;
        }
        if (targets) {
            m_filters.push({ id: targets, name: targets });
@@ -608,7 +638,7 @@ function team($, teamName) {
 
        // We can use: params.torusquery = "udb=NAME"
        // Note: that won't work when running against raw pazpar2
-       m_paz.search(m_query, m_perpage, m_sort, pp2filter, undefined, params);
+       m_paz.search(m_query, m_perpage, m_sortOrder, pp2filter, undefined, params);
     }
 
 
@@ -688,23 +718,23 @@ function team($, teamName) {
 
     that.showPage = function (pageNum)
     {
-       m_curPage = pageNum;
-       m_paz.showPage(m_curPage - 1);
+       m_currentPage = pageNum;
+       m_paz.showPage(m_currentPage - 1);
     }
 
 
     // simple paging functions
     that.pagerNext = function () {
-       if (m_totalRec - m_perpage*m_curPage > 0) {
+       if (m_totalRecordCount - m_perpage*m_currentPage > 0) {
             m_paz.showNext();
-            m_curPage++;
+            m_currentPage++;
        }
     }
 
 
     that.pagerPrev = function () {
        if (m_paz.showPrev() != false)
-            m_curPage--;
+            m_currentPage--;
     }
 
 
@@ -747,19 +777,19 @@ function team($, teamName) {
     // detailed record drawing
     that.showDetails = function (prefixRecId) {
        var recId = prefixRecId.replace('mkwsRec_', '');
-       var oldRecId = m_curDetRecId;
-       m_curDetRecId = recId;
+       var oldRecordId = m_currentRecordId;
+       m_currentRecordId = recId;
 
        // remove current detailed view if any
-       var detRecordDiv = document.getElementById('mkwsDet_' + m_teamName + '_' + oldRecId);
+       var detRecordDiv = document.getElementById('mkwsDet_' + m_teamName + '_' + oldRecordId);
        // lovin DOM!
        if (detRecordDiv)
            detRecordDiv.parentNode.removeChild(detRecordDiv);
 
        // if the same clicked, just hide
-       if (recId == oldRecId) {
-            m_curDetRecId = '';
-            m_curDetRecData = null;
+       if (recId == oldRecordId) {
+            m_currentRecordId = '';
+            m_currentRecordData = null;
             return;
        }
        // request the record
@@ -858,7 +888,7 @@ function team($, teamName) {
 
 
     function mkwsSetLang()  {
-       var lang = parseQuerystring().lang || mkws_config.lang;
+       var lang = mkws.getParameterByName("lang") || mkws_config.lang;
        if (!lang || !mkws.locale_lang[lang]) {
            mkws_config.lang = ""
        } else {
@@ -914,7 +944,7 @@ function team($, teamName) {
 
 
     function mkwsHtmlSort() {
-       debug("HTML sort, m_sort = '" + m_sort + "'");
+       debug("HTML sort, m_sortOrder = '" + m_sortOrder + "'");
        var sort_html = '<select class="mkwsSort mkwsTeam_' + m_teamName + '">';
 
        for(var i = 0; i < mkws_config.sort_options.length; i++) {
@@ -923,7 +953,7 @@ function team($, teamName) {
            var val = opt.length == 1 ? opt[0] : opt[1];
 
            sort_html += '<option value="' + key + '"';
-           if (m_sort == key || m_sort == val) {
+           if (m_sortOrder == key || m_sortOrder == val) {
                sort_html += ' selected="selected"';
            }
            sort_html += '>' + M(val) + '</option>';
@@ -972,15 +1002,14 @@ function team($, teamName) {
 
 
     that.runAutoSearch = function() {
-       // ### should check mkwsTermlist as well, for facet-only teams
-       var node = findnode('.mkwsRecords');
+       var node = findnode('.mkwsRecords,.mkwsTermlists');
        var query = node.attr('autosearch');
        if (!query)
            return;
 
        if (query.match(/^!param!/)) {
            var param = query.replace(/^!param!/, '');
-           query = getParameterByName(param);
+           query = mkws.getParameterByName(param);
            debug("obtained query '" + query + "' from param '" + param + "'");
            if (!query) {
                alert("This page has a MasterKey widget that needs a query specified by the '" + param + "' parameter");
@@ -997,42 +1026,22 @@ function team($, teamName) {
 
        debug("node=" + node + ", class='" + node.className + "', query=" + query);
 
-       var sort = node.attr('sort');
+       var sortOrder = node.attr('sort');
        var targets = node.attr('targets');
        var s = "running auto search: '" + query + "'";
-       if (sort) s += " sorted by '" + sort + "'";
+       if (sortOrder) s += " sorted by '" + sortOrder + "'";
        if (targets) s += " in targets '" + targets + "'";
        debug(s);
 
-       newSearch(query, sort, targets);
+       newSearch(query, sortOrder, targets);
     }
 
 
-    // implement $.parseQuerystring() for parsing URL parameters
-    function parseQuerystring() {
-       var nvpair = {};
-       var qs = window.location.search.replace('?', '');
-       var pairs = qs.split('&');
-       $.each(pairs, function(i, v){
-           var pair = v.split('=');
-           nvpair[pair[0]] = pair[1];
-       });
-       return nvpair;
-    }
-
-
-    // This function is taken from a StackOverflow answer
-    // http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript/901144#901144
-    // ### should we unify this and parseQuerystring()?
-    function getParameterByName(name) {
-       name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
-       var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
-           results = regex.exec(location.search);
-       return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
-    }
-
-
-    /* locale */
+    // Translation function. At present, this is properly a
+    // global-level function (hence the assignment to mkws.M) but we
+    // want to make it per-team so different teams can operate in
+    // different languages.
+    //
     function M(word) {
        var lang = mkws_config.lang;
 
@@ -1051,7 +1060,7 @@ function team($, teamName) {
     function findnode(selector, teamName) {
        teamName = teamName || m_teamName;
 
-       selector = selector.split(',').map(function(s) {
+       selector = $.map(selector.split(','), function(s, i) {
            return s + '.mkwsTeam_' + teamName;
        }).join(',');
 
@@ -1059,15 +1068,6 @@ function team($, teamName) {
     }
 
 
-    function renderSummary(hit)
-    {
-       var template = loadTemplate("Summary");
-       hit._id = "mkwsRec_" + hit.recid;
-       hit._onclick = "mkws.showDetails('" + m_teamName + "', this.id);return false;"
-       return template(hit);
-    }
-
-
     function renderDetails(data, marker)
     {
        var template = loadTemplate("Record");
@@ -1089,7 +1089,7 @@ function team($, teamName) {
 
            var source = node.html();
            if (!source) {
-               source = defaultTemplate(name);
+               source = mkws.defaultTemplate(name);
            }
 
            template = Handlebars.compile(source);
@@ -1099,88 +1099,31 @@ function team($, teamName) {
 
        return template;
     }
+    that.loadTemplate = loadTemplate;
 
 
-    function defaultTemplate(name)
-    {
-       if (name === 'Record') {
-           return '\
-<table>\
-  <tr>\
-    <th>{{translate "Title"}}</th>\
-    <td>\
-      {{md-title}}\
-      {{#if md-title-remainder}}\
-       ({{md-title-remainder}})\
-      {{/if}}\
-      {{#if md-title-responsibility}}\
-       <i>{{md-title-responsibility}}</i>\
-      {{/if}}\
-    </td>\
-  </tr>\
-  {{#if md-date}}\
-  <tr>\
-    <th>{{translate "Date"}}</th>\
-    <td>{{md-date}}</td>\
-  </tr>\
-  {{/if}}\
-  {{#if md-author}}\
-  <tr>\
-    <th>{{translate "Author"}}</th>\
-    <td>{{md-author}}</td>\
-  </tr>\
-  {{/if}}\
-  {{#if md-electronic-url}}\
-  <tr>\
-    <th>{{translate "Links"}}</th>\
-    <td>\
-      {{#each md-electronic-url}}\
-       <a href="{{this}}">Link{{index1}}</a>\
-      {{/each}}\
-    </td>\
-  </tr>\
-  {{/if}}\
-  {{#if-any location having="md-subject"}}\
-  <tr>\
-    <th>{{translate "Subject"}}</th>\
-    <td>\
-      {{#first location having="md-subject"}}\
-       {{#if md-subject}}\
-         {{#commaList md-subject}}\
-           {{this}}{{/commaList}}\
-       {{/if}}\
-      {{/first}}\
-    </td>\
-  </tr>\
-  {{/if-any}}\
-  <tr>\
-    <th>{{translate "Locations"}}</th>\
-    <td>\
-      {{#commaList location}}\
-       {{attr "@name"}}{{/commaList}}\
-    </td>\
-  </tr>\
-</table>\
-';
-       } else if (name === "Summary") {
-           return '\
-<a href="#" id="{{_id}}" onclick="{{_onclick}}">\
-  <b>{{md-title}}</b>\
-</a>\
-{{#if md-title-remainder}}\
-  <span>{{md-title-remainder}}</span>\
-{{/if}}\
-{{#if md-title-responsibility}}\
-  <span><i>{{md-title-responsibility}}</i></span>\
-{{/if}}\
-';
+    // The following PubSub code is modified from the jQuery manual:
+    // https://api.jquery.com/jQuery.Callbacks/
+    //
+    // Use as:
+    // team.queue("eventName").subscribe(function(param1, param2 ...) { ... });
+    // team.queue("eventName").publish(arg1, arg2, ...);
+
+    var queues = {};
+    var queue = function(id) {
+       if (!queues[id]) {
+           var callbacks = $.Callbacks();
+           queues[id] = {
+               publish: callbacks.fire,
+               subscribe: callbacks.add,
+               unsubscribe: callbacks.remove
+           };
        }
-
-       var s = "There is no default '" + name +"' template!";
-       alert(s);
-       return s;
+       return queues[id];
     }
 
+    that.queue = queue;
+
 
     // main
     (function() {
@@ -1316,6 +1259,97 @@ function team($, teamName) {
     }
 
 
+    // This function is taken from a StackOverflow answer
+    // http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript/901144#901144
+    mkws.getParameterByName = function(name) {
+       name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
+       var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
+           results = regex.exec(location.search);
+       return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
+    }
+
+
+    mkws.defaultTemplate = function(name)
+    {
+       if (name === 'Record') {
+           return '\
+<table>\
+  <tr>\
+    <th>{{translate "Title"}}</th>\
+    <td>\
+      {{md-title}}\
+      {{#if md-title-remainder}}\
+       ({{md-title-remainder}})\
+      {{/if}}\
+      {{#if md-title-responsibility}}\
+       <i>{{md-title-responsibility}}</i>\
+      {{/if}}\
+    </td>\
+  </tr>\
+  {{#if md-date}}\
+  <tr>\
+    <th>{{translate "Date"}}</th>\
+    <td>{{md-date}}</td>\
+  </tr>\
+  {{/if}}\
+  {{#if md-author}}\
+  <tr>\
+    <th>{{translate "Author"}}</th>\
+    <td>{{md-author}}</td>\
+  </tr>\
+  {{/if}}\
+  {{#if md-electronic-url}}\
+  <tr>\
+    <th>{{translate "Links"}}</th>\
+    <td>\
+      {{#each md-electronic-url}}\
+       <a href="{{this}}">Link{{index1}}</a>\
+      {{/each}}\
+    </td>\
+  </tr>\
+  {{/if}}\
+  {{#if-any location having="md-subject"}}\
+  <tr>\
+    <th>{{translate "Subject"}}</th>\
+    <td>\
+      {{#first location having="md-subject"}}\
+       {{#if md-subject}}\
+         {{#commaList md-subject}}\
+           {{this}}{{/commaList}}\
+       {{/if}}\
+      {{/first}}\
+    </td>\
+  </tr>\
+  {{/if-any}}\
+  <tr>\
+    <th>{{translate "Locations"}}</th>\
+    <td>\
+      {{#commaList location}}\
+       {{attr "@name"}}{{/commaList}}\
+    </td>\
+  </tr>\
+</table>\
+';
+       } else if (name === "Summary") {
+           return '\
+<a href="#" id="{{_id}}" onclick="{{_onclick}}">\
+  <b>{{md-title}}</b>\
+</a>\
+{{#if md-title-remainder}}\
+  <span>{{md-title-remainder}}</span>\
+{{/if}}\
+{{#if md-title-responsibility}}\
+  <span><i>{{md-title-responsibility}}</i></span>\
+{{/if}}\
+';
+       }
+
+       var s = "There is no default '" + name +"' template!";
+       alert(s);
+       return s;
+    }
+
+
     function defaultMkwsConfig() {
        /* default mkws config */
        var config_default = {
@@ -1463,7 +1497,7 @@ function team($, teamName) {
            }
        });
 
-       // Find all nodes with an class, and determine their team from
+       // Find all nodes with an MKWS class, and determine their team from
        // the mkwsTeam_* class. Make all team objects.
        var then = $.now();
        $('[class^="mkws"],[class*=" mkws"]').each(function () {
@@ -1472,6 +1506,14 @@ function team($, teamName) {
                    mkws.teams[tname] = team(j, tname);
                    debug("Made MKWS team '" + tname + "'");
                }
+           });
+       });
+       // Second pass: make the individual widget objects. This has
+       // to be done separately, and after the team-creation, since
+       // that sometimes makes new widget nodes (e.g. creating
+       // mkwsTermlists instead mkwsResults.
+       $('[class^="mkws"],[class*=" mkws"]').each(function () {
+           mkws.handleNodeWithTeam(this, function(tname, type) {
                var myTeam = mkws.teams[tname];
                var myWidget = widget(j, myTeam, type, this);
            });