I'm fairly new to JavaScript and jQuery and this is my first attempt at creating a plugin. The code below I've written to parse a JSONP feed from a search engine API (funnelback) using ajax() calls. The returned object is used to create a set of results, some pagination links, a facetted navigation etc, everything you'd expect from a basic search results UI.
I suspect there is a fair amount of redundant code, code that could be merged or performance hits written in so I'd appreciate your expert opinion as to what I could modify to improve these aspects.
I'd also appreciate any best practice advice I could incorporate to further develop my own skills and knowledge.
You'll notice there's a reference to the jQuery version used, this is because this plugin can be used with a range of CMS templates that have anything from jQuery 1.3.2 to 1.7 and beyond, so I need to cover the bases. (in reference to the on(), delegate() and live() methods)
(function($) {
$.fn.uclfunnelback = function(options) {
// define the default values of parameters
// these can be overwritten from the plugin call
var defaults = {
collection: 'enterprise-case-studies-demo',
query: '!showmeall',
categories: [{"facet_group_id": "discipline", "facet_group_title": "Discipline", "facet_items": {"mathematical-physical-sciences": "Mathematical & Physical Sciences", "arts-humanities": "Arts & Humanities", "engineering": "Engineering", "built-environment": "Built Environment", "social-historical-science": "Social & Historical Science", "life-medical-sciences": "Life & Medical Sciences", "laws": "Laws"}}, {"facet_group_id": "mechanisms", "facet_group_title": "Mechanisms", "facet_items": {"research-collaborations-studentships": "Research Collaborations Studentships", "student-engagement": "Student Engagement", "placements": "Placements", "business-support": "Business Support", "consultancy": "Consultancy", "partnerships": "Partnerships", "licenses": "Licenses", "spin-out": "Spin Out", "subsidiary": "Subsidiary"}}, {"facet_group_id": "grand_challenges", "facet_group_title": "Grand Challenges", "facet_items": {"sustainable-cities": "Sustainable Cities", "intercultural-interaction": "Intercultural Interaction", "global-health": "Global Health", "human-wellbeing": "Human Wellbeing"}}]
};
options = $.extend(defaults, options);
// Define the global vars
var jq_version = $().jquery,
facetsSelected = {},
qs,
facetqs,
objid,
categories = {},
facetgroups,
facetTotals = {},
catArray = options.categories,
catLen = catArray.length;
// jq_version - The jquery version being used.
// facetsSelected - The facets that are selected by the facetted navigation, used to generate the facetLabels
// qs - The main query string passed to ajaxCall()
// facetqs - The query string that is generated by the facetted navigation, used to perform the ajax call
// objid - The main id of the container div into which everything is output
// categories - The empty JavaScript object for all of the facet categories, used by facetLabels and checkBoxTotals
// facetgroups - The collections of facets from each facet group
// facetTotals - The JavaScript object for all the total articles matching each category, used by facettedNav and checkBoxTotals
// catArray - Assign the user supplied list of categories to an Array, used by facettedNav & below
// catLen - Predetermine the array length to speed up the for loop, used by facettedNav & below
// Merge each of the category facets together into the JavaScript object 'categories'
for (var i=0; i < catLen; i++) {
facetgroups = catArray[i].facet_items;
$.extend(categories, facetgroups);
}
Object.size = function(obj) {
var size = 0, key;
for (key in obj) {
if (obj.hasOwnProperty(key)) {
size++;
}
}
return size;
};
function AjaxTimer(status) {
// A timer to record the funnelback json response time
var aCounter = 0;
var airSeconds = $('#air_seconds');
var anInterval;
if (status === "start") {
anInterval = setInterval(function () {
aCounter += 1;
airSeconds.text(aCounter);
}, 1000);
} else {
clearInterval(anInterval);
anInterval = null;
}
}
function showLoading() {
// Display the AJAX loading icon & the timer if slow to load
$('#awaiting_response').show();
$('#fb-data').hide();
$("#error").hide();
AjaxTimer("start");
}
function ajaxError(request, type, errorThrown) {
// Handle the AJAX load errors
AjaxTimer("stop");
$("#awaiting_response").hide();
$('#fb-data').hide();
$("#error").show();
var message = "There was an error requesting the data.";
switch (type) {
case 'timeout':
message += "The data took too long to retrieve.";
break;
case 'notmodified':
message += "The request was not modified but was retrieved from the cache.";
break;
case 'parsererror':
message += "The data was badly formatted.";
break;
default:
message += "HTTP Error: (" + request.status + " " + request.statusText + ").";
}
message += "<br />";
$("#error").append(message);
}
function paginationClicky(pageNo) {
// Click function for the pagination buttons
if ( facetqs ) {
ajaxCall(pageNo, facetqs);
} else {
ajaxCall(pageNo, null);
}
}
function bindPaginationEvent(pagclass, pageNo) {
// Bind the pagination events, with modifications for older versions of jQuery
if (jq_version >= '1.7') {
// As of jQuery 1.7, the .live() method is deprecated. Use .on() to attach event handlers.
$(objid).off('click', pagclass);
$(objid).on('click', pagclass, function(e) {
paginationClicky(pageNo);
e.preventDefault();
});
} else if ( jq_version >= '1.4.3' && jq_version < '1.7') {
// Users of jQuery versions 1.4.3 to 1.7 should use .delegate() in preference to .live().
$(objid).undelegate(pagclass, 'click');
$(objid).delegate(pagclass, 'click', function(e){
paginationClicky(pageNo);
e.preventDefault();
});
} else {
// Users of jquery 1.3.2 have no choice but to use live()
$(pagclass).die('click');
$(pagclass).live('click', function(e){
paginationClicky(pageNo);
e.preventDefault();
});
}
}
function checkboxToggle(checkbox) {
// Toggle function for the checkboxes
if (checkbox.attr("checked")) {
facetsSelected[checkbox.attr('value')] = checkbox.val();
} else {
delete facetsSelected[checkbox.attr('value')];
}
facetFormSubmit();
}
function bindCheckboxToggle(selectorStr) {
// Bind the checkbox toggle events, with modifications for older versions of jQuery
var checkboxSelector = 'input:checkbox.' + selectorStr;
if (jq_version >= '1.7') {
// As of jQuery 1.7, the .live() method is deprecated. Use .on() to attach event handlers.
$(objid).off('change', checkboxSelector);
$(objid).on('change', checkboxSelector, function() {
var $this = $(this);
checkboxToggle($this);
});
} else if ( jq_version >= '1.4.3' && jq_version < '1.7') {
// Users of jQuery versions 1.4.3 to 1.7 should use .delegate() in preference to .live().
$(objid).undelegate(checkboxSelector, 'change');
$(objid).delegate(checkboxSelector, 'change', function(){
var $this = $(this);
checkboxToggle($this);
});
} else {
// Users of jquery 1.3.2 have no choice but to use live()
$(checkboxSelector).die('change');
$(checkboxSelector).live('change', function(){
var $this = $(this);
checkboxToggle($this);
});
}
}
function bindFacetLabel(facetid) {
// Bind the facetLables click events, with modifications for older versions of jQuery
var facetselector = 'a.' + facetid;
var checkboxSelector = 'input:checkbox.' + facetid;
if (jq_version >= '1.7') {
// As of jQuery 1.7, the .live() method is deprecated. Use .on() to attach event handlers.
$(objid).off('click', facetselector);
$(objid).on('click', facetselector, function(e) {
delete facetsSelected[facetid];
$(checkboxSelector).removeAttr('checked');
e.preventDefault();
facetFormSubmit();
});
} else if ( jq_version >= '1.4.3' && jq_version < '1.7') {
// Users of jQuery versions 1.4.3 to 1.7 should use .delegate() in preference to .live().
$(objid).undelegate(facetselector, 'click');
$(objid).delegate(facetselector, 'click', function(e){
delete facetsSelected[facetid];
$(checkboxSelector).removeAttr('checked');
e.preventDefault();
facetFormSubmit();
});
} else {
// Users of jquery 1.3.2 have no choice but to use live()
$(facetselector).die('click');
$(facetselector).live('click', function(e){
delete facetsSelected[facetid];
$(checkboxSelector).removeAttr('checked');
e.preventDefault();
facetFormSubmit();
});
}
}
function facetLabels(totalMatching) {
// Generate the facet labels
var freeText = $("#fbquery").val();
var res, csText;
var facetLen = Object.size(facetsSelected);
var facetoutput = "";
if (totalMatching === 1) {
res = " result";
csText = " Item";
} else {
res = " results";
csText = " Items";
}
if (facetLen > 0) {
facetoutput += "<div id='pillboxes'><h3>Active Filters</h3><ul>";
for (var prop in facetsSelected) {
if (facetsSelected.hasOwnProperty(prop)) {
facetoutput += "<li><a class='"+ prop +"' href='#'>" + categories[prop] + "</a></li>";
bindFacetLabel(prop);
}
}
facetoutput += "</ul><div style='clear:both;'></div></div>";
}
facetoutput += "<h3><span class='resultCount'>" + totalMatching + "</span> ";
if (facetLen === 0 && !freeText) {
facetoutput += csText + " found";
} else if (facetLen === 0 && freeText) {
facetoutput += res + " matched your search for <span class='searchTerm'>'" + freeText + "'</span>";
} else {
var facetCounter = 0;
for (var facet in facetsSelected) {
if (facetsSelected.hasOwnProperty(facet)) {
facetCounter++;
var facetTitle = categories[facetsSelected[facet]];
if (facetCounter === 1) {
facetoutput += res + " matched your search for <span class='searchTerm'>" + facetTitle + "</span>";
} else if (facetCounter === facetLen - 1) {
facetoutput += ", <span class='searchTerm'>" + facetTitle + "</span>";
} else {
if (!freeText) {
facetoutput += " and <span class='searchTerm'>" + facetTitle + "</span>";
} else {
facetoutput += ", <span class='searchTerm'>" + facetTitle + "</span>";
}
}
}
}
if (freeText) {
facetoutput += " and <span class='searchTerm'>'" + freeText + "'</span>";
}
}
facetoutput += "</h3>";
$('.resultcount').html(facetoutput);
}
function searchResults(currStart, data) {
// Generate the basic search results
// the results are derived from the json feed (data).
var i,
res_output = "",
searchItems = data.response.resultPacket.results;
res_output += "<ol start='" + currStart + "'><br />";
for (i in searchItems) {
if (searchItems.hasOwnProperty(i)) {
res_output += "<li><h4><a href='" + searchItems[i].liveUrl + "'>" + searchItems[i].title + "</a></h4><p>" + searchItems[i].summary+"</p></li>";
}
}
res_output += '</ol>';
$(objid).html(res_output);
}
function pagination (currStart, numPages, currPage, numRanks, nextStart, prevStart) {
// Generate the pagination links
var linksString = "<p class='fb-page-nav'>";
var page_class;
if (currStart > 1) {
// previous links
linksString += "<a class='fb-previous-result-page' href='#'>Prev 10</a> ";
bindPaginationEvent('.fb-previous-result-page',prevStart);
}
var startRank = 1;
if (numPages > 1) {
for (i=0; i<numPages; i++){
if (i !== currPage) {
// if the page is not the current page
page_class = "fb-result-page"+ (1+i);
linksString += " <a class='" + page_class + "' href='#'>" + (1+i) +"</a> ";
bindPaginationEvent('a.' + page_class,startRank);
} else {
// if the page is the current page
linksString += " <a class='fb-current-result-page' href='#'>" + (1+i) + "</a>";
bindPaginationEvent('.fb-current-result-page',startRank);
}
startRank += numRanks;
}
}
if (nextStart) {
// next links
linksString += " <a class='fb-next-result-page fb-page-nav' href='#'>Next 10</a>";
bindPaginationEvent('.fb-next-result-page',nextStart);
}
linksString += "</p>";
$('.pagination').html(linksString);
}
function contains(array, value) {
// function to determine if a value is contained within an array
// Faster than inArray() and indexOf() and..
// supported by *all* browsers unlike indexOf()
var index = -1,
length = array.length;
while (++index < length) {
if (array[index] === value) {
return true;
}
}
return false;
}
function checkboxTotals(facetData) {
// Calculate the facet checkbox totals
var temp, tally, isMultiFacetted;
// iterate the 'categories' json array
for (var facet in categories) {
if (categories.hasOwnProperty(facet)) {
facetTotals[facet] = 0;
}
}
// iterate the rmcs json object {}
for (var i in facetData) {
if (facetData.hasOwnProperty(i)) {
temp = i.slice(2,i.length);
tally = facetData[i];
isMultiFacetted = contains(temp, ',');
if (isMultiFacetted) {
// if temp is more than one item
// split it and make an array
var token = temp.split(",");
var tokenlen = token.length;
// iterate the array
// and compare the item (j) found with each item in categories object
// if j and c same then increment facetTotals for the appropriate facet
for (var j=0; j<tokenlen; j++) {
for (var c in categories) {
if (categories.hasOwnProperty(c)) {
if (token[j] === c) {
facetTotals[c] += tally;
break;
}
}
}
}
} else {
facetTotals[temp] += tally;
}
}
}
}
function resetForm() {
// Event handler to reset the form back to the defaults
$('.reset-button').die('click');
$('.reset-button').live('click', function(){
facetsSelected = {};
ajaxCall(null, null);
});
}
function facettedNav() {
// Generate the facetted Navigation
var facetoutput = '';
// catArray & catLen are global vars.
for (var i = 0; i < catLen; i++) {
// Generate the facet titles from the json objects in catArray
facetoutput += '<h3>' + catArray[i].facet_group_title + '</h3>\n<ul>';
// iterate the json array of the facet iems to generate the checkboxes
var itemsObj = catArray[i].facet_items;
var groupID = catArray[i].facet_group_id;
var metadataClass = 'meta_' + groupID.charAt(0).toUpperCase() + '_sand';
for (var key in itemsObj) {
if (itemsObj.hasOwnProperty(key)) {
// if there total no of results for any given facet is greater than 0, display the facet checkbox
if (facetTotals[key] > 0) {
if (facetsSelected.hasOwnProperty(key)) {
facetoutput += '<li id="' + key + '" style="opacity: 1;"><label><input type="checkbox" class="' + key + '" name="' + metadataClass + '" value="' + key + '" checked>' + itemsObj[key] + ' <span class="count">(' + facetTotals[key] + ')</span> </label></li>';
} else {
facetoutput += '<li id="' + key + '" style="opacity: 1;"><label><input type="checkbox" class="' + key + '" name="' + metadataClass + '" value="' + key + '">' + itemsObj[key] + ' <span class="count">(' + facetTotals[key] + ')</span> </label></li>';
}
bindCheckboxToggle(key);
} else {
// else show the facet checkbox but disable it and lowever the labels opacity
facetoutput += '<li id="' + key + '" style="opacity: 0.5;"><label><input type="checkbox" class="' + key + '" disabled="disabled" name="' + metadataClass + '" value="' + key + '">' + itemsObj[key] + ' <span class="count">(' + facetTotals[key] + ')</span> </label></li>';
}
}
}
facetoutput += '</ul>';
}
// Add the reset button
facetoutput += '<input class="reset-button" type="reset" name="reset" value="Reset">';
resetForm();
// Insert the generated output to the appropriate div
$('.facetted-nav').html(facetoutput);
}
function facetFormSubmit() {
// Generte the new query string everytime the form is submit
facetqs = $('#facetsearch').serialize();
ajaxCall(null, facetqs);
}
function parseResults(data){
// Parse the json feed and render the results
// 1st stop the timer
AjaxTimer("stop");
// & show or hide the appropriate divs
$('#awaiting_response').hide();
$('#fb-data').show();
$("#error").hide();
// collate the data from the json feed
var totalMatching = data.response.resultPacket.resultsSummary.totalMatching;
var numRanks = data.response.resultPacket.resultsSummary.numRanks;
var currStart = data.response.resultPacket.resultsSummary.currStart;
var prevStart = data.response.resultPacket.resultsSummary.prevStart;
var nextStart = data.response.resultPacket.resultsSummary.nextStart;
var numPages = Math.ceil(totalMatching/numRanks);
var currPage = Math.floor(currStart/numRanks);
var facetData = data.response.resultPacket.rmcs;
// Calculate the checkbox totals
checkboxTotals(facetData);
// Display the facetted navigation beneath the search
facettedNav();
// Display the heading and the filtering info
facetLabels(totalMatching);
// Display the search results in the appropriate div
searchResults(currStart, data);
// Display the pagination
pagination(currStart, numPages, currPage, numRanks, nextStart, prevStart);
// Bind an event handler to the form "submit" JavaScript event
$('#facetsearch').submit(function(e) {
facetFormSubmit();
e.preventDefault();
});
}
function ajaxCall(startRank, facetqs) {
// This is the universal ajax call method
// Reset the query string to '' first
qs = '';
// Build the query string for the ajax() call
// Generate the 'query' element so that if facets are checked they concatenate
if (facetqs && facetqs !== 'query=') {
// Trim 'query=' off the beginning of facetqs
var trimmedFacetqs = facetqs.substring(6);
qs += '&query=' + trimmedFacetqs;
} else {
// If no facets checked, set the default 'query' element to options.query
qs += '&query=' + options.query;
}
// Append the collection
qs += '&collection=' + options.collection;
// And append the startRank, if there is one
if (startRank) {
qs += '&start_rank=' + startRank;
}
// set the base funnelback url
var baseurl = 'http://funnelback-uat.ucl.ac.uk/s/search.json?callback=?';
// var testingurl = 'http://localhost:8217/silva/default-funnelback-search/search.json';
$.ajax({
type:'GET',
data: qs,
url: baseurl,
dataType: "jsonp",
// url: testingurl,
// dataType: "json", // Test dataType
beforeSend: showLoading,
timeout: 5000,
error: ajaxError,
scriptCharset: "utf-8",
success: function (json) {
parseResults(json);
}
});
}
// Initiate the plugin
return this.each(function() {
// set some variables
var obj = $(this);
objid = "#" + obj.attr('id');
// perform the inital ajaxCall
ajaxCall(null, null);
});
};
})(jQuery);
I appreciate any help!!