View difference between Paste ID: nwAX5kcj and PQkPWfeT
SHOW: | | - or go back to the newest paste.
1
/*
2
* jSuggest
3-
* Version 0.8 | Updated: Apr. 11, 2011
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);