Tuesday, November 24, 2009

JQuery Autocomplete - more complete!

Hi there!
A couple of days ago I started to develop something that needed a fancy, unobtrusive jquery autocomplete plugin, that could accept JSON as data source and created light HTML and I found this one.
But then, it missed two simple functionalities that I added:

One -> the for each of your result items, isn't controlled by you, it expects that your JSON contains a value property, and uses it, but my JSON was a little more complex than that, so I coded a setting to the autocomplete plugin, so you can define, or not, your drawing function!
Two -> If you needed to force your autocomplete list to show up, you couldn't, and I coded that two.

So here's the result

or here:

/*!
* Auto Complete 4.1
* October 5, 2009
* Corey Hart @ http://www.codenothing.com
* Ricardo Rodrigues @ http://sharpdevpt.blogspot.com
*/
; (function($, undefined) {
    // Expose autoComplete to the jQuery chain
    $.fn.autoComplete = function() {
        // Force array of arguments
        var args = Array.prototype.slice.call(arguments);

        // Autocomplete special triggers
        if (typeof args[0] === 'string')
        // Trigger the requested function, and dont break the chain!
            return $(this).trigger('autoComplete.' + args.shift(), args);

        // Initiate the autocomplete
        return autoComplete.call(this, args[0]);
    };

    // bgiframe is needed to fix z-index problem for IE6 users.
    $.fn.bgiframe = $.fn.bgiframe ? $.fn.bgiframe : $.fn.bgIframe ? $.fn.bgIframe : function() {
        // For applications that don't have bgiframe plugin installed, create a useless 
        // function that doesn't break the chain
        return this;
    };

    // Autocomplete function
    var inputIndex = 0, autoComplete = function(options) {
        return this.each(function() {
            // Cache objects
            var $input = $(this).attr('autocomplete', 'off'), $li, timeid, timeid2, blurid,
            // Internal Per Input Cache
				cache = {
				    length: 0,
				    val: undefined,
				    list: {}
				},
            // Set defaults and include metadata support
				settings = $.extend({
				    // Inner Function Defaults (Best to leave alone)
				    opt: -1,
				    inputval: undefined,
				    mouseClick: false,
				    dataName: 'ac-data',
				    inputIndex: ++inputIndex,
				    // Server Script Path
				    ajax: 'ajax.php',
				    dataSupply: [],
				    dataFn: undefined,
				    // Drop List CSS
				    list: 'auto-complete-list',
				    rollover: 'auto-complete-list-rollover',
				    width: $input.outerWidth(),
				    // Post Data
				    postVar: 'value',
				    postData: {},
				    // Limitations
				    minChars: 1,
				    maxItems: -1,
				    maxRequests: 0,
				    requestType: 'post',
				    requests: 0, // Inner Function Default
				    // Events
				    onMaxRequest: function() { },
				    onSelect: function() { },
				    onRollover: function() { },
				    onBlur: function() { },
				    onFocus: function() { },
				    inputControl: function(v) { return v; },
				    preventEnterSubmit: false,
				    enter: true, // Inner Function Default
				    delay: 100,
				    selectFuncFire: true, // Inner Function Default
				    // Caching Options
				    useCache: true,
				    cacheLimit: 50,
				    htmlCustomFormatter: undefined
				}, options || {}, $.metadata ? $input.metadata() : {}),

            // Create the drop list (Use an existing one if possible)
				$ul = $('ul.' + settings.list)[0] ?
					$('ul.' + settings.list).bgiframe() :
					$('
    ').appendTo('body').addClass(settings.list).bgiframe().hide(); // Input Events $input.data('ac-input-index', settings.inputIndex) // Attach input index // Central autoComplete specific function .bind('keyup.autoComplete', function(event) { var key = event.keyCode; settings.mouseClick = false; // Enter Key if (key == 13 && $li) { settings.opt = -1; // Ensure the select function only gets fired once if (settings.selectFuncFire) { settings.selectFuncFire = false; settings.onSelect.call($input[0], $li.data(settings.dataName), $li, $ul); if (timeid2) clearTimeout(timeid2); timeid2 = setTimeout(function() { settings.selectFuncFire = true; }, 1000); } $ul.hide(); } // Up Arrow else if (key == 38) { if (settings.opt > 0) { settings.opt--; $li = $('li', $ul).removeClass(settings.rollover).eq(settings.opt).addClass(settings.rollover); $input.val($li.data(settings.dataName).value || ''); settings.onRollover.call($input[0], $li.data(settings.dataName), $li, $ul); } else { settings.opt = -1; $input.val(settings.inputval); $ul.hide(); } } // Down Arrow else if (key == 40) { if (settings.opt < $('li', $ul).length - 1) { settings.opt++; $li = $('li', $ul.show()).removeClass(settings.rollover).eq(settings.opt).addClass(settings.rollover); $input.val($li.data(settings.dataName).value || ''); settings.onRollover.call($input[0], $li.data(settings.dataName), $li, $ul); } } // Everything else is possible input else { settings.opt = -1; settings.inputval = $input.val(); cache.val = settings.inputControl.call($input, settings.inputval, key); if (cache.val.length >= settings.minChars) { // Send request on timer so fast typing doesn't overload requests if (timeid) clearTimeout(timeid); timeid = setTimeout(function() { sendRequest(settings, cache); clearTimeout(timeid); }, settings.delay); } else if (key == 8) { // Remove list on backspace of small string $ul.html('').hide(); } } }) // Bind specific Blur Actions .bind('blur.autoComplete', function() { settings.enter = true; blurid = setTimeout(function() { if (settings.mouseClick) return false; settings.opt = -1; settings.onBlur.call($input[0], settings.inputval, $ul); $ul.hide(); }, 150); }) // Bind specific focus actions .bind('focus.autoComplete', function() { settings.enter = false; // If ul is not associated with current input, clear it if (settings.inputIndex != $ul.data('ac-input-index')) $ul.html('').hide(); settings.onFocus.call($input[0], $ul); }) /** * Autocomplete Special Triggers * -Extensions off autoComplete event */ // Allows for change of settings at any point .bind('autoComplete.settings', function(event, newSettings) { // Give access to current settings and cache if ($.isFunction(newSettings)) { var ret = newSettings.call($input[0], settings, cache); // Allow for extending of settings/cache based off function return values if ($.isArray(ret) && ret.length) { settings = $.extend(true, {}, settings, ret[0] || settings); cache = $.extend(true, {}, cache, ret[1] || cache); } } else { // Extend deep so settings are kept settings = $.extend(true, {}, settings, newSettings || {}); } }) // Clears the Cache & requests (requests can be blocked on request) .bind('autoComplete.flush', function(event, cacheOnly) { cache = { length: 0, val: undefined, list: {} }; if (!cacheOnly) settings.requests = 0; }) // External button trigger for ajax requests .bind('autoComplete.button.ajax', function(event, postData, cacheName) { // Refocus the input box $input.focus(); // Remove blur trigger if (blurid) clearTimeout(blurid); // Allow for just passing the cache name if (typeof postData === 'string') { cacheName = postData; postData = {}; } // If no cache name is given, supply a non-common word cache.val = cacheName || 'NON_404_<>!@$^&'; // Send request on timer so focus event doesn't override if (timeid) clearTimeout(timeid); timeid = setTimeout(function() { sendRequest($.extend(true, {}, settings, { opt: -1, maxItems: -1, postData: postData || {} }), cache); clearTimeout(timeid); }, settings.delay); }) // External button trigger for supplied data .bind('autoComplete.button.supply', function(event, data, cacheName) { // Refocus the input box $input.focus(); // Remove blur trigger if (blurid) clearTimeout(blurid); // Allow for just passing of cacheName if (typeof data === 'string') { cacheName = data; data = undefined; } // If no cache name is given, supply a non-common word cache.val = cacheName || 'NON_404_SUPPLY_<>!@$^&'; // If no data is supplied, use data in settings data = $.isArray(data) ? data : settings.dataSupply; // Send request on timer so focus event doesn't override if (timeid) clearTimeout(timeid); timeid = setTimeout(function() { sendRequest($.extend(true, {}, settings, { opt: -1, maxItems: -1, dataSupply: data, dataFn: function() { return true; } }), cache); clearTimeout(timeid); }, settings.delay); }) // Add a destruction function .bind('autoComplete.destroy', function() { // Unbind input events $input.unbind('keyup.autoComplete blur.autoComplete focus.autoComplete autoComplete') // Unbind the form submission event .parents('form').eq(0).unbind('submit.autoComplete.' + settings.inputIndex); }) // Add a show list function .bind('autoComplete.showlist', function() { $ul.show(); }) // Back to normal events // Prevent form submission if defined in settings .parents('form').eq(0).bind('submit.autoComplete.' + settings.inputIndex, function() { return settings.preventEnterSubmit ? settings.enter : true; }); // Ajax/Cache Request function sendRequest(settings, cache) { // Check Max reqests first if (settings.maxRequests && ++settings.requests >= settings.maxRequests) return settings.requests > settings.maxRequests ? false : settings.onMaxRequest.call($input[0], settings.inputval, $ul); // Load from cache if possible if (settings.useCache && cache.list[cache.val]) return loadResults(cache.list[cache.val], settings, cache); // Use user supplied data when defined if (settings.dataSupply.length) return userSuppliedData(settings, cache); // Send request server side settings.postData[settings.postVar] = cache.val $[settings.requestType](settings.ajax, settings.postData, function(json) { // Show the list if there is a return, else hide it loadResults(json, settings, cache); // Use jQuery's method of json evaluation // (thus, can only send 'get' or 'post' jQuery requests) }, 'json'); } // Parse User Supplied Data function userSuppliedData(settings, cache) { var json = [], // Result list fn = $.isFunction(settings.dataFn), // User supplied function regex = fn ? undefined : new RegExp('^' + cache.val, 'i'), // Only compile regex if needed k = 0, entry, i; // Looping vars // Loop through each entry and find matches for (i in settings.dataSupply) { entry = settings.dataSupply[i]; // Force object entry = typeof entry === 'object' && entry.value ? entry : { value: entry }; // If user supplied function, use that, otherwise test with default regex if ((fn && settings.dataFn.call($input[0], cache.val, entry.value, json, i, settings.dataSupply)) || (!fn && entry.value.match(regex))) { // Reduce browser load by breaking on limit if it exists if (settings.maxItems > -1 && ++k > settings.maxItems) break; json.push(entry); } } // Use normal load functionality loadResults(json, settings, cache); } // List Functionality function loadResults(list, settings, cache) { // Store results into the cache if need be if (settings.useCache) { cache.length++; cache.list[cache.val] = list; // Clear cache if necessary if (settings.cacheLength > settings.cacheLimit) { cache.list = {}; cache.length = 0; } } // Ensure there is a list if (!list || list.length < 1) return $ul.html('').hide(); // Initialize Vars together (save bytes) var offset = $input.offset(), // Store offsets aci = 0, i; // Index list items // Clear the List and align it properly $ul.data('ac-input-index', settings.inputIndex).html('').css({ top: offset.top + $input.outerHeight(), left: offset.left, width: settings.width }); // Add new rows to the list for (i in list) { var fn = $.isFunction(settings.htmlCustomFormatter); if (list[i].value || fn) { if (settings.maxItems > -1 && ++aci > settings.maxItems) { break; } var appendHTML = list[i].display || list[i].value; //user custom function to compose HTML if (fn) { appendHTML = settings.htmlCustomFormatter(list[i]); } $('
  • ').appendTo($ul).html(appendHTML)
    .data(settings.dataName, list[i]).data('ac-index', aci);
    }
    }

    // Remove old mouseout event and return orignal val when not hovering
    $ul.show().unbind('mouseout.autoComplete').bind('mouseout.autoComplete', function() {
    $('li.' + settings.rollover, $ul).removeClass(settings.rollover);
    if (!settings.mouseClick && settings.selectFuncFire)
    $input.val(settings.inputval);
    // Unbind any events that linger from previous drops
    // I don't understand why this helps yet, because the old li elements are
    // removed and new ones created/added to the ul element; but for now, it works
    }).children('li').unbind('mouseover.autoComplete').unbind('click.autoComplete')
    // New mouseover and click events
    .bind('mouseover.autoComplete', function() {
    $li = $(this);
    $('li.' + settings.rollover, $ul).removeClass(settings.rollover);
    $input.val($li.addClass(settings.rollover).data(settings.dataName).value);
    settings.onRollover.call($input[0], $li.data(settings.dataName), $li, $ul);
    settings.opt = $li.data('ac-index');
    }).bind('click.autoComplete', function() {
    settings.mouseClick = true;
    if (blurid) clearTimeout(blurid);
    settings.onSelect.call($input[0], $li.data(settings.dataName), $li, $ul);
    $ul.hide();
    // Bring the focus back to the input when clicking a list member
    $input.focus();
    });
    }
    });
    };
    })(jQuery);

Make it good use! :)

No comments: