Advertisement
bkwdesign

jSuggest AutoSuggest plugin: fixed for IE8 hover issues

Feb 11th, 2013
75
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /*
  2. * jSuggest
  3. * Version 0.81 | Updated: Feb. 11, 2013
  4. *
  5. * This Plug-In will set up a UI that suggest results for your search queries as you type.
  6. * You can add multiple selections as tokens and remove them on the fly.
  7. * It supports keybord navigation, ajax source, prefill and multiple jSuggest fields on the same page.
  8. * (patched by Bkwdesign to workaround IE8 hover support-lack)
  9. *
  10. * Built on top of the Autocomplete plugin by: Drew Wilson | www.drewwilson.com
  11. * code.drewwilson.com/entry/autosuggest-jquery-plugin
  12. *
  13. * This jSuggest jQuery plug-in is dual licensed under the MIT and GPL licenses:
  14. * http:// www.opensource.org/licenses/mit-license.php
  15. * http:// www.gnu.org/licenses/gpl.html
  16. */
  17.  
  18. (function($){
  19.   $.fn.jSuggest = function(options) {
  20.     var defaults = {
  21.       source: {}, // Object or URL where jSuggest gets the suggestions from.
  22.       uniqID: false,
  23.       startText: 'Enter a Value', // Text to display when the jSuggest input field is empty.
  24.       emptyText: 'No Results Found', // Text to display when their are no search results.
  25.       preFill: {}, // Enables you to pre-fill the jSuggest box with selections when the page is first loaded.
  26.       limitText: 'No More Values Are Allowed', // Text to display when the number of selections has reached it's limit.
  27.       newItem: false, // If set to false, the user will not be able to add new items by any other way than by selecting from the suggestions list.
  28.       newText: 'Adding New Values Is Not Allowed', // Text to display when the user tries to enter a new item by typing.
  29.       selectedItemProp: 'value', // Name of object property.
  30.       selectProp: 'value', // Name of object property.
  31.       seekVal: 'value', // Comma separated list of object property names.
  32.       queryParam: 'q', // The name of the param that will hold the search string value in the AJAX request.
  33.       queryLimit: false, // Number for 'limit' param on ajax request.
  34.       extraParams: '', // This will be added onto the end of the AJAX request URL. Make sure you add an '&' before each param.
  35.       matchCase: false, // Make the search case sensitive when set to true.
  36.       minChars: 1, // Minimum number of characters that must be entered before the search begins.
  37.       keyDelay: 400, //  The delay after a keydown on the jSuggest input field and before search is started.
  38.       resultsHighlight: true, // Option to choose whether or not to highlight the matched text in each result item.
  39.       selectionLimit: false, // Limits the number of selections that are allowed.
  40.       showResultList: true, // If set to false, the Results Dropdown List will never be shown at any time.
  41.       selectionClick: function(elem){}, // Custom function that is run when a previously chosen item is clicked.
  42.       selectionAdded: function(elem, data){}, // Custom function that is run when an item is added to the items holder.
  43.       selectionRemoved: function(elem, data){ elem.remove(); }, // Custom function that is run when an item is removed from the items holder.
  44.       spotFirst: true,
  45.       formatList: false, // Custom function that is run after all the data has been retrieved and before the results are put into the suggestion results list.
  46.       beforeRetrieve: function(string){ return string; }, // Custom function that is run before the AJAX request is made, or the local objected is searched.
  47.       retrieveComplete: function(data){ return data; },
  48.       resultClick: function(data){}, // Custom function that is run when a search result item is clicked.
  49.       resultsComplete: function(){} // Custom function that is run when the suggestion results dropdown list is made visible.
  50.     };  
  51.    
  52.     // Merge the options passed with the defaults.
  53.     var opts = $.extend(defaults, options);    
  54.    
  55.     // Get the data type of the source.
  56.     var dType = typeof opts.source;
  57.  
  58.     // Ensure that the source is either an object or a string.
  59.     if (dType === 'object' || dType === 'string'){
  60.      
  61.       return this.each(function(x) {
  62.      
  63.         //If there is not a uniqID, build a ramdom ID so we can call this plugin multiple times.
  64.         x = !opts.uniqID ? x+""+Math.floor(Math.random()*100) : x = opts.uniqID;
  65.         var xID = !opts.uniqID ? "as-input-"+x : x;
  66.          
  67.         // Get the text input and tune it up.
  68.         var input = $(this);
  69.        
  70.         input.attr('autocomplete', 'off').addClass('as-input').attr('id', xID).val(opts.startText);
  71.        
  72.         // Setup basic elements and render them to the DOM.
  73.         input.wrap('<ul class="as-selections" id="as-selections-'+x+'"></ul>').wrap('<li class="as-original" id="as-original-'+x+'"></li>');
  74.        
  75.         // UL that acts as a global container of the selected items and the orgLI.
  76.         var itemsHolder = $('#as-selections-'+x);
  77.        
  78.         // LI element that holds the text input and the hidden input.
  79.         var orgLI = $('#as-original-'+x);
  80.          
  81.         // Div that holds each result or message inside the resultsUL.
  82.         var resultsHolder = $('<div class="as-results" id="as-results-'+x+'"></div>').hide();
  83.        
  84.         // UL where all search results and messages are placed.
  85.         var resultsUL =  $('<ul class="as-list"></ul>').css('width', itemsHolder.outerWidth()).appendTo(resultsHolder);
  86.        
  87.         // Hidden input where all selected items are placed.
  88.         var hiddenInput = $('<input type="hidden" class="as-values" name="as_values_'+x+'" id="as-values-'+x+'" />');
  89.        
  90.         // Get the query limit value.
  91.         var qLimit = opts.queryLimit;
  92.        
  93.         // Get the selection limit value.
  94.         var sLimit = opts.selectionLimit;
  95.  
  96.         //set up hover detection to workaround IE8 and lower deficiencies (bkwdesign)
  97.     resultsHolder.hover(  
  98.            function () {    
  99.              $(this).addClass("bkhover");  
  100.            },  
  101.            function () {    
  102.              $(this).removeClass("bkhover");  
  103.            }
  104.         );
  105.        
  106.         // If the preFill source is a string.
  107.         if (opts.preFill && typeof opts.preFill === 'object') {
  108.  
  109.           // Set up a prefill counter.
  110.           var pCount = 0;
  111.  
  112.           // Get the number of elements inside the prefill object.
  113.           for (var k in opts.preFill) if (opts.preFill.hasOwnProperty(k)) pCount++;
  114.            
  115.           // If we got at least one element.
  116.           if (pCount > 0) {
  117.  
  118.             // Take into account the selection limit when adding new items.
  119.             if (sLimit && pCount > sLimit) { pCount = sLimit; }
  120.  
  121.             // Add each value of the prefill object.
  122.             for(var n=0; n<pCount; n++) { addItem(opts.preFill[n], "000"+n); }
  123.            
  124.             // Remove the start text and set up the styles.
  125.             $('li.as-selection-item', itemsHolder).addClass('blur').removeClass('selected');
  126.             input.val('');
  127.      
  128.           }
  129.      
  130.         }
  131.        
  132.         // Insert the selected values hidden input in the DOM.
  133.         input.after(hiddenInput);
  134.        
  135.         // When the jSuggest container is clicked trigger the focus() event on the input.
  136.         itemsHolder.click(function() {
  137.           input.focus();
  138.         }).after(resultsHolder);  
  139.  
  140.         var timeout = null;
  141.        
  142.         // Handle input field events.
  143.         input.focus(function(){
  144.          
  145.           // Remove the startText if we click on the input.
  146.           if (input.val() === opts.startText && hiddenInput.val() === '') { input.val(''); }
  147.            
  148.           // When the input is active, highlight the selections by removing the 'blur' class.
  149.           $("li.as-selection-item", itemsHolder).removeClass('blur');
  150.          
  151.           // Show the results list if there is a value in the input.
  152.           if (input.val() !== ''){ resultsHolder.show(); }
  153.          
  154.         }).blur(function() { // When we loose the focus.
  155.          
  156.           // If no selections where made, show startText again.
  157.           if (input.val() === '' && hiddenInput.val() === ''){ input.val(opts.startText); }
  158.          
  159.           // Set the input's width back.
  160.           input.width(input.val().length * 8 + 30);
  161.  
  162.           // If the user is no longer manipulating the results list, hide it.
  163.           if (!(resultsHolder.hasClass('bkhover'))){
  164.             $('li.as-selection-item', itemsHolder).addClass('blur').removeClass('selected');
  165.             resultsHolder.hide();
  166.           }
  167.          
  168.         }).keydown(function(e) { // The user is typing on the input.
  169.  
  170.          
  171.           // Track last key pressed.
  172.           lastKey = e.keyCode;
  173.  
  174.          
  175.           switch(lastKey) {
  176.            
  177.             // Up / Down arrow keys pressed.
  178.             case 38: case 40:
  179.              
  180.               e.preventDefault();
  181.              
  182.               if (lastKey === 38) spotResult('up'); else spotResult('down');
  183.              
  184.               break;
  185.  
  186.             // Delete key pressed.
  187.             case 8:
  188.          
  189.               // If the input field is empty.
  190.               if (input.val() === '') {
  191.  
  192.                 // If there was a previous item with the 'selected' class, remove it.
  193.                 itemsHolder.children().not(orgLI.prev()).removeClass('selected');
  194.  
  195.                 // If the last item is already selected, trigger the close click event on that item.
  196.                 if (orgLI.prev().hasClass('selected')) { orgLI.prev().find('.as-close').click(); }
  197.  
  198.                 // Else, select the last item, and call the selectionClick custom function.
  199.                 else {
  200.                
  201.                   opts.selectionClick.call(this, orgLI.prev());
  202.                   orgLI.prev().addClass('selected');    
  203.                
  204.                 }
  205.                
  206.               }
  207.  
  208.               // Remove the last char from the input and hide the results list.
  209.               if (input.val().length === 1){ resultsHolder.hide(); }
  210.  
  211.               // If there is at least one visible item in the results list.
  212.               if ($(':visible', resultsHolder).length > 0){
  213.  
  214.                 // Take into account the keyDelay before showing the results again.
  215.                 if (timeout){ clearTimeout(timeout); }
  216.                 timeout = setTimeout(function(){ keyChange(); }, opts.keyDelay);
  217.  
  218.               }
  219.  
  220.               break;
  221.  
  222.             // Tab or comma keys pressed.
  223.             case 9: case 188: case 13:
  224.  
  225.             // Prevent default behaviour if the comma or return keys are pressed to avoid submiting the form which jSuggest is part of.
  226.             /*if (lastKey === 13 || lastKey === 188) */e.preventDefault();
  227.  
  228.             var nInput = input.val().replace(/(,)/g, '');
  229.             if (nInput !== '' && hiddenInput.val().search(nInput + ',') < 0 && nInput.length >= opts.minChars) {
  230.  
  231.               // If the tab or return keys are pressed when an result item is active, add it.
  232.               if ((lastKey === 9 || lastKey === 13) && $('li.as-result-item:visible', resultsHolder).length > 0) { $('li.active:first', resultsUL).click(); }
  233.  
  234.               // The tab or return keys where pressed when no results where found.
  235.               else {
  236.  
  237.                 // If adding new items is allowed.
  238.                 if (opts.newItem) {
  239.  
  240.                   // If we still are in within the number of items allowed.
  241.                   if (sLimit && $('li', itemsHolder).length <= sLimit) {
  242.  
  243.                     // Get the custom formated object from the new item function.
  244.                     var nData = opts.newItem.call(this, nInput);
  245.  
  246.                     // Generate a custom number identifier for the new item.
  247.                     var lis = $('li', itemsHolder).length;
  248.  
  249.                     // Add the new item.
  250.                     addItem(nData, '00' + (lis+1));
  251.  
  252.                     // Hide the results list.
  253.                     resultsHolder.hide();
  254.  
  255.                     // Reset the text input.
  256.                     input.val('');
  257.  
  258.                   }
  259.                   // Show the message that alerts we cannot add more items.
  260.                   else { resultsUL.html('<li class="as-message">'+opts.limitText+'</li>').show(); }
  261.  
  262.                 }
  263.                 // Show the newText message.
  264.                 else{ resultsUL.html('<li class="as-message">'+opts.newText+'</li>').show(); }
  265.  
  266.               }
  267.  
  268.  
  269.             }
  270.  
  271.             break;
  272.  
  273.             // Any other key is pressed.
  274.             default:
  275.               if (opts.showResultList) {
  276.                 if (sLimit && $("li.as-selection-item", itemsHolder).length == sLimit) {
  277.                   resultsUL.html('<li class="as-message">'+opts.limitText+'</li>');
  278.  
  279.                   // Show the results list.
  280.                   resultsHolder.show();
  281.                 }
  282.                 else {
  283.                   // Take into account the keyDelay before showing the results again.
  284.                   if (timeout) { clearTimeout(timeout); }
  285.                   timeout = setTimeout(function(){ keyChange(); }, opts.keyDelay);
  286.                 }
  287.               }
  288.  
  289.               break;
  290.           }
  291.  
  292.         }).keyup(function() {
  293.  
  294.           // Dynamically set the input width for a better user experience.
  295.           input.width(input.val().length * 8 + 30);
  296.          
  297.         });
  298.        
  299.         // Function that is executed when typing and after the key delay timeout.
  300.         function keyChange() {
  301.          
  302.           // ignore if the following keys are pressed: [del] [shift] [capslock]
  303.           if ( lastKey == 46 || (lastKey > 8 && lastKey < 32) ){ return resultsHolder.hide(); }
  304.  
  305.           // Get the text from the input.
  306.           var string = input.val().replace(/[\\]+|[\/]+/g,"");
  307.  
  308.           // If we passed the min chars limit, proceed.
  309.           if (string.length >= opts.minChars) {
  310.             itemsHolder.addClass('loading');
  311.  
  312.             // This variable will hold the object from the source to be processed or the URL to get the values via an ajax request.
  313.             var theData = opts.source;
  314.  
  315.             // This counter is to get the number of values inside the source.
  316.             var dCount = 0;
  317.  
  318.             // Call the custom beforeRetrieve function.
  319.             if (opts.beforeRetrieve){ string = opts.beforeRetrieve.call(this, string); }
  320.  
  321.             // If the data is a URL, retrieve the results from it. Else, the data is an object, retrieve the results directly from the source.
  322.             if (dType === 'string'){
  323.  
  324.               // Set up the limit of the query.
  325.               var limit = qLimit ? "&limit="+encodeURIComponent(qLimit) : '';
  326.  
  327.               // Build the query and retrieve the response in JSON format.
  328.               $.getJSON(theData+"?"+opts.queryParam+"="+encodeURIComponent(string)+limit+opts.extraParams, function(rData){ theData = rData; });
  329.  
  330.             }
  331.  
  332.             // Call the custom retrieveComplete function.
  333.             theData = opts.retrieveComplete.call(this, theData);
  334.  
  335.             // Count the number of items inside the object.
  336.             for (var k in theData) if (theData.hasOwnProperty(k)) dCount++;
  337.  
  338.             // Send the object, the number of items inside and the string to processData function.
  339.             processData(theData, string, dCount);
  340.  
  341.           }
  342.  
  343.           // We don't have the min chars required.
  344.           else {
  345.             itemsHolder.removeClass('loading');
  346.             resultsHolder.hide();
  347.           }
  348.  
  349.         }
  350.        
  351.          
  352.         function processData(data, query, counter) {
  353.          
  354.           var matchCount = 0, num, str, name;
  355.  
  356.           resultsHolder.html(resultsUL.html('')).hide();
  357.          
  358.           // Loop the data to get an index of each element.
  359.           for(var i=0; i<counter; i++) {
  360.  
  361.             num = i;
  362.                  
  363.             str = '';
  364.            
  365.             // Get the properties which the user wants to search with.
  366.             var props = opts.seekVal.split(',');
  367.            
  368.             for (var y=0; y<props.length; y++) {
  369.              
  370.               name = $.trim(props[y]);
  371.              
  372.               str = str+data[num][name];
  373.        
  374.             }
  375.          
  376.             // If not required, ignore the case sensitive search.
  377.             if (!opts.matchCase) { str = str.toLowerCase(); query = query.toLowerCase(); }
  378.            
  379.             // If the search returned at least one result, and that result is not already selected.
  380.             if (str.search(query) !== -1 && hiddenInput.val().search(data[num][opts.selectProp]+',') === -1) {
  381.              
  382.               // Build each result li element to show on the results list, and bind the click event as a way to select results.
  383.               var resultLI = $('<li class="as-result-item" id="as-result-item-'+num+'"></li>').click(function() {
  384.                 var rawData = $(this).data('data');
  385.                 var number = rawData.num;
  386.                 if ($('#as-selection-'+number, itemsHolder).length <= 0) {
  387.                   var data = rawData.attributes;
  388.                   input.val('').focus();
  389.  
  390.                   // Add the clicked result as a new item.
  391.                   addItem(data, number);
  392.  
  393.                   // Call the custom resultClick event.
  394.                   opts.resultClick.call(this, rawData);
  395.  
  396.                   // Hide the results list.
  397.                   resultsHolder.hide();
  398.                 }
  399.               }).mouseover(function() { // When the mouse is over a suggestion, spot it.
  400.                 $('li', resultsUL).removeClass('active');
  401.                 $(this).addClass('active');
  402.               }).data('data',{attributes: data[num], num: num+1});
  403.  
  404.               var thisData = $.extend({}, data[num]);
  405.  
  406.               // Make the suggestions case sensitive or not.
  407.               var cType = !opts.matchCase ? 'gi' : 'g';
  408.               var regx = new RegExp('(?![^&;]+;)(?!<[^<>]*)(' + query + ')(?![^<>]*>)(?![^&;]+;)', ''+ cType + '');
  409.  
  410.               // Highlight the results if the option is set to true.
  411.               if (opts.resultsHighlight) {
  412.                 thisData[opts.selectedItemProp] = thisData[opts.selectedItemProp].replace(regx,"<em>$1</em>");
  413.               }
  414.  
  415.               // Call the custom formatList event if it exists.
  416.               resultLI = !opts.formatList ? resultLI.html(thisData[opts.selectedItemProp]) : opts.formatList.call(this, thisData, resultLI);
  417.  
  418.               // Add the LI element to the results list.
  419.               resultsUL.append(resultLI);
  420.               delete thisData;
  421.  
  422.               // Increment the results counter after each result is added to the results list.
  423.               matchCount++;
  424.  
  425.               // Check if we reached the limit of results to show.
  426.               if (qLimit && qLimit == matchCount ){ break; }
  427.            
  428.             }  
  429.          
  430.           }
  431.      
  432.           itemsHolder.removeClass('loading');
  433.  
  434.           // If no results were found, show the empty text message.
  435.           if (matchCount <= 0){ resultsUL.html('<li class="as-message">'+opts.emptyText+'</li>'); }
  436.  
  437.           // Show the results list.
  438.           resultsHolder.show();
  439.  
  440.           // Set the first result with the 'active' class if required.
  441.           if (opts.spotFirst) { spotResult('down'); }
  442.  
  443.           // Call the custom resultsComplete function.
  444.           opts.resultsComplete.call(this);
  445.        
  446.         }
  447.        
  448.         function addItem(data, num) {
  449.          
  450.           // Add to the hidden input the seleced values property from the passed data.
  451.           hiddenInput.val(hiddenInput.val()+data[opts.selectProp]+',');
  452.  
  453.           // If a selected item is clicked, add the selected class and call the custom selectionClick function.
  454.           var item = $('<li class="as-selection-item" id="as-selection-'+num+'"></li>').click(function() {
  455.          
  456.             opts.selectionClick.call(this, $(this));
  457.             itemsHolder.children().removeClass('selected');
  458.             $(this).addClass('selected');
  459.          
  460.           });
  461.  
  462.           // If the close cross is clicked,
  463.           var close = $('<a class="as-close">x</a>').click(function() {
  464.  
  465.             // Remove the item from the hidden input.
  466.             hiddenInput.val(hiddenInput.val().replace(data[opts.selectProp]+',',''));
  467.  
  468.             // Call the custom selectionRemoved function.
  469.             opts.selectionRemoved.call(this, item, data);
  470.             input.focus();
  471.             return false;
  472.  
  473.           });
  474.  
  475.           // Insert the item with the selectedItemProp as text and the close cross.
  476.           orgLI.before(item.html(data[opts.selectedItemProp]).prepend(close));
  477.  
  478.           // Call the custom selectionAdded function with the recently added item as elem and its associated data.
  479.           opts.selectionAdded.call(this, orgLI.prev(), data);
  480.  
  481.         }
  482.        
  483.        
  484.         // Function that handles the up & down key press events to select the results.
  485.         function spotResult(dir) {
  486.  
  487.           // If there is at least one visible item in the results list.
  488.           if ($(':visible', resultsHolder).length > 0) {
  489.          
  490.             // Get all the LI elements from the results list.
  491.             var lis = $('li', resultsHolder);
  492.  
  493.             // If the direction is 'down' spot the first result. If it is 'up', spot the last result.
  494.             var spot = dir === 'down' ? lis.eq(0) : lis.filter(':last');
  495.  
  496.             // If a LI element was already spoted, take it as the base for future movements.
  497.             var active = $('li.active:first', resultsHolder);
  498.             if (active.length > 0){ spot = dir === 'down' ? active.next() : active.prev(); }
  499.  
  500.             // Set the 'active' class to the current result item.
  501.             lis.removeClass('active');
  502.             spot.addClass('active');
  503.  
  504.           }
  505.          
  506.         }
  507.      
  508.       });
  509.    
  510.     }
  511.  
  512.   };
  513.  
  514. })(jQuery);
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement