/*jshint forin:true, noarg:true, noempty:true, eqeqeq:false, bitwise:true, strict:true, undef:true, curly:true, browser:true, indent:4, maxerr:50, onevar:false, nomen:false, regexp:false, plusplus:false, newcap:true */ (function ($) { "use strict"; //Thanks John - http://ejohn.org/blog/javascript-array-remove/ var arrayRemove = function (from, to) { var rest = this.slice((to || from) + 1 || this.length); this.length = from < 0 ? this.length + from : from; return this.push.apply(this, rest); }; $.fn.ajaxChosen = function (ajaxOptions, options, chosenOptions) { var select = $(this), chosen, keyRight, input, inputBG, callback, throttle = false, requestQueue = [], typing = false, loadingImg = '/img/loading.gif', minLength = 1; if ($('option', select).length === 0) { //adding empty option so you don't have to, and chosen can perform search correctly select.append('<option value=""></option>'); } if (chosenOptions) { select.chosen(chosenOptions); } else { select.chosen(); } chosen = select.next(); input = $('input', chosen); inputBG = input.get(0) ? input.get(0).style.background : ''; //copy out success callback if ('success' in ajaxOptions && $.isFunction(ajaxOptions.success)) { callback = ajaxOptions.success; } //replace with our success callback ajaxOptions.success = function (data, textStatus, jqXHR) { var items = data, selected, valuesArray, valuesHash = {}, requestQueueLength = requestQueue.length, old = false, keep = false, inputEmptied = false, inputEmptiedValue = ''; if (typing) { //server returned a response, but it's about to become an older response //so discard it and wait until the user is done typing requestQueue.shift(); return false; } if (requestQueueLength > 1) { $.each(requestQueue, function (idx, elem) { if (data.q === elem) { if (idx !== (requestQueueLength - 1)) { //found an older response, remove it from the queue and wait for newest response old = true; arrayRemove.call(requestQueue, idx); } else { //this handles the out of order request/response //last request came in first, and we want to keep it keep = true; //remove all the other older requests requestQueue.length = 0; } return false; } }); //if we found an old response or we found the newest response and want to keep processing if (old || !keep) { return false; } } else { //only 1 request was made by the user remove it from queue and continue processing if (typeof requestQueue.shift() === 'undefined') { //If all the old responses have been discarded because we've received the new one already return false; } } //while the request was processing did the user "empty" the input box inputEmptiedValue = $.trim(input.val());//if we have a minLength > 1 then we have to preserve the value (TODO::watch out for potential XSS/other breakages) inputEmptied = inputEmptiedValue.length < minLength; //if additional processing needs to occur on the returned json if ('processItems' in options && $.isFunction(options.processItems)) { items = options.processItems(data); } else if ('results' in items) { //default behavior if process items isn't defined //expects there to be a results key in data returned that has the results of the search items = items.results; } else { console.log('Expected results key in data, but was not found. Options could not be built'); return false; } //.chzn-choices is only present with multi-selects selected = $('option:selected', select).not(':empty').clone().attr('selected', true); //saving values for deduplication if (!$.isArray(select.val())) { valuesArray = [select.val()]; } else { valuesArray = select.val(); } $.each(valuesArray, function (i, value) { valuesHash[value] = 1; }); $('option', select).remove(); $('<option value=""/>').appendTo(select); //appending this even on single select in the event the user changes their mind and input is blurred. Keeps selected option selected selected.appendTo(select); if (!inputEmptied) { if ($.isArray(items)) { //array of kv pairs [{id:'', text:''}...] $.each(items, function (i, opt) { if (typeof valuesHash[opt.id] === 'undefined') { $('<option value="' + opt.id + '">' + opt.text + '</option>').appendTo(select); } }); } else { //hash of kv pairs {'id':'text'...} $.each(items, function (value, text) { if (typeof valuesHash[value] === 'undefined') { $('<option value="' + value + '">' + text + '</option>').appendTo(select); } }); } } //update chosen select.trigger("chosen:updated"); //right key, for highlight options after ajax is performed keyRight = $.Event('keyup'); keyRight.which = 39; //highlight input.val(!inputEmptied ? data.q : inputEmptiedValue).trigger(keyRight).get(0).style.background = inputBG; if (items.length > 0) { $('.no-results', chosen).hide(); } else { $('.no-results', chosen).show(); } //fire original success if (callback) { callback(data, textStatus, jqXHR); } }; //set loading image options || (options = {}); if ('loadingImg' in options) { loadingImg = options.loadingImg; } //set minimum length that will trigger autocomplete if ('minLength' in options) { minLength = options.minLength; } $('.chosen-search > input, .chosen-choices .search-field input', chosen). bind('keyup', processValue). bind('paste', function (e) { var that = this; setTimeout(function() { processValue.call(that, e); }, 50); }); function processValue(e) { var field = $(this), q = field.val(); //don't fire ajax if... if ((e.type === 'paste' && field.is(':not(:focus)')) || (e.which && ( (e.which === 9) ||//Tab (e.which === 13) ||//Enter (e.which === 16) ||//Shift (e.which === 17) ||//Ctrl (e.which === 18) ||//Alt (e.which === 19) ||//Pause, Break (e.which === 20) ||//CapsLock (e.which === 27) ||//Esc (e.which === 33) ||//Page Up (e.which === 34) ||//Page Down (e.which === 35) ||//End (e.which === 36) ||//Home (e.which === 37) ||//Left arrow (e.which === 38) ||//Up arrow (e.which === 39) ||//Right arrow (e.which === 40) ||//Down arrow (e.which === 44) ||//PrntScrn (e.which === 45) ||//Insert (e.which === 144) ||//NumLock (e.which === 145) ||//ScrollLock (e.which === 91) ||//WIN Key (Start) (e.which === 93) ||//WIN Menu (e.which === 224) ||//command key (e.which >= 112 && e.which <= 123)//F1 to F12 ))) { return false; } //backout of ajax dynamically if ('useAjax' in options && $.isFunction(options.useAjax)) { if (!options.useAjax(e)) { return false; } } //hide no results $('.no-results', chosen).hide(); //backout if nothing is in input box if ($.trim(q).length < minLength) { input.get(0).style.background = inputBG; if (throttle) { clearTimeout(throttle); } return false; } typing = true; //add query to data if ($.isArray(ajaxOptions.data)) { //array if (ajaxOptions.data[ajaxOptions.data.length - 1].name === 'q') { ajaxOptions.data.pop(); } ajaxOptions.data = ajaxOptions.data.concat({ name: 'q', value: q}); } else { //hash if (!('data' in ajaxOptions)) { ajaxOptions.data = {}; } $.extend(ajaxOptions.data, { q: q}); } $.extend(ajaxOptions.data, { _ : Date.now() }); //dynamically generate url if ('generateUrl' in options && $.isFunction(options.generateUrl)) { ajaxOptions.url = options.generateUrl(q); } //show loading input.get(0).style.background = 'transparent url("' + loadingImg + '") no-repeat right 3px'; //throttle that bitch, so we don't kill the server if (throttle) { clearTimeout(throttle); } throttle = setTimeout(function () { requestQueue.push(q); typing = false; $.ajax(ajaxOptions); }, 700); }; return select; }; })(jQuery);