1
\$\begingroup\$

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 += "&nbsp;<a class='" + page_class + "'  href='#'>" + (1+i) +"</a> ";
                    bindPaginationEvent('a.' + page_class,startRank);
                } else {
                    // if the page is the current page
                    linksString += "&nbsp;<a class='fb-current-result-page' href='#'>" + (1+i) + "</a>";
                    bindPaginationEvent('.fb-current-result-page',startRank);
                }
                startRank += numRanks;
            }
        }
        if (nextStart) {
            // next links
            linksString += "&nbsp; <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!!

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Johntyb, the main problem you will have is with all those vars defined in the $.fn.uclfunnelback namespace; options, defaults, facetsSelected, etc.

Any vars defined there belong to the plugin itself, not to each instance of the plugin, therefore different instances of the plugin will crosstalk.

The plugin pattern explained/advocated here shows how this can be overcome.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Ahh, thanks. I'll have a read of that page. Much appreciated. \$\endgroup\$
    – Johntyb
    Commented Apr 17, 2013 at 11:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.