1/*!
2* jQuery FlexBox $Version: 0.9.6 $
3*
4* Copyright (c) 2008-2010 Noah Heldman and Fairway Technologies (http://www.fairwaytech.com/flexbox)
5* Licensed under Ms-PL (http://www.codeplex.com/flexbox/license)
6*
7* $Date: 2010-11-24 01:02:00 PM $
8* $Rev: 0.9.6.1 $
9*/
10(function($) {
11    $.flexbox = function(div, o) {
12
13        // TODO: in straight type-ahead mode (showResults: false), if noMatchingResults, dropdown appears after new match
14        // TODO: consider having options.mode (select, which replaces html select; combobox; suggest; others?)
15        // TODO: on resize (at least when wrapping within a table), the arrow is pushed down to the next line
16        // TODO: check for boundary/value problems (such as minChars of -1) and alert them
17        // TODO: add options for advanced paging template
18        // TODO: general cleanup and refactoring, commenting
19        // TODO: detailed Exception handling, logging
20        // TODO: FF2, up arrow from bottom has erratic scroll behavior (if multiple flexboxes on page)
21        // TODO: FF2 (and maybe IE7): if maxVisibleRows == number of returned rows, height is a bit off (maybe set to auto?)
22        // TODO: escape key only works from input box (this might be okay)
23        // TODO: make .getJSON parameters (object and callback function) configurable (e.g. when calling yahoo image search)
24        // TODO: escape key reverts to previous value (FF only?) (is this a good thing?)
25
26		// TEST: highlightMatches uses the case of whatever you typed in to replace the match string, which can look funny
27        // TEST: handle pageDown and pageUp keys when scrolling through results
28        // TEST: allow client-side paging (return all data initially, set paging:{pageSize:#}, and ensure maxCacheBytes is > 0)
29        // TEST: accept json object as first parameter to flexbox instead of page source, and have it work like a combobox
30        // TEST: implement no results template
31        // TEST: implement noResultsText and class
32        // TEST: watermark color should be configurable (and so should default input color)
33        // TEST: exception handling and alerts for common mistakes
34        // TEST: first example should use defaults ONLY
35        // TEST: add property initialValue, so you can set it when the flexbox loads
36        // TEST: handle hidden input value for form submissions
37        // TEST: how can we allow programmatically setting the field value (and therefore hidden value).  add jquery function?
38        // TEST: use pageSize parameter as threshold to switch from no paging to paging based on results
39        // TEST: if you type in an input value that matches the html, it might display html code (try typing "class" in the input box)
40        // TEST: don't require all paging subprops (let default override)
41        // TEST: when tabbing from one ffb to another, the previous ffb results flash...
42        // TEST: IE7: when two non-paging ffbs right after each other, with only a clear-both div between them, the bottom ffb jumps down when selecting a value, then jumps back up on mouseover
43        // TEST: FF2, make sure we scroll to top before showing results (maxVisibleRows only)
44        // TEST: if maxVisibleRows is hiding the value the user types in to the input, scroll to that value (is this even possible?)
45        // TEST: make sure caching supports multiple ffbs uniquely
46        // TEST: when entering a number in the paging input box, the results are displayed twice
47
48        var timeout = false, 	// hold timeout ID for suggestion results to appear
49        cache = [], 		    // simple array with cacheData key values, MRU is the first element
50        cacheData = [],         // associative array holding actual cached data
51        cacheSize = 0, 		    // size of cache in bytes (cache up to o.maxCacheBytes bytes)
52        delim = '\u25CA',       // use an obscure unicode character (lozenge) as the cache key delimiter
53        str_scroll_click = false,
54        scrolling = false,
55        pageSize = o.paging && o.paging.pageSize ? o.paging.pageSize : 0,
56		retrievingRemoteData = false,
57        $div = $(div).css('position', 'relative').css('z-index', 0);
58
59        // The hiddenField MUST be appended to the div before the input, or IE7 does not shift the dropdown below the input field (it overlaps)
60        var $hdn = $('<input type="hidden"/>')
61            .attr('id', $div.attr('id') + '_hidden')
62            .attr('name', $div.attr('id'))
63            .val(o.initialValue)
64            .appendTo($div);
65        var $input = $('<input/>')
66            .attr('id', $div.attr('id') + '_input')
67            .attr('autocomplete', 'off')
68            .addClass(o.inputClass)
69            .css('width', o.width + 'px')
70            .appendTo($div)
71            .click(function(e) {
72                if (o.watermark !== '' && this.value === o.watermark)
73                    this.value = '';
74                else
75                    this.select();
76            })
77            .focus(function(e) {
78                $(this).removeClass('watermark');
79            })
80            .blur(function(e) {
81				if (this.value === '') $hdn.val('');
82                setTimeout(function() {
83                    if (!$input.data('active') && !str_scroll_click) {
84                        str_scroll_click = false;
85                        hideResults();
86                    }
87                }, 200);
88            })
89            .keydown(processKeyDown);
90
91        if (o.initialValue !== '')
92            $input.val(o.initialValue).removeClass('watermark');
93        else
94            $input.val(o.watermark).addClass('watermark');
95
96        var arrowWidth = 0;
97        if (o.showArrow && o.showResults) {
98            var arrowClick = function() {
99                if ($ctr.is(':visible')) {
100                    hideResults();
101                }
102                else {
103                    $input.focus();
104                    if (o.watermark !== '' && $input.val() === o.watermark)
105                        $input.val('');
106                    else
107                        $input.select();
108                    if (timeout)
109                        clearTimeout(timeout);
110                    timeout = setTimeout(function() { flexbox(1, true, o.arrowQuery); }, o.queryDelay);
111                }
112            };
113            var $arrow = $('<span></span>')
114                .attr('id', $div.attr('id') + '_arrow')
115                .addClass(o.arrowClass)
116                .addClass('out')
117                .hover(function() {
118                    $(this).removeClass('out').addClass('over');
119                }, function() {
120                    $(this).removeClass('over').addClass('out');
121                })
122                .mousedown(function() {
123                    $(this).removeClass('over').addClass('active');
124                })
125                .mouseup(function() {
126                    $(this).removeClass('active').addClass('over');
127                })
128                .click(arrowClick)
129                .appendTo($div);
130            arrowWidth = $arrow.width();
131            $input.css('width', (o.width - arrowWidth) + 'px');
132        }
133        if (!o.allowInput) { o.selectFirstMatch = false; $input.click(arrowClick); } // simulate <select> behavior
134
135        // Handle presence of CSS Universal Selector (*) that defines padding by verifying what the browser thinks the outerHeight is.
136        // In FF, the outerHeight() will not pick up the correct input field padding
137        var inputPad = $input.outerHeight() - $input.height() - 2;
138        var inputWidth = $input.outerWidth() - 2;
139        var top = $input.outerHeight();
140
141        if (inputPad === 0) {
142            inputWidth += 4;
143            top += 4;
144        }
145        else if (inputPad !== 4) {
146            inputWidth += inputPad;
147            top += inputPad;
148        }
149
150        var $ctr = $('<div></div>')
151            .attr('id', $div.attr('id') + '_ctr')
152            .css('width', inputWidth + arrowWidth)
153            .css('top', top)
154            .css('left', 0)
155            .addClass(o.containerClass)
156            .appendTo($div)
157			.mousedown(function(e) {
158				//$input.data('active', true);
159                str_scroll_click = true;
160			})
161            .mouseout(function(e) {
162                //alert("mouseout");
163				//$input.data('active', true);
164                str_scroll_click = false;
165                $input.focus();
166			})
167            .hide();
168
169        var $content = $('<div></div>')
170            .addClass(o.contentClass)
171            .appendTo($ctr)
172            .scroll(function() {
173                $input.data('active', false);
174                scrolling = true;
175            });
176
177        var $paging = $('<div></div>').appendTo($ctr);
178		$div.css('height', $input.outerHeight());
179
180        function processKeyDown(e) {
181            // handle modifiers
182            var mod = 0;
183            if (typeof (e.ctrlKey) !== 'undefined') {
184                if (e.ctrlKey) mod |= 1;
185                if (e.shiftKey) mod |= 2;
186            } else {
187                if (e.modifiers & Event.CONTROL_MASK) mod |= 1;
188                if (e.modifiers & Event.SHIFT_MASK) mod |= 2;
189            }
190            // if the keyCode is one of the modifiers, bail out (we'll catch it on the next keypress)
191            if (/16$|17$/.test(e.keyCode)) return; // 16 = Shift, 17 = Ctrl
192
193            var tab = e.keyCode === 9, esc = e.keyCode === 27;
194            var tabWithModifiers = e.keyCode === 9 && mod > 0;
195            var backspace = e.keyCode === 8; // we will end up extending the delay time for backspaces...
196
197            // tab is a special case, since we want to bubble events...
198            if (tab) if (getCurr()) selectCurr();
199
200            // handling up/down/escape/right arrow/left arrow requires results to be visible
201            // handling enter requires that AND a result to be selected
202            if ((/27$|38$|33$|34$/.test(e.keyCode) && $ctr.is(':visible')) ||
203				(/13$|40$/.test(e.keyCode)) || !o.allowInput) {
204
205                if (e.preventDefault) e.preventDefault();
206                if (e.stopPropagation) e.stopPropagation();
207
208                e.cancelBubble = true;
209                e.returnValue = false;
210
211                switch (e.keyCode) {
212                    case 38: // up arrow
213                        prevResult();
214                        break;
215                    case 40: // down arrow
216                        if ($ctr.is(':visible')) nextResult();
217                        else flexboxDelay(true);
218                        break;
219                    case 13: // enter
220                        if (getCurr()) selectCurr();
221                        else flexboxDelay(true);
222                        break;
223                    case 27: //	escape
224                        hideResults();
225                        break;
226                    case 34: // page down
227						if (!retrievingRemoteData) {
228							if (o.paging) $('#' + $div.attr('id') + 'n').click();
229							else nextPage();
230						}
231                        break;
232                    case 33: // page up
233						if (!retrievingRemoteData) {
234							if (o.paging) $('#' + $div.attr('id') + 'p').click();
235							else prevPage();
236						}
237                        break;
238                    default:
239                        if (!o.allowInput) { return; }
240                }
241            } else if (!esc && !tab && !tabWithModifiers) { // skip esc and tab key and any modifiers
242                flexboxDelay(false, backspace);
243            }
244        }
245
246        function flexboxDelay(simulateArrowClick, increaseDelay) {
247            if (timeout) clearTimeout(timeout);
248            var delay = increaseDelay ? o.queryDelay * 5 : o.queryDelay;
249            timeout = setTimeout(function() { flexbox(1, simulateArrowClick, ''); }, delay);
250        }
251
252        function flexbox(p, arrowOrPagingClicked, prevQuery) {
253            if (arrowOrPagingClicked) prevQuery = '';
254            var q = prevQuery && prevQuery.length > 0 ? prevQuery : $.trim($input.val());
255
256            if (q.length >= o.minChars || arrowOrPagingClicked) {
257				// If we are getting data from the server, set the height of the content box so it doesn't shrink when navigating between pages, due to the $content.html('') below...
258				if ($content.outerHeight() > 0)
259					$content.css('height', $content.outerHeight());
260                $content.html('').attr('scrollTop', 0);
261
262                var cached = checkCache(q, p);
263                if (cached) {
264                    if($.browser.msie && $.browser.version.substr(0,1) === '6'){}
265                    else
266                        $content.css('height', 'auto');
267
268                    displayItems(cached.data, q);
269                    showPaging(p, cached.t);
270                }
271                else {
272                    var params = { q: q, p: p, s: pageSize, contentType: 'application/json; charset=utf-8' };
273                    var callback = function(data, overrideQuery) {
274                        if (overrideQuery === true) q = overrideQuery; // must compare to boolean because by default, the string value "success" is passed when the jQuery $.getJSON method's callback is called
275                        var totalResults = parseInt(data[o.totalProperty]);
276
277                        // Handle client-side paging, if any paging configuration options were specified
278                        if (isNaN(totalResults) && o.paging) {
279                            if (o.maxCacheBytes <= 0) alert('The "maxCacheBytes" configuration option must be greater\nthan zero when implementing client-side paging.');
280                            totalResults = data[o.resultsProperty].length;
281
282                            var pages = totalResults / pageSize;
283                            if (totalResults % pageSize > 0) pages = parseInt(++pages);
284
285                            for (var i = 1; i <= pages; i++) {
286                                var pageData = {};
287                                pageData[o.totalProperty] = totalResults;
288                                pageData[o.resultsProperty] = data[o.resultsProperty].splice(0, pageSize);
289
290                                if (i === 1) totalSize = displayItems(pageData, q);
291                                updateCache(q, i, pageSize, totalResults, pageData, totalSize);
292                            }
293                        }
294                        else {
295
296                            //var totalSize = displayItems(data, q);
297                            //hideResults();
298                            //var totalSize = displayItems(data, q);
299
300                            if(navigator.userAgent.indexOf("Chrome") != -1 )
301                            {
302                                var totalSize = displayItems(data, q);
303                                hideResults();
304                                var totalSize = displayItems2(data, q);
305                            }
306                            else
307                            {
308                                hideResults();
309                                var totalSize = displayItems(data, q);
310                            }
311                            updateCache(q, p, pageSize, totalResults, data, totalSize);
312                        }
313                        showPaging(p, totalResults);
314                        if($.browser.msie && $.browser.version.substr(0,1) === '6'){}
315                        else
316                            $content.css('height', 'auto');
317						retrievingRemoteData = false;
318                    };
319					if (typeof (o.source) === 'object') {
320						if (o.allowInput) callback(filter(o.source, params));
321						else callback(o.source);
322					}
323					else {
324						retrievingRemoteData = true;
325						if (o.method.toUpperCase() == 'POST') $.post(o.source, params, callback, 'json');
326						else $.getJSON(o.source, params, callback);
327					}
328                }
329            } else
330                hideResults();
331        }
332
333		function filter(data, params) {
334			var filtered = {};
335			filtered[o.resultsProperty] = [];
336			filtered[o.totalProperty] = 0;
337			var index = 0;
338
339			for (var i=0; i < data[o.resultsProperty].length; i++) {
340				var indexOfMatch = data[o.resultsProperty][i][o.displayValue].toLowerCase().indexOf(params.q.toLowerCase());
341				if ((o.matchAny && indexOfMatch !== -1) || (!o.matchAny && indexOfMatch === 0)) {
342					filtered[o.resultsProperty][index++] = data[o.resultsProperty][i];
343					filtered[o.totalProperty] += 1;
344				}
345			}
346			if (o.paging) {
347				var start = (params.p - 1) * params.s;
348				var howMany = (start + params.s) > filtered[o.totalProperty] ? filtered[o.totalProperty] - start : params.s;
349				filtered[o.resultsProperty] = filtered[o.resultsProperty].splice(start, howMany);
350			}
351			return filtered;
352		}
353
354        function showPaging(p, totalResults) {
355            $paging.html('').removeClass(o.paging.cssClass); // clear out for threshold scenarios
356            if (o.showResults && o.paging && totalResults > pageSize) {
357                var pages = totalResults / pageSize;
358                if (totalResults % pageSize > 0) pages = parseInt(++pages);
359                outputPagingLinks(pages, p, totalResults);
360            }
361        }
362
363        function handleKeyPress(e, page, totalPages) {
364            if (/^13$|^39$|^37$/.test(e.keyCode)) {
365                if (e.preventDefault)
366                    e.preventDefault();
367                if (e.stopPropagation)
368                    e.stopPropagation();
369
370                e.cancelBubble = true;
371                e.returnValue = false;
372
373                switch (e.keyCode) {
374                    case 13: // Enter
375                        if (/^\d+$/.test(page) && page > 0 && page <= totalPages)
376                            flexbox(page, true);
377                        else
378                            alert('Please enter a page number between 1 and ' + totalPages);
379                        // TODO: make this alert a function call, and a customizable parameter
380                        break;
381                    case 39: // right arrow
382                        $('#' + $div.attr('id') + 'n').click();
383                        break;
384                    case 37: // left arrow
385                        $('#' + $div.attr('id') + 'p').click();
386                        break;
387                }
388            }
389        }
390
391        function handlePagingClick(e) {
392            flexbox(parseInt($(this).attr('page')), true, $input.attr('pq')); // pq == previous query
393            return false;
394        }
395
396        function outputPagingLinks(totalPages, currentPage, totalResults) {
397            // TODO: make these configurable images
398            var first = '&lt;&lt;',
399            prev = '&lt;',
400            next = '&gt;',
401            last = '&gt;&gt;',
402            more = '...';
403
404            $paging.addClass(o.paging.cssClass);
405
406            // set up our base page link element
407            var $link = $('<a/>')
408                .attr('href', '#')
409                .addClass('page')
410                .click(handlePagingClick),
411            $span = $('<span></span>').addClass('page'),
412            divId = $div.attr('id');
413
414            // show first page
415            if (currentPage > 1) {
416                $link.clone(true).attr('id', divId + 'f').attr('page', 1).html(first).appendTo($paging);
417                $link.clone(true).attr('id', divId + 'p').attr('page', currentPage - 1).html(prev).appendTo($paging);
418            }
419            else {
420                $span.clone(true).html(first).appendTo($paging);
421                $span.clone(true).html(prev).appendTo($paging);
422            }
423
424            if (o.paging.style === 'links') {
425                var maxPageLinks = o.paging.maxPageLinks;
426                // show page numbers
427                if (totalPages <= maxPageLinks) {
428                    for (var i = 1; i <= totalPages; i++) {
429                        if (i === currentPage) {
430                            $span.clone(true).html(currentPage).appendTo($paging);
431                        }
432                        else {
433                            $link.clone(true).attr('page', i).html(i).appendTo($paging);
434                        }
435                    }
436                }
437                else {
438                    if ((currentPage + parseInt(maxPageLinks / 2)) > totalPages) {
439                        startPage = totalPages - maxPageLinks + 1;
440                    }
441                    else {
442                        startPage = currentPage - parseInt(maxPageLinks / 2);
443                    }
444
445                    if (startPage > 1) {
446                        $link.clone(true).attr('page', startPage - 1).html(more).appendTo($paging);
447                    }
448                    else {
449                        startPage = 1;
450                    }
451
452                    for (var i = startPage; i < startPage + maxPageLinks; i++) {
453                        if (i === currentPage) {
454                            $span.clone(true).html(i).appendTo($paging);
455                        }
456                        else {
457                            $link.clone(true).attr('page', i).html(i).appendTo($paging);
458                        }
459                    }
460
461                    if (totalPages > (startPage + maxPageLinks)) {
462                        $link.clone(true).attr('page', i).html(more).appendTo($paging);
463                    }
464                }
465            }
466            else if (o.paging.style === 'input') {
467                var $pagingBox = $('<input/>')
468                    .addClass('box')
469                    .click(function(e) {
470                        this.select();
471                    })
472                    .keypress(function(e) {
473                        return handleKeyPress(e, this.value, totalPages);
474                    })
475                    .val(currentPage)
476                    .appendTo($paging);
477            }
478
479            if (currentPage < totalPages) {
480                $link.clone(true).attr('id', divId + 'n').attr('page', +currentPage + 1).html(next).appendTo($paging);
481                $link.clone(true).attr('id', divId + 'l').attr('page', totalPages).html(last).appendTo($paging);
482            }
483            else {
484                $span.clone(true).html(next).appendTo($paging);
485                $span.clone(true).html(last).appendTo($paging);
486            }
487            var startingResult = (currentPage - 1) * pageSize + 1;
488            var endingResult = (startingResult > (totalResults - pageSize)) ? totalResults : startingResult + pageSize - 1;
489
490            if (o.paging.showSummary) {
491                var summaryData = {
492                    "start": startingResult,
493                    "end": endingResult,
494                    "total": totalResults,
495                    "page": currentPage,
496                    "pages": totalPages
497                };
498                var html = o.paging.summaryTemplate.applyTemplate(summaryData);
499                $('<br/>').appendTo($paging);
500                $('<span></span>')
501                    .addClass(o.paging.summaryClass)
502                    .html(html)
503                    .appendTo($paging);
504            }
505        }
506
507        function checkCache(q, p) {
508            var key = q + delim + p; // use null character as delimiter
509            if (cacheData[key]) {
510                for (var i = 0; i < cache.length; i++) { // TODO: is it possible to not loop here?
511                    if (cache[i] === key) {
512                        // pull out the matching element (splice), and add it to the beginning of the array (unshift)
513                        cache.unshift(cache.splice(i, 1)[0]);
514                        return cacheData[key];
515                    }
516                }
517            }
518            return false;
519        }
520
521        function updateCache(q, p, s, t, data, size) {
522            if (o.maxCacheBytes > 0) {
523                while (cache.length && (cacheSize + size > o.maxCacheBytes)) {
524                    var cached = cache.pop();
525                    cacheSize -= cached.size;
526                }
527                var key = q + delim + p; // use null character as delimiter
528                cacheData[key] = {
529                    q: q,
530                    p: p,
531                    s: s,
532                    t: t,
533                    size: size,
534                    data: data
535                }; // add the data to the cache at the hash key location
536                cache.push(key); // add the key to the MRU list
537                cacheSize += size;
538            }
539        }
540
541        function displayItems(d, q) {
542            var totalSize = 0, itemCount = 0;
543            flexbox_opened = true;
544            $('#auto_conn_time').css("position","relative");
545
546            if (!d)
547                return;
548
549			$hdn.val($input.val());
550            if (parseInt(d[o.totalProperty]) === 0 && o.noResultsText && o.noResultsText.length > 0) {
551                $content.addClass(o.noResultsClass).html(o.noResultsText);
552                $ctr.show();
553                return;
554            } else $content.removeClass(o.noResultsClass);
555
556            for (var i = 0; i < d[o.resultsProperty].length; i++) {
557                var data = d[o.resultsProperty][i],
558                result = o.resultTemplate.applyTemplate(data),
559                exactMatch = q === result,
560                selectedMatch = false,
561                hasHtmlTags = false,
562				match = data[o.displayValue];
563
564                if (!exactMatch && o.highlightMatches && q !== '') {
565					var pattern = q,
566					highlightStart = match.toLowerCase().indexOf(q.toLowerCase()),
567                    replaceString = '<span class="' + o.matchClass + '">' + match.substr(highlightStart,q.length) + '</span>';
568                    if (result.match('<(.|\n)*?>')) { // see if the content contains html tags
569                        hasHtmlTags = true;
570                        pattern = '(>)([^<]*?)(' + q + ')((.|\n)*?)(<)'; // TODO: look for a better way
571                        replaceString = '$1$2<span class="' + o.matchClass + '">$3</span>$4$6';
572                    }
573                    result = result.replace(new RegExp(pattern, o.highlightMatchesRegExModifier), replaceString);
574                }
575
576                // write the value of the first match to the input box, and select the remainder,
577                // but only if autoCompleteFirstMatch is set, and there are no html tags in the response
578                if (o.autoCompleteFirstMatch && !hasHtmlTags && i === 0) {
579                    if (q.length > 0 && match.toLowerCase().indexOf(q.toLowerCase()) === 0) {
580                        $input.attr('pq', q); // pq == previous query
581						$hdn.val(data[o.hiddenValue]);
582                        $input.val(data[o.displayValue]);
583                        selectedMatch = selectRange(q.length, $input.val().length);
584                    }
585                }
586
587                if (!o.showResults) return;
588
589                $row = $('<div></div>')
590                    .attr('id', data[o.hiddenValue])
591                    .attr('val', data[o.displayValue])
592                    .addClass('row')
593                    .html(result)
594                    .appendTo($content);
595                    if($.browser.msie && $.browser.version.substr(0,1) === '6'){
596
597                        $row.css('height','auto');}
598
599                if (exactMatch || (++itemCount == 1 && o.selectFirstMatch) || selectedMatch) {
600                    $row.addClass(o.selectClass);
601                }
602                totalSize += result.length;
603            }
604
605            if (totalSize === 0) {
606                hideResults();
607                return;
608            }
609            var scroll_height = $('.scroll-pane')[0].scrollHeight;
610            $ctr.parent().css('z-index', 11000);
611
612            $ctr.show();
613
614            $content
615				.children('div')
616				.mouseover(function() {
617				    $content.children('div').removeClass(o.selectClass);
618				    $(this).addClass(o.selectClass);
619				})
620				.mouseup(function(e) {
621				    e.preventDefault();
622				    e.stopPropagation();
623				    selectCurr();
624				});
625
626            if (o.maxVisibleRows > 0) {
627                var maxHeight = $row.outerHeight() * o.maxVisibleRows;
628
629                if($.browser.msie && $.browser.version.substr(0,1) === '6'){
630
631                    $content.css('height', maxHeight);
632                } else{
633                $content.css('max-height', maxHeight);}
634            }
635
636            return totalSize;
637        }
638
639        function displayItems2(d, q) {
640            var totalSize = 0, itemCount = 0;
641            flexbox_opened = true;
642            $('#auto_conn_time').css("position","relative");
643
644            if (!d)
645                return;
646
647			$hdn.val($input.val());
648            if (parseInt(d[o.totalProperty]) === 0 && o.noResultsText && o.noResultsText.length > 0) {
649                $content.addClass(o.noResultsClass).html(o.noResultsText);
650                $ctr.show();
651                return;
652            } else $content.removeClass(o.noResultsClass);
653
654            for (var i = 0; i < d[o.resultsProperty].length; i++) {
655                var data = d[o.resultsProperty][i],
656                result = o.resultTemplate.applyTemplate(data),
657                exactMatch = q === result,
658                selectedMatch = false,
659                hasHtmlTags = false,
660				match = data[o.displayValue];
661
662                if (!exactMatch && o.highlightMatches && q !== '') {
663					var pattern = q,
664					highlightStart = match.toLowerCase().indexOf(q.toLowerCase()),
665                    replaceString = '<span class="' + o.matchClass + '">' + match.substr(highlightStart,q.length) + '</span>';
666                    if (result.match('<(.|\n)*?>')) { // see if the content contains html tags
667                        hasHtmlTags = true;
668                        pattern = '(>)([^<]*?)(' + q + ')((.|\n)*?)(<)'; // TODO: look for a better way
669                        replaceString = '$1$2<span class="' + o.matchClass + '">$3</span>$4$6';
670                    }
671                    result = result.replace(new RegExp(pattern, o.highlightMatchesRegExModifier), replaceString);
672                }
673
674                // write the value of the first match to the input box, and select the remainder,
675                // but only if autoCompleteFirstMatch is set, and there are no html tags in the response
676                if (o.autoCompleteFirstMatch && !hasHtmlTags && i === 0) {
677                    if (q.length > 0 && match.toLowerCase().indexOf(q.toLowerCase()) === 0) {
678                        $input.attr('pq', q); // pq == previous query
679						$hdn.val(data[o.hiddenValue]);
680                        $input.val(data[o.displayValue]);
681                        selectedMatch = selectRange(q.length, $input.val().length);
682                    }
683                }
684
685                if (!o.showResults) return;
686                if($.browser.msie && $.browser.version.substr(0,1) === '6'){
687                    $row.css('height','auto');}
688
689            }
690            var scroll_height = $('.scroll-pane')[0].scrollHeight;
691            $ctr.parent().css('z-index', 11000);
692            $ctr.show();
693
694            $content
695				.children('div')
696				.mouseover(function() {
697				    $content.children('div').removeClass(o.selectClass);
698				    $(this).addClass(o.selectClass);
699				})
700				.mouseup(function(e) {
701				    e.preventDefault();
702				    e.stopPropagation();
703				    selectCurr();
704				});
705
706            if (o.maxVisibleRows > 0) {
707                var maxHeight = $row.outerHeight() * o.maxVisibleRows;
708
709                if($.browser.msie && $.browser.version.substr(0,1) === '6'){
710
711                    $content.css('height', maxHeight);
712                } else{
713                $content.css('max-height', maxHeight);}
714            }
715
716
717
718            return totalSize;
719        }
720
721        function selectRange(s, l) {
722            var tb = $input[0];
723            if (tb.createTextRange) {
724                var r = tb.createTextRange();
725                r.moveStart('character', s);
726                r.moveEnd('character', l - tb.value.length);
727                r.select();
728            } else if (tb.setSelectionRange) {
729                tb.setSelectionRange(s, l);
730            }
731            tb.focus();
732            return true;
733        }
734
735        String.prototype.applyTemplate = function(d) {
736            try {
737                if (d === '') return this;
738                return this.replace(/{([^{}]*)}/g,
739                    function(a, b) {
740                        var r;
741                        if (b.indexOf('.') !== -1) { // handle dot notation in {}, such as {Thumbnail.Url}
742                            var ary = b.split('.');
743                            var obj = d;
744                            for (var i = 0; i < ary.length; i++)
745                                obj = obj[ary[i]];
746                            r = obj;
747                        }
748                        else
749                            r = d[b];
750                        if (typeof r === 'string' || typeof r === 'number') return r; else throw (a);
751                    }
752                );
753            } catch (ex) {
754                alert('Invalid JSON property ' + ex + ' found when trying to apply resultTemplate or paging.summaryTemplate.\nPlease check your spelling and try again.');
755            }
756        };
757
758        function hideResults() {
759            flexbox_opened = false;
760            $('#auto_conn_time').css("position","");
761            $input.data('active', false); // for input blur
762            $div.css('z-index', 0);
763            $ctr.hide();
764        }
765
766        function getCurr() {
767            if (!$ctr.is(':visible'))
768                return false;
769
770            var $curr = $content.children('div.' + o.selectClass);
771
772            if (!$curr.length)
773                $curr = false;
774
775            return $curr;
776        }
777
778        function selectCurr() {
779            $curr = getCurr();
780
781            if ($curr) {
782				$hdn.val($curr.attr('id'));
783                $input.val($curr.attr('val')).focus();
784                hideResults();
785
786                if (o.onSelect) {
787                    o.onSelect.apply($input[0]);
788                }
789            }
790        }
791
792        function supportsGetBoxObjectFor() {
793            try {
794                document.getBoxObjectFor(document.body);
795                return true;
796            }
797            catch (e) {
798                return false;
799            }
800        }
801
802        function supportsGetBoundingClientRect() {
803            try {
804                document.body.getBoundingClientRect();
805                return true;
806            }
807            catch (e) {
808                return false;
809            }
810        }
811
812        function nextPage() {
813            $curr = getCurr();
814
815            if ($curr && $curr.next().length > 0) {
816                $curr.removeClass(o.selectClass);
817
818                for (var i = 0; i < o.maxVisibleRows; i++) {
819                    if ($curr.next().length > 0) {
820                        $curr = $curr.next();
821                    }
822                }
823
824                $curr.addClass(o.selectClass);
825                var scrollPos = $content.attr('scrollTop');
826                $content.attr('scrollTop', scrollPos + $content.height());
827            }
828            else if (!$curr)
829                $content.children('div:first-child').addClass(o.selectClass);
830        }
831
832        function prevPage() {
833            $curr = getCurr();
834
835            if ($curr && $curr.prev().length > 0) {
836                $curr.removeClass(o.selectClass);
837
838                for (var i = 0; i < o.maxVisibleRows; i++) {
839                    if ($curr.prev().length > 0) {
840                        $curr = $curr.prev();
841                    }
842                }
843
844                $curr.addClass(o.selectClass);
845                var scrollPos = $content.attr('scrollTop');
846                $content.attr('scrollTop', scrollPos - $content.height());
847            }
848            else if (!$curr)
849                $content.children('div:last-child').addClass(o.selectClass);
850        }
851
852        function nextResult() {
853            $curr = getCurr();
854
855            if ($curr && $curr.next().length > 0) {
856                $curr.removeClass(o.selectClass).next().addClass(o.selectClass);
857                var scrollPos = $content.attr('scrollTop'),
858                    curr = $curr[0], parentBottom, bottom, height;
859                if (supportsGetBoxObjectFor()) {
860                    parentBottom = document.getBoxObjectFor($content[0]).y + $content.attr('offsetHeight');
861                    bottom = document.getBoxObjectFor(curr).y + $curr.attr('offsetHeight');
862                    height = document.getBoxObjectFor(curr).height;
863                }
864                else if (supportsGetBoundingClientRect()) {
865                    parentBottom = $content[0].getBoundingClientRect().bottom;
866                    var rect = curr.getBoundingClientRect();
867                    bottom = rect.bottom;
868                    height = bottom - rect.top;
869                }
870                if (bottom >= parentBottom)
871                    $content.attr('scrollTop', scrollPos + height);
872            }
873            else if (!$curr)
874                $content.children('div:first-child').addClass(o.selectClass);
875        }
876
877        function prevResult() {
878            $curr = getCurr();
879
880            if ($curr && $curr.prev().length > 0) {
881                $curr.removeClass(o.selectClass).prev().addClass(o.selectClass);
882                var scrollPos = $content.attr('scrollTop'),
883                curr = $curr[0],
884                parent = $curr.parent()[0],
885                parentTop, top, height;
886                if (supportsGetBoxObjectFor()) {
887                    height = document.getBoxObjectFor(curr).height;
888                    parentTop = document.getBoxObjectFor($content[0]).y - (height * 2); // TODO: this is not working when i add another control...
889                    top = document.getBoxObjectFor(curr).y - document.getBoxObjectFor($content[0]).y;
890                }
891                else if (supportsGetBoundingClientRect()) {
892                    parentTop = parent.getBoundingClientRect().top;
893                    var rect = curr.getBoundingClientRect();
894                    top = rect.top;
895                    height = rect.bottom - top;
896                }
897                if (top <= parentTop)
898                    $content.attr('scrollTop', scrollPos - height);
899            }
900            else if (!$curr)
901                $content.children('div:last-child').addClass(o.selectClass);
902        }
903    };
904
905    $.fn.flexbox = function(source, options) {
906        if (!source)
907            return;
908
909        try {
910            var defaults = $.fn.flexbox.defaults;
911            var o = $.extend({}, defaults, options);
912
913            for (var prop in o) {
914                if (defaults[prop] === undefined) throw ('Invalid option specified: ' + prop + '\nPlease check your spelling and try again.');
915            }
916            o.source = source;
917
918            if (options) {
919                o.paging = (options.paging || options.paging == null) ? $.extend({}, defaults.paging, options.paging) : false;
920
921                for (var prop in o.paging) {
922                    if (defaults.paging[prop] === undefined) throw ('Invalid option specified: ' + prop + '\nPlease check your spelling and try again.');
923                }
924
925                if (options.displayValue && !options.hiddenValue) {
926                    o.hiddenValue = options.displayValue;
927                }
928            }
929
930            this.each(function() {
931                new $.flexbox(this, o);
932            });
933
934            return this;
935        } catch (ex) {
936            if (typeof ex === 'object') alert(ex.message); else alert(ex);
937        }
938    };
939
940    // plugin defaults - added as a property on our plugin function so they can be set independently
941    $.fn.flexbox.defaults = {
942        method: 'GET', // One of 'GET' or 'POST'
943        queryDelay: 100, // num of milliseconds before query is run.
944        allowInput: true, // set to false to disallow the user from typing in queries
945        containerClass: 'ffb',
946        contentClass: 'content',
947        selectClass: 'ffb-sel',
948        inputClass: 'ffb-input',
949        arrowClass: 'ffb-arrow',
950        matchClass: 'ffb-match',
951        noResultsText: 'No matching results', // text to show when no results match the query
952        noResultsClass: 'ffb-no-results', // class to apply to noResultsText
953        showResults: true, // whether to show results at all, or just typeahead
954        selectFirstMatch: true, // whether to highlight the first matching value
955        autoCompleteFirstMatch: false, // whether to complete the first matching value in the input box
956        highlightMatches: true, // whether all matches within the string should be highlighted with matchClass
957        highlightMatchesRegExModifier: 'i', // 'i' for case-insensitive, 'g' for global (all occurrences), or combine
958		matchAny: true, // for client-side filtering ONLY, match any occurrence of the search term in the result (e.g. "ar" would find "area" and "cart")
959        minChars: 1, // the minimum number of characters the user must enter before a search is executed
960        showArrow: true, // set to false to simulate google suggest
961        arrowQuery: '', // the query to run when the arrow is clicked
962        onSelect: false, // function to run when a result is selected
963        maxCacheBytes: 32768, // in bytes, 0 means caching is disabled
964        resultTemplate: '{name}', // html template for each row (put json properties in curly braces)
965        displayValue: 'name', // json element whose value is displayed on select
966        hiddenValue: 'id', // json element whose value is submitted when form is submitted
967        initialValue: '', // what should the value of the input field be when the form is loaded?
968        watermark: '', // text that appears when flexbox is loaded, if no initialValue is specified.  style with css class '.ffb-input.watermark'
969        width: 200, // total width of flexbox.  auto-adjusts based on showArrow value
970        resultsProperty: 'results', // json property in response that references array of results
971        totalProperty: 'total', // json property in response that references the total results (for paging)
972        maxVisibleRows: 0, // default is 0, which means it is ignored.  use either this, or paging.pageSize
973        paging: {
974            style: 'input', // or 'links'
975            cssClass: 'paging', // prefix with containerClass (e.g. .ffb .paging)
976            pageSize: 10, // acts as a threshold.  if <= pageSize results, paging doesn't appear
977            maxPageLinks: 5, // used only if style is 'links'
978            showSummary: true, // whether to show 'displaying 1-10 of 200 results' text
979            summaryClass: 'summary', // class for 'displaying 1-10 of 200 results', prefix with containerClass
980            summaryTemplate: 'Displaying {start}-{end} of {total} results' // can use {page} and {pages} as well
981        }
982    };
983
984    $.fn.setValue = function(val) {
985        var id = '#' + this.attr('id');
986        $(id + '_hidden,' + id + '_input').val(val).removeClass('watermark');
987    };
988})(jQuery);