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); |