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 = '<<', 399 prev = '<', 400 next = '>', 401 last = '>>', 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);