| @@ 13289-14729 (lines=1441) @@ | ||
| 13286 | * |
|
| 13287 | */ |
|
| 13288 | ||
| 13289 | ;(function ($, window, document, undefined) { |
|
| 13290 | ||
| 13291 | "use strict"; |
|
| 13292 | ||
| 13293 | window = (typeof window != 'undefined' && window.Math == Math) |
|
| 13294 | ? window |
|
| 13295 | : (typeof self != 'undefined' && self.Math == Math) |
|
| 13296 | ? self |
|
| 13297 | : Function('return this')() |
|
| 13298 | ; |
|
| 13299 | ||
| 13300 | $.fn.search = function(parameters) { |
|
| 13301 | var |
|
| 13302 | $allModules = $(this), |
|
| 13303 | moduleSelector = $allModules.selector || '', |
|
| 13304 | ||
| 13305 | time = new Date().getTime(), |
|
| 13306 | performance = [], |
|
| 13307 | ||
| 13308 | query = arguments[0], |
|
| 13309 | methodInvoked = (typeof query == 'string'), |
|
| 13310 | queryArguments = [].slice.call(arguments, 1), |
|
| 13311 | returnedValue |
|
| 13312 | ; |
|
| 13313 | $(this) |
|
| 13314 | .each(function() { |
|
| 13315 | var |
|
| 13316 | settings = ( $.isPlainObject(parameters) ) |
|
| 13317 | ? $.extend(true, {}, $.fn.search.settings, parameters) |
|
| 13318 | : $.extend({}, $.fn.search.settings), |
|
| 13319 | ||
| 13320 | className = settings.className, |
|
| 13321 | metadata = settings.metadata, |
|
| 13322 | regExp = settings.regExp, |
|
| 13323 | fields = settings.fields, |
|
| 13324 | selector = settings.selector, |
|
| 13325 | error = settings.error, |
|
| 13326 | namespace = settings.namespace, |
|
| 13327 | ||
| 13328 | eventNamespace = '.' + namespace, |
|
| 13329 | moduleNamespace = namespace + '-module', |
|
| 13330 | ||
| 13331 | $module = $(this), |
|
| 13332 | $prompt = $module.find(selector.prompt), |
|
| 13333 | $searchButton = $module.find(selector.searchButton), |
|
| 13334 | $results = $module.find(selector.results), |
|
| 13335 | $result = $module.find(selector.result), |
|
| 13336 | $category = $module.find(selector.category), |
|
| 13337 | ||
| 13338 | element = this, |
|
| 13339 | instance = $module.data(moduleNamespace), |
|
| 13340 | ||
| 13341 | disabledBubbled = false, |
|
| 13342 | resultsDismissed = false, |
|
| 13343 | ||
| 13344 | module |
|
| 13345 | ; |
|
| 13346 | ||
| 13347 | module = { |
|
| 13348 | ||
| 13349 | initialize: function() { |
|
| 13350 | module.verbose('Initializing module'); |
|
| 13351 | module.determine.searchFields(); |
|
| 13352 | module.bind.events(); |
|
| 13353 | module.set.type(); |
|
| 13354 | module.create.results(); |
|
| 13355 | module.instantiate(); |
|
| 13356 | }, |
|
| 13357 | instantiate: function() { |
|
| 13358 | module.verbose('Storing instance of module', module); |
|
| 13359 | instance = module; |
|
| 13360 | $module |
|
| 13361 | .data(moduleNamespace, module) |
|
| 13362 | ; |
|
| 13363 | }, |
|
| 13364 | destroy: function() { |
|
| 13365 | module.verbose('Destroying instance'); |
|
| 13366 | $module |
|
| 13367 | .off(eventNamespace) |
|
| 13368 | .removeData(moduleNamespace) |
|
| 13369 | ; |
|
| 13370 | }, |
|
| 13371 | ||
| 13372 | refresh: function() { |
|
| 13373 | module.debug('Refreshing selector cache'); |
|
| 13374 | $prompt = $module.find(selector.prompt); |
|
| 13375 | $searchButton = $module.find(selector.searchButton); |
|
| 13376 | $category = $module.find(selector.category); |
|
| 13377 | $results = $module.find(selector.results); |
|
| 13378 | $result = $module.find(selector.result); |
|
| 13379 | }, |
|
| 13380 | ||
| 13381 | refreshResults: function() { |
|
| 13382 | $results = $module.find(selector.results); |
|
| 13383 | $result = $module.find(selector.result); |
|
| 13384 | }, |
|
| 13385 | ||
| 13386 | bind: { |
|
| 13387 | events: function() { |
|
| 13388 | module.verbose('Binding events to search'); |
|
| 13389 | if(settings.automatic) { |
|
| 13390 | $module |
|
| 13391 | .on(module.get.inputEvent() + eventNamespace, selector.prompt, module.event.input) |
|
| 13392 | ; |
|
| 13393 | $prompt |
|
| 13394 | .attr('autocomplete', 'off') |
|
| 13395 | ; |
|
| 13396 | } |
|
| 13397 | $module |
|
| 13398 | // prompt |
|
| 13399 | .on('focus' + eventNamespace, selector.prompt, module.event.focus) |
|
| 13400 | .on('blur' + eventNamespace, selector.prompt, module.event.blur) |
|
| 13401 | .on('keydown' + eventNamespace, selector.prompt, module.handleKeyboard) |
|
| 13402 | // search button |
|
| 13403 | .on('click' + eventNamespace, selector.searchButton, module.query) |
|
| 13404 | // results |
|
| 13405 | .on('mousedown' + eventNamespace, selector.results, module.event.result.mousedown) |
|
| 13406 | .on('mouseup' + eventNamespace, selector.results, module.event.result.mouseup) |
|
| 13407 | .on('click' + eventNamespace, selector.result, module.event.result.click) |
|
| 13408 | ; |
|
| 13409 | } |
|
| 13410 | }, |
|
| 13411 | ||
| 13412 | determine: { |
|
| 13413 | searchFields: function() { |
|
| 13414 | // this makes sure $.extend does not add specified search fields to default fields |
|
| 13415 | // this is the only setting which should not extend defaults |
|
| 13416 | if(parameters && parameters.searchFields !== undefined) { |
|
| 13417 | settings.searchFields = parameters.searchFields; |
|
| 13418 | } |
|
| 13419 | } |
|
| 13420 | }, |
|
| 13421 | ||
| 13422 | event: { |
|
| 13423 | input: function() { |
|
| 13424 | if(settings.searchDelay) { |
|
| 13425 | clearTimeout(module.timer); |
|
| 13426 | module.timer = setTimeout(function() { |
|
| 13427 | if(module.is.focused()) { |
|
| 13428 | module.query(); |
|
| 13429 | } |
|
| 13430 | }, settings.searchDelay); |
|
| 13431 | } |
|
| 13432 | else { |
|
| 13433 | module.query(); |
|
| 13434 | } |
|
| 13435 | }, |
|
| 13436 | focus: function() { |
|
| 13437 | module.set.focus(); |
|
| 13438 | if(settings.searchOnFocus && module.has.minimumCharacters() ) { |
|
| 13439 | module.query(function() { |
|
| 13440 | if(module.can.show() ) { |
|
| 13441 | module.showResults(); |
|
| 13442 | } |
|
| 13443 | }); |
|
| 13444 | } |
|
| 13445 | }, |
|
| 13446 | blur: function(event) { |
|
| 13447 | var |
|
| 13448 | pageLostFocus = (document.activeElement === this), |
|
| 13449 | callback = function() { |
|
| 13450 | module.cancel.query(); |
|
| 13451 | module.remove.focus(); |
|
| 13452 | module.timer = setTimeout(module.hideResults, settings.hideDelay); |
|
| 13453 | } |
|
| 13454 | ; |
|
| 13455 | if(pageLostFocus) { |
|
| 13456 | return; |
|
| 13457 | } |
|
| 13458 | resultsDismissed = false; |
|
| 13459 | if(module.resultsClicked) { |
|
| 13460 | module.debug('Determining if user action caused search to close'); |
|
| 13461 | $module |
|
| 13462 | .one('click.close' + eventNamespace, selector.results, function(event) { |
|
| 13463 | if(module.is.inMessage(event) || disabledBubbled) { |
|
| 13464 | $prompt.focus(); |
|
| 13465 | return; |
|
| 13466 | } |
|
| 13467 | disabledBubbled = false; |
|
| 13468 | if( !module.is.animating() && !module.is.hidden()) { |
|
| 13469 | callback(); |
|
| 13470 | } |
|
| 13471 | }) |
|
| 13472 | ; |
|
| 13473 | } |
|
| 13474 | else { |
|
| 13475 | module.debug('Input blurred without user action, closing results'); |
|
| 13476 | callback(); |
|
| 13477 | } |
|
| 13478 | }, |
|
| 13479 | result: { |
|
| 13480 | mousedown: function() { |
|
| 13481 | module.resultsClicked = true; |
|
| 13482 | }, |
|
| 13483 | mouseup: function() { |
|
| 13484 | module.resultsClicked = false; |
|
| 13485 | }, |
|
| 13486 | click: function(event) { |
|
| 13487 | module.debug('Search result selected'); |
|
| 13488 | var |
|
| 13489 | $result = $(this), |
|
| 13490 | $title = $result.find(selector.title).eq(0), |
|
| 13491 | $link = $result.is('a[href]') |
|
| 13492 | ? $result |
|
| 13493 | : $result.find('a[href]').eq(0), |
|
| 13494 | href = $link.attr('href') || false, |
|
| 13495 | target = $link.attr('target') || false, |
|
| 13496 | title = $title.html(), |
|
| 13497 | // title is used for result lookup |
|
| 13498 | value = ($title.length > 0) |
|
| 13499 | ? $title.text() |
|
| 13500 | : false, |
|
| 13501 | results = module.get.results(), |
|
| 13502 | result = $result.data(metadata.result) || module.get.result(value, results), |
|
| 13503 | returnedValue |
|
| 13504 | ; |
|
| 13505 | if( $.isFunction(settings.onSelect) ) { |
|
| 13506 | if(settings.onSelect.call(element, result, results) === false) { |
|
| 13507 | module.debug('Custom onSelect callback cancelled default select action'); |
|
| 13508 | disabledBubbled = true; |
|
| 13509 | return; |
|
| 13510 | } |
|
| 13511 | } |
|
| 13512 | module.hideResults(); |
|
| 13513 | if(value) { |
|
| 13514 | module.set.value(value); |
|
| 13515 | } |
|
| 13516 | if(href) { |
|
| 13517 | module.verbose('Opening search link found in result', $link); |
|
| 13518 | if(target == '_blank' || event.ctrlKey) { |
|
| 13519 | window.open(href); |
|
| 13520 | } |
|
| 13521 | else { |
|
| 13522 | window.location.href = (href); |
|
| 13523 | } |
|
| 13524 | } |
|
| 13525 | } |
|
| 13526 | } |
|
| 13527 | }, |
|
| 13528 | handleKeyboard: function(event) { |
|
| 13529 | var |
|
| 13530 | // force selector refresh |
|
| 13531 | $result = $module.find(selector.result), |
|
| 13532 | $category = $module.find(selector.category), |
|
| 13533 | $activeResult = $result.filter('.' + className.active), |
|
| 13534 | currentIndex = $result.index( $activeResult ), |
|
| 13535 | resultSize = $result.length, |
|
| 13536 | hasActiveResult = $activeResult.length > 0, |
|
| 13537 | ||
| 13538 | keyCode = event.which, |
|
| 13539 | keys = { |
|
| 13540 | backspace : 8, |
|
| 13541 | enter : 13, |
|
| 13542 | escape : 27, |
|
| 13543 | upArrow : 38, |
|
| 13544 | downArrow : 40 |
|
| 13545 | }, |
|
| 13546 | newIndex |
|
| 13547 | ; |
|
| 13548 | // search shortcuts |
|
| 13549 | if(keyCode == keys.escape) { |
|
| 13550 | module.verbose('Escape key pressed, blurring search field'); |
|
| 13551 | module.hideResults(); |
|
| 13552 | resultsDismissed = true; |
|
| 13553 | } |
|
| 13554 | if( module.is.visible() ) { |
|
| 13555 | if(keyCode == keys.enter) { |
|
| 13556 | module.verbose('Enter key pressed, selecting active result'); |
|
| 13557 | if( $result.filter('.' + className.active).length > 0 ) { |
|
| 13558 | module.event.result.click.call($result.filter('.' + className.active), event); |
|
| 13559 | event.preventDefault(); |
|
| 13560 | return false; |
|
| 13561 | } |
|
| 13562 | } |
|
| 13563 | else if(keyCode == keys.upArrow && hasActiveResult) { |
|
| 13564 | module.verbose('Up key pressed, changing active result'); |
|
| 13565 | newIndex = (currentIndex - 1 < 0) |
|
| 13566 | ? currentIndex |
|
| 13567 | : currentIndex - 1 |
|
| 13568 | ; |
|
| 13569 | $category |
|
| 13570 | .removeClass(className.active) |
|
| 13571 | ; |
|
| 13572 | $result |
|
| 13573 | .removeClass(className.active) |
|
| 13574 | .eq(newIndex) |
|
| 13575 | .addClass(className.active) |
|
| 13576 | .closest($category) |
|
| 13577 | .addClass(className.active) |
|
| 13578 | ; |
|
| 13579 | event.preventDefault(); |
|
| 13580 | } |
|
| 13581 | else if(keyCode == keys.downArrow) { |
|
| 13582 | module.verbose('Down key pressed, changing active result'); |
|
| 13583 | newIndex = (currentIndex + 1 >= resultSize) |
|
| 13584 | ? currentIndex |
|
| 13585 | : currentIndex + 1 |
|
| 13586 | ; |
|
| 13587 | $category |
|
| 13588 | .removeClass(className.active) |
|
| 13589 | ; |
|
| 13590 | $result |
|
| 13591 | .removeClass(className.active) |
|
| 13592 | .eq(newIndex) |
|
| 13593 | .addClass(className.active) |
|
| 13594 | .closest($category) |
|
| 13595 | .addClass(className.active) |
|
| 13596 | ; |
|
| 13597 | event.preventDefault(); |
|
| 13598 | } |
|
| 13599 | } |
|
| 13600 | else { |
|
| 13601 | // query shortcuts |
|
| 13602 | if(keyCode == keys.enter) { |
|
| 13603 | module.verbose('Enter key pressed, executing query'); |
|
| 13604 | module.query(); |
|
| 13605 | module.set.buttonPressed(); |
|
| 13606 | $prompt.one('keyup', module.remove.buttonFocus); |
|
| 13607 | } |
|
| 13608 | } |
|
| 13609 | }, |
|
| 13610 | ||
| 13611 | setup: { |
|
| 13612 | api: function(searchTerm, callback) { |
|
| 13613 | var |
|
| 13614 | apiSettings = { |
|
| 13615 | debug : settings.debug, |
|
| 13616 | on : false, |
|
| 13617 | cache : true, |
|
| 13618 | action : 'search', |
|
| 13619 | urlData : { |
|
| 13620 | query : searchTerm |
|
| 13621 | }, |
|
| 13622 | onSuccess : function(response) { |
|
| 13623 | module.parse.response.call(element, response, searchTerm); |
|
| 13624 | callback(); |
|
| 13625 | }, |
|
| 13626 | onFailure : function() { |
|
| 13627 | module.displayMessage(error.serverError); |
|
| 13628 | callback(); |
|
| 13629 | }, |
|
| 13630 | onAbort : function(response) { |
|
| 13631 | }, |
|
| 13632 | onError : module.error |
|
| 13633 | }, |
|
| 13634 | searchHTML |
|
| 13635 | ; |
|
| 13636 | $.extend(true, apiSettings, settings.apiSettings); |
|
| 13637 | module.verbose('Setting up API request', apiSettings); |
|
| 13638 | $module.api(apiSettings); |
|
| 13639 | } |
|
| 13640 | }, |
|
| 13641 | ||
| 13642 | can: { |
|
| 13643 | useAPI: function() { |
|
| 13644 | return $.fn.api !== undefined; |
|
| 13645 | }, |
|
| 13646 | show: function() { |
|
| 13647 | return module.is.focused() && !module.is.visible() && !module.is.empty(); |
|
| 13648 | }, |
|
| 13649 | transition: function() { |
|
| 13650 | return settings.transition && $.fn.transition !== undefined && $module.transition('is supported'); |
|
| 13651 | } |
|
| 13652 | }, |
|
| 13653 | ||
| 13654 | is: { |
|
| 13655 | animating: function() { |
|
| 13656 | return $results.hasClass(className.animating); |
|
| 13657 | }, |
|
| 13658 | hidden: function() { |
|
| 13659 | return $results.hasClass(className.hidden); |
|
| 13660 | }, |
|
| 13661 | inMessage: function(event) { |
|
| 13662 | if(!event.target) { |
|
| 13663 | return; |
|
| 13664 | } |
|
| 13665 | var |
|
| 13666 | $target = $(event.target), |
|
| 13667 | isInDOM = $.contains(document.documentElement, event.target) |
|
| 13668 | ; |
|
| 13669 | return (isInDOM && $target.closest(selector.message).length > 0); |
|
| 13670 | }, |
|
| 13671 | empty: function() { |
|
| 13672 | return ($results.html() === ''); |
|
| 13673 | }, |
|
| 13674 | visible: function() { |
|
| 13675 | return ($results.filter(':visible').length > 0); |
|
| 13676 | }, |
|
| 13677 | focused: function() { |
|
| 13678 | return ($prompt.filter(':focus').length > 0); |
|
| 13679 | } |
|
| 13680 | }, |
|
| 13681 | ||
| 13682 | get: { |
|
| 13683 | inputEvent: function() { |
|
| 13684 | var |
|
| 13685 | prompt = $prompt[0], |
|
| 13686 | inputEvent = (prompt !== undefined && prompt.oninput !== undefined) |
|
| 13687 | ? 'input' |
|
| 13688 | : (prompt !== undefined && prompt.onpropertychange !== undefined) |
|
| 13689 | ? 'propertychange' |
|
| 13690 | : 'keyup' |
|
| 13691 | ; |
|
| 13692 | return inputEvent; |
|
| 13693 | }, |
|
| 13694 | value: function() { |
|
| 13695 | return $prompt.val(); |
|
| 13696 | }, |
|
| 13697 | results: function() { |
|
| 13698 | var |
|
| 13699 | results = $module.data(metadata.results) |
|
| 13700 | ; |
|
| 13701 | return results; |
|
| 13702 | }, |
|
| 13703 | result: function(value, results) { |
|
| 13704 | var |
|
| 13705 | lookupFields = ['title', 'id'], |
|
| 13706 | result = false |
|
| 13707 | ; |
|
| 13708 | value = (value !== undefined) |
|
| 13709 | ? value |
|
| 13710 | : module.get.value() |
|
| 13711 | ; |
|
| 13712 | results = (results !== undefined) |
|
| 13713 | ? results |
|
| 13714 | : module.get.results() |
|
| 13715 | ; |
|
| 13716 | if(settings.type === 'category') { |
|
| 13717 | module.debug('Finding result that matches', value); |
|
| 13718 | $.each(results, function(index, category) { |
|
| 13719 | if($.isArray(category.results)) { |
|
| 13720 | result = module.search.object(value, category.results, lookupFields)[0]; |
|
| 13721 | // don't continue searching if a result is found |
|
| 13722 | if(result) { |
|
| 13723 | return false; |
|
| 13724 | } |
|
| 13725 | } |
|
| 13726 | }); |
|
| 13727 | } |
|
| 13728 | else { |
|
| 13729 | module.debug('Finding result in results object', value); |
|
| 13730 | result = module.search.object(value, results, lookupFields)[0]; |
|
| 13731 | } |
|
| 13732 | return result || false; |
|
| 13733 | }, |
|
| 13734 | }, |
|
| 13735 | ||
| 13736 | select: { |
|
| 13737 | firstResult: function() { |
|
| 13738 | module.verbose('Selecting first result'); |
|
| 13739 | $result.first().addClass(className.active); |
|
| 13740 | } |
|
| 13741 | }, |
|
| 13742 | ||
| 13743 | set: { |
|
| 13744 | focus: function() { |
|
| 13745 | $module.addClass(className.focus); |
|
| 13746 | }, |
|
| 13747 | loading: function() { |
|
| 13748 | $module.addClass(className.loading); |
|
| 13749 | }, |
|
| 13750 | value: function(value) { |
|
| 13751 | module.verbose('Setting search input value', value); |
|
| 13752 | $prompt |
|
| 13753 | .val(value) |
|
| 13754 | ; |
|
| 13755 | }, |
|
| 13756 | type: function(type) { |
|
| 13757 | type = type || settings.type; |
|
| 13758 | if(settings.type == 'category') { |
|
| 13759 | $module.addClass(settings.type); |
|
| 13760 | } |
|
| 13761 | }, |
|
| 13762 | buttonPressed: function() { |
|
| 13763 | $searchButton.addClass(className.pressed); |
|
| 13764 | } |
|
| 13765 | }, |
|
| 13766 | ||
| 13767 | remove: { |
|
| 13768 | loading: function() { |
|
| 13769 | $module.removeClass(className.loading); |
|
| 13770 | }, |
|
| 13771 | focus: function() { |
|
| 13772 | $module.removeClass(className.focus); |
|
| 13773 | }, |
|
| 13774 | buttonPressed: function() { |
|
| 13775 | $searchButton.removeClass(className.pressed); |
|
| 13776 | } |
|
| 13777 | }, |
|
| 13778 | ||
| 13779 | query: function(callback) { |
|
| 13780 | callback = $.isFunction(callback) |
|
| 13781 | ? callback |
|
| 13782 | : function(){} |
|
| 13783 | ; |
|
| 13784 | var |
|
| 13785 | searchTerm = module.get.value(), |
|
| 13786 | cache = module.read.cache(searchTerm) |
|
| 13787 | ; |
|
| 13788 | callback = callback || function() {}; |
|
| 13789 | if( module.has.minimumCharacters() ) { |
|
| 13790 | if(cache) { |
|
| 13791 | module.debug('Reading result from cache', searchTerm); |
|
| 13792 | module.save.results(cache.results); |
|
| 13793 | module.addResults(cache.html); |
|
| 13794 | module.inject.id(cache.results); |
|
| 13795 | callback(); |
|
| 13796 | } |
|
| 13797 | else { |
|
| 13798 | module.debug('Querying for', searchTerm); |
|
| 13799 | if($.isPlainObject(settings.source) || $.isArray(settings.source)) { |
|
| 13800 | module.search.local(searchTerm); |
|
| 13801 | callback(); |
|
| 13802 | } |
|
| 13803 | else if( module.can.useAPI() ) { |
|
| 13804 | module.search.remote(searchTerm, callback); |
|
| 13805 | } |
|
| 13806 | else { |
|
| 13807 | module.error(error.source); |
|
| 13808 | callback(); |
|
| 13809 | } |
|
| 13810 | } |
|
| 13811 | settings.onSearchQuery.call(element, searchTerm); |
|
| 13812 | } |
|
| 13813 | else { |
|
| 13814 | module.hideResults(); |
|
| 13815 | } |
|
| 13816 | }, |
|
| 13817 | ||
| 13818 | search: { |
|
| 13819 | local: function(searchTerm) { |
|
| 13820 | var |
|
| 13821 | results = module.search.object(searchTerm, settings.content), |
|
| 13822 | searchHTML |
|
| 13823 | ; |
|
| 13824 | module.set.loading(); |
|
| 13825 | module.save.results(results); |
|
| 13826 | module.debug('Returned local search results', results); |
|
| 13827 | ||
| 13828 | searchHTML = module.generateResults({ |
|
| 13829 | results: results |
|
| 13830 | }); |
|
| 13831 | module.remove.loading(); |
|
| 13832 | module.addResults(searchHTML); |
|
| 13833 | module.inject.id(results); |
|
| 13834 | module.write.cache(searchTerm, { |
|
| 13835 | html : searchHTML, |
|
| 13836 | results : results |
|
| 13837 | }); |
|
| 13838 | }, |
|
| 13839 | remote: function(searchTerm, callback) { |
|
| 13840 | callback = $.isFunction(callback) |
|
| 13841 | ? callback |
|
| 13842 | : function(){} |
|
| 13843 | ; |
|
| 13844 | if($module.api('is loading')) { |
|
| 13845 | $module.api('abort'); |
|
| 13846 | } |
|
| 13847 | module.setup.api(searchTerm, callback); |
|
| 13848 | $module |
|
| 13849 | .api('query') |
|
| 13850 | ; |
|
| 13851 | }, |
|
| 13852 | object: function(searchTerm, source, searchFields) { |
|
| 13853 | var |
|
| 13854 | results = [], |
|
| 13855 | fuzzyResults = [], |
|
| 13856 | searchExp = searchTerm.toString().replace(regExp.escape, '\\$&'), |
|
| 13857 | matchRegExp = new RegExp(regExp.beginsWith + searchExp, 'i'), |
|
| 13858 | ||
| 13859 | // avoid duplicates when pushing results |
|
| 13860 | addResult = function(array, result) { |
|
| 13861 | var |
|
| 13862 | notResult = ($.inArray(result, results) == -1), |
|
| 13863 | notFuzzyResult = ($.inArray(result, fuzzyResults) == -1) |
|
| 13864 | ; |
|
| 13865 | if(notResult && notFuzzyResult) { |
|
| 13866 | array.push(result); |
|
| 13867 | } |
|
| 13868 | } |
|
| 13869 | ; |
|
| 13870 | source = source || settings.source; |
|
| 13871 | searchFields = (searchFields !== undefined) |
|
| 13872 | ? searchFields |
|
| 13873 | : settings.searchFields |
|
| 13874 | ; |
|
| 13875 | ||
| 13876 | // search fields should be array to loop correctly |
|
| 13877 | if(!$.isArray(searchFields)) { |
|
| 13878 | searchFields = [searchFields]; |
|
| 13879 | } |
|
| 13880 | ||
| 13881 | // exit conditions if no source |
|
| 13882 | if(source === undefined || source === false) { |
|
| 13883 | module.error(error.source); |
|
| 13884 | return []; |
|
| 13885 | } |
|
| 13886 | ||
| 13887 | // iterate through search fields looking for matches |
|
| 13888 | $.each(searchFields, function(index, field) { |
|
| 13889 | $.each(source, function(label, content) { |
|
| 13890 | var |
|
| 13891 | fieldExists = (typeof content[field] == 'string') |
|
| 13892 | ; |
|
| 13893 | if(fieldExists) { |
|
| 13894 | if( content[field].search(matchRegExp) !== -1) { |
|
| 13895 | // content starts with value (first in results) |
|
| 13896 | addResult(results, content); |
|
| 13897 | } |
|
| 13898 | else if(settings.searchFullText && module.fuzzySearch(searchTerm, content[field]) ) { |
|
| 13899 | // content fuzzy matches (last in results) |
|
| 13900 | addResult(fuzzyResults, content); |
|
| 13901 | } |
|
| 13902 | } |
|
| 13903 | }); |
|
| 13904 | }); |
|
| 13905 | return $.merge(results, fuzzyResults); |
|
| 13906 | } |
|
| 13907 | }, |
|
| 13908 | ||
| 13909 | fuzzySearch: function(query, term) { |
|
| 13910 | var |
|
| 13911 | termLength = term.length, |
|
| 13912 | queryLength = query.length |
|
| 13913 | ; |
|
| 13914 | if(typeof query !== 'string') { |
|
| 13915 | return false; |
|
| 13916 | } |
|
| 13917 | query = query.toLowerCase(); |
|
| 13918 | term = term.toLowerCase(); |
|
| 13919 | if(queryLength > termLength) { |
|
| 13920 | return false; |
|
| 13921 | } |
|
| 13922 | if(queryLength === termLength) { |
|
| 13923 | return (query === term); |
|
| 13924 | } |
|
| 13925 | search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) { |
|
| 13926 | var |
|
| 13927 | queryCharacter = query.charCodeAt(characterIndex) |
|
| 13928 | ; |
|
| 13929 | while(nextCharacterIndex < termLength) { |
|
| 13930 | if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) { |
|
| 13931 | continue search; |
|
| 13932 | } |
|
| 13933 | } |
|
| 13934 | return false; |
|
| 13935 | } |
|
| 13936 | return true; |
|
| 13937 | }, |
|
| 13938 | ||
| 13939 | parse: { |
|
| 13940 | response: function(response, searchTerm) { |
|
| 13941 | var |
|
| 13942 | searchHTML = module.generateResults(response) |
|
| 13943 | ; |
|
| 13944 | module.verbose('Parsing server response', response); |
|
| 13945 | if(response !== undefined) { |
|
| 13946 | if(searchTerm !== undefined && response[fields.results] !== undefined) { |
|
| 13947 | module.addResults(searchHTML); |
|
| 13948 | module.inject.id(response[fields.results]); |
|
| 13949 | module.write.cache(searchTerm, { |
|
| 13950 | html : searchHTML, |
|
| 13951 | results : response[fields.results] |
|
| 13952 | }); |
|
| 13953 | module.save.results(response[fields.results]); |
|
| 13954 | } |
|
| 13955 | } |
|
| 13956 | } |
|
| 13957 | }, |
|
| 13958 | ||
| 13959 | cancel: { |
|
| 13960 | query: function() { |
|
| 13961 | if( module.can.useAPI() ) { |
|
| 13962 | $module.api('abort'); |
|
| 13963 | } |
|
| 13964 | } |
|
| 13965 | }, |
|
| 13966 | ||
| 13967 | has: { |
|
| 13968 | minimumCharacters: function() { |
|
| 13969 | var |
|
| 13970 | searchTerm = module.get.value(), |
|
| 13971 | numCharacters = searchTerm.length |
|
| 13972 | ; |
|
| 13973 | return (numCharacters >= settings.minCharacters); |
|
| 13974 | }, |
|
| 13975 | results: function() { |
|
| 13976 | if($results.length === 0) { |
|
| 13977 | return false; |
|
| 13978 | } |
|
| 13979 | var |
|
| 13980 | html = $results.html() |
|
| 13981 | ; |
|
| 13982 | return html != ''; |
|
| 13983 | } |
|
| 13984 | }, |
|
| 13985 | ||
| 13986 | clear: { |
|
| 13987 | cache: function(value) { |
|
| 13988 | var |
|
| 13989 | cache = $module.data(metadata.cache) |
|
| 13990 | ; |
|
| 13991 | if(!value) { |
|
| 13992 | module.debug('Clearing cache', value); |
|
| 13993 | $module.removeData(metadata.cache); |
|
| 13994 | } |
|
| 13995 | else if(value && cache && cache[value]) { |
|
| 13996 | module.debug('Removing value from cache', value); |
|
| 13997 | delete cache[value]; |
|
| 13998 | $module.data(metadata.cache, cache); |
|
| 13999 | } |
|
| 14000 | } |
|
| 14001 | }, |
|
| 14002 | ||
| 14003 | read: { |
|
| 14004 | cache: function(name) { |
|
| 14005 | var |
|
| 14006 | cache = $module.data(metadata.cache) |
|
| 14007 | ; |
|
| 14008 | if(settings.cache) { |
|
| 14009 | module.verbose('Checking cache for generated html for query', name); |
|
| 14010 | return (typeof cache == 'object') && (cache[name] !== undefined) |
|
| 14011 | ? cache[name] |
|
| 14012 | : false |
|
| 14013 | ; |
|
| 14014 | } |
|
| 14015 | return false; |
|
| 14016 | } |
|
| 14017 | }, |
|
| 14018 | ||
| 14019 | create: { |
|
| 14020 | id: function(resultIndex, categoryIndex) { |
|
| 14021 | var |
|
| 14022 | resultID = (resultIndex + 1), // not zero indexed |
|
| 14023 | categoryID = (categoryIndex + 1), |
|
| 14024 | firstCharCode, |
|
| 14025 | letterID, |
|
| 14026 | id |
|
| 14027 | ; |
|
| 14028 | if(categoryIndex !== undefined) { |
|
| 14029 | // start char code for "A" |
|
| 14030 | letterID = String.fromCharCode(97 + categoryIndex); |
|
| 14031 | id = letterID + resultID; |
|
| 14032 | module.verbose('Creating category result id', id); |
|
| 14033 | } |
|
| 14034 | else { |
|
| 14035 | id = resultID; |
|
| 14036 | module.verbose('Creating result id', id); |
|
| 14037 | } |
|
| 14038 | return id; |
|
| 14039 | }, |
|
| 14040 | results: function() { |
|
| 14041 | if($results.length === 0) { |
|
| 14042 | $results = $('<div />') |
|
| 14043 | .addClass(className.results) |
|
| 14044 | .appendTo($module) |
|
| 14045 | ; |
|
| 14046 | } |
|
| 14047 | } |
|
| 14048 | }, |
|
| 14049 | ||
| 14050 | inject: { |
|
| 14051 | result: function(result, resultIndex, categoryIndex) { |
|
| 14052 | module.verbose('Injecting result into results'); |
|
| 14053 | var |
|
| 14054 | $selectedResult = (categoryIndex !== undefined) |
|
| 14055 | ? $results |
|
| 14056 | .children().eq(categoryIndex) |
|
| 14057 | .children(selector.result).eq(resultIndex) |
|
| 14058 | : $results |
|
| 14059 | .children(selector.result).eq(resultIndex) |
|
| 14060 | ; |
|
| 14061 | module.verbose('Injecting results metadata', $selectedResult); |
|
| 14062 | $selectedResult |
|
| 14063 | .data(metadata.result, result) |
|
| 14064 | ; |
|
| 14065 | }, |
|
| 14066 | id: function(results) { |
|
| 14067 | module.debug('Injecting unique ids into results'); |
|
| 14068 | var |
|
| 14069 | // since results may be object, we must use counters |
|
| 14070 | categoryIndex = 0, |
|
| 14071 | resultIndex = 0 |
|
| 14072 | ; |
|
| 14073 | if(settings.type === 'category') { |
|
| 14074 | // iterate through each category result |
|
| 14075 | $.each(results, function(index, category) { |
|
| 14076 | resultIndex = 0; |
|
| 14077 | $.each(category.results, function(index, value) { |
|
| 14078 | var |
|
| 14079 | result = category.results[index] |
|
| 14080 | ; |
|
| 14081 | if(result.id === undefined) { |
|
| 14082 | result.id = module.create.id(resultIndex, categoryIndex); |
|
| 14083 | } |
|
| 14084 | module.inject.result(result, resultIndex, categoryIndex); |
|
| 14085 | resultIndex++; |
|
| 14086 | }); |
|
| 14087 | categoryIndex++; |
|
| 14088 | }); |
|
| 14089 | } |
|
| 14090 | else { |
|
| 14091 | // top level |
|
| 14092 | $.each(results, function(index, value) { |
|
| 14093 | var |
|
| 14094 | result = results[index] |
|
| 14095 | ; |
|
| 14096 | if(result.id === undefined) { |
|
| 14097 | result.id = module.create.id(resultIndex); |
|
| 14098 | } |
|
| 14099 | module.inject.result(result, resultIndex); |
|
| 14100 | resultIndex++; |
|
| 14101 | }); |
|
| 14102 | } |
|
| 14103 | return results; |
|
| 14104 | } |
|
| 14105 | }, |
|
| 14106 | ||
| 14107 | save: { |
|
| 14108 | results: function(results) { |
|
| 14109 | module.verbose('Saving current search results to metadata', results); |
|
| 14110 | $module.data(metadata.results, results); |
|
| 14111 | } |
|
| 14112 | }, |
|
| 14113 | ||
| 14114 | write: { |
|
| 14115 | cache: function(name, value) { |
|
| 14116 | var |
|
| 14117 | cache = ($module.data(metadata.cache) !== undefined) |
|
| 14118 | ? $module.data(metadata.cache) |
|
| 14119 | : {} |
|
| 14120 | ; |
|
| 14121 | if(settings.cache) { |
|
| 14122 | module.verbose('Writing generated html to cache', name, value); |
|
| 14123 | cache[name] = value; |
|
| 14124 | $module |
|
| 14125 | .data(metadata.cache, cache) |
|
| 14126 | ; |
|
| 14127 | } |
|
| 14128 | } |
|
| 14129 | }, |
|
| 14130 | ||
| 14131 | addResults: function(html) { |
|
| 14132 | if( $.isFunction(settings.onResultsAdd) ) { |
|
| 14133 | if( settings.onResultsAdd.call($results, html) === false ) { |
|
| 14134 | module.debug('onResultsAdd callback cancelled default action'); |
|
| 14135 | return false; |
|
| 14136 | } |
|
| 14137 | } |
|
| 14138 | if(html) { |
|
| 14139 | $results |
|
| 14140 | .html(html) |
|
| 14141 | ; |
|
| 14142 | module.refreshResults(); |
|
| 14143 | if(settings.selectFirstResult) { |
|
| 14144 | module.select.firstResult(); |
|
| 14145 | } |
|
| 14146 | module.showResults(); |
|
| 14147 | } |
|
| 14148 | else { |
|
| 14149 | module.hideResults(function() { |
|
| 14150 | $results.empty(); |
|
| 14151 | }); |
|
| 14152 | } |
|
| 14153 | }, |
|
| 14154 | ||
| 14155 | showResults: function(callback) { |
|
| 14156 | callback = $.isFunction(callback) |
|
| 14157 | ? callback |
|
| 14158 | : function(){} |
|
| 14159 | ; |
|
| 14160 | if(resultsDismissed) { |
|
| 14161 | return; |
|
| 14162 | } |
|
| 14163 | if(!module.is.visible() && module.has.results()) { |
|
| 14164 | if( module.can.transition() ) { |
|
| 14165 | module.debug('Showing results with css animations'); |
|
| 14166 | $results |
|
| 14167 | .transition({ |
|
| 14168 | animation : settings.transition + ' in', |
|
| 14169 | debug : settings.debug, |
|
| 14170 | verbose : settings.verbose, |
|
| 14171 | duration : settings.duration, |
|
| 14172 | onComplete : function() { |
|
| 14173 | callback(); |
|
| 14174 | }, |
|
| 14175 | queue : true |
|
| 14176 | }) |
|
| 14177 | ; |
|
| 14178 | } |
|
| 14179 | else { |
|
| 14180 | module.debug('Showing results with javascript'); |
|
| 14181 | $results |
|
| 14182 | .stop() |
|
| 14183 | .fadeIn(settings.duration, settings.easing) |
|
| 14184 | ; |
|
| 14185 | } |
|
| 14186 | settings.onResultsOpen.call($results); |
|
| 14187 | } |
|
| 14188 | }, |
|
| 14189 | hideResults: function(callback) { |
|
| 14190 | callback = $.isFunction(callback) |
|
| 14191 | ? callback |
|
| 14192 | : function(){} |
|
| 14193 | ; |
|
| 14194 | if( module.is.visible() ) { |
|
| 14195 | if( module.can.transition() ) { |
|
| 14196 | module.debug('Hiding results with css animations'); |
|
| 14197 | $results |
|
| 14198 | .transition({ |
|
| 14199 | animation : settings.transition + ' out', |
|
| 14200 | debug : settings.debug, |
|
| 14201 | verbose : settings.verbose, |
|
| 14202 | duration : settings.duration, |
|
| 14203 | onComplete : function() { |
|
| 14204 | callback(); |
|
| 14205 | }, |
|
| 14206 | queue : true |
|
| 14207 | }) |
|
| 14208 | ; |
|
| 14209 | } |
|
| 14210 | else { |
|
| 14211 | module.debug('Hiding results with javascript'); |
|
| 14212 | $results |
|
| 14213 | .stop() |
|
| 14214 | .fadeOut(settings.duration, settings.easing) |
|
| 14215 | ; |
|
| 14216 | } |
|
| 14217 | settings.onResultsClose.call($results); |
|
| 14218 | } |
|
| 14219 | }, |
|
| 14220 | ||
| 14221 | generateResults: function(response) { |
|
| 14222 | module.debug('Generating html from response', response); |
|
| 14223 | var |
|
| 14224 | template = settings.templates[settings.type], |
|
| 14225 | isProperObject = ($.isPlainObject(response[fields.results]) && !$.isEmptyObject(response[fields.results])), |
|
| 14226 | isProperArray = ($.isArray(response[fields.results]) && response[fields.results].length > 0), |
|
| 14227 | html = '' |
|
| 14228 | ; |
|
| 14229 | if(isProperObject || isProperArray ) { |
|
| 14230 | if(settings.maxResults > 0) { |
|
| 14231 | if(isProperObject) { |
|
| 14232 | if(settings.type == 'standard') { |
|
| 14233 | module.error(error.maxResults); |
|
| 14234 | } |
|
| 14235 | } |
|
| 14236 | else { |
|
| 14237 | response[fields.results] = response[fields.results].slice(0, settings.maxResults); |
|
| 14238 | } |
|
| 14239 | } |
|
| 14240 | if($.isFunction(template)) { |
|
| 14241 | html = template(response, fields); |
|
| 14242 | } |
|
| 14243 | else { |
|
| 14244 | module.error(error.noTemplate, false); |
|
| 14245 | } |
|
| 14246 | } |
|
| 14247 | else if(settings.showNoResults) { |
|
| 14248 | html = module.displayMessage(error.noResults, 'empty'); |
|
| 14249 | } |
|
| 14250 | settings.onResults.call(element, response); |
|
| 14251 | return html; |
|
| 14252 | }, |
|
| 14253 | ||
| 14254 | displayMessage: function(text, type) { |
|
| 14255 | type = type || 'standard'; |
|
| 14256 | module.debug('Displaying message', text, type); |
|
| 14257 | module.addResults( settings.templates.message(text, type) ); |
|
| 14258 | return settings.templates.message(text, type); |
|
| 14259 | }, |
|
| 14260 | ||
| 14261 | setting: function(name, value) { |
|
| 14262 | if( $.isPlainObject(name) ) { |
|
| 14263 | $.extend(true, settings, name); |
|
| 14264 | } |
|
| 14265 | else if(value !== undefined) { |
|
| 14266 | settings[name] = value; |
|
| 14267 | } |
|
| 14268 | else { |
|
| 14269 | return settings[name]; |
|
| 14270 | } |
|
| 14271 | }, |
|
| 14272 | internal: function(name, value) { |
|
| 14273 | if( $.isPlainObject(name) ) { |
|
| 14274 | $.extend(true, module, name); |
|
| 14275 | } |
|
| 14276 | else if(value !== undefined) { |
|
| 14277 | module[name] = value; |
|
| 14278 | } |
|
| 14279 | else { |
|
| 14280 | return module[name]; |
|
| 14281 | } |
|
| 14282 | }, |
|
| 14283 | debug: function() { |
|
| 14284 | if(!settings.silent && settings.debug) { |
|
| 14285 | if(settings.performance) { |
|
| 14286 | module.performance.log(arguments); |
|
| 14287 | } |
|
| 14288 | else { |
|
| 14289 | module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); |
|
| 14290 | module.debug.apply(console, arguments); |
|
| 14291 | } |
|
| 14292 | } |
|
| 14293 | }, |
|
| 14294 | verbose: function() { |
|
| 14295 | if(!settings.silent && settings.verbose && settings.debug) { |
|
| 14296 | if(settings.performance) { |
|
| 14297 | module.performance.log(arguments); |
|
| 14298 | } |
|
| 14299 | else { |
|
| 14300 | module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); |
|
| 14301 | module.verbose.apply(console, arguments); |
|
| 14302 | } |
|
| 14303 | } |
|
| 14304 | }, |
|
| 14305 | error: function() { |
|
| 14306 | if(!settings.silent) { |
|
| 14307 | module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); |
|
| 14308 | module.error.apply(console, arguments); |
|
| 14309 | } |
|
| 14310 | }, |
|
| 14311 | performance: { |
|
| 14312 | log: function(message) { |
|
| 14313 | var |
|
| 14314 | currentTime, |
|
| 14315 | executionTime, |
|
| 14316 | previousTime |
|
| 14317 | ; |
|
| 14318 | if(settings.performance) { |
|
| 14319 | currentTime = new Date().getTime(); |
|
| 14320 | previousTime = time || currentTime; |
|
| 14321 | executionTime = currentTime - previousTime; |
|
| 14322 | time = currentTime; |
|
| 14323 | performance.push({ |
|
| 14324 | 'Name' : message[0], |
|
| 14325 | 'Arguments' : [].slice.call(message, 1) || '', |
|
| 14326 | 'Element' : element, |
|
| 14327 | 'Execution Time' : executionTime |
|
| 14328 | }); |
|
| 14329 | } |
|
| 14330 | clearTimeout(module.performance.timer); |
|
| 14331 | module.performance.timer = setTimeout(module.performance.display, 500); |
|
| 14332 | }, |
|
| 14333 | display: function() { |
|
| 14334 | var |
|
| 14335 | title = settings.name + ':', |
|
| 14336 | totalTime = 0 |
|
| 14337 | ; |
|
| 14338 | time = false; |
|
| 14339 | clearTimeout(module.performance.timer); |
|
| 14340 | $.each(performance, function(index, data) { |
|
| 14341 | totalTime += data['Execution Time']; |
|
| 14342 | }); |
|
| 14343 | title += ' ' + totalTime + 'ms'; |
|
| 14344 | if(moduleSelector) { |
|
| 14345 | title += ' \'' + moduleSelector + '\''; |
|
| 14346 | } |
|
| 14347 | if($allModules.length > 1) { |
|
| 14348 | title += ' ' + '(' + $allModules.length + ')'; |
|
| 14349 | } |
|
| 14350 | if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { |
|
| 14351 | console.groupCollapsed(title); |
|
| 14352 | if(console.table) { |
|
| 14353 | console.table(performance); |
|
| 14354 | } |
|
| 14355 | else { |
|
| 14356 | $.each(performance, function(index, data) { |
|
| 14357 | console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); |
|
| 14358 | }); |
|
| 14359 | } |
|
| 14360 | console.groupEnd(); |
|
| 14361 | } |
|
| 14362 | performance = []; |
|
| 14363 | } |
|
| 14364 | }, |
|
| 14365 | invoke: function(query, passedArguments, context) { |
|
| 14366 | var |
|
| 14367 | object = instance, |
|
| 14368 | maxDepth, |
|
| 14369 | found, |
|
| 14370 | response |
|
| 14371 | ; |
|
| 14372 | passedArguments = passedArguments || queryArguments; |
|
| 14373 | context = element || context; |
|
| 14374 | if(typeof query == 'string' && object !== undefined) { |
|
| 14375 | query = query.split(/[\. ]/); |
|
| 14376 | maxDepth = query.length - 1; |
|
| 14377 | $.each(query, function(depth, value) { |
|
| 14378 | var camelCaseValue = (depth != maxDepth) |
|
| 14379 | ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) |
|
| 14380 | : query |
|
| 14381 | ; |
|
| 14382 | if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { |
|
| 14383 | object = object[camelCaseValue]; |
|
| 14384 | } |
|
| 14385 | else if( object[camelCaseValue] !== undefined ) { |
|
| 14386 | found = object[camelCaseValue]; |
|
| 14387 | return false; |
|
| 14388 | } |
|
| 14389 | else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { |
|
| 14390 | object = object[value]; |
|
| 14391 | } |
|
| 14392 | else if( object[value] !== undefined ) { |
|
| 14393 | found = object[value]; |
|
| 14394 | return false; |
|
| 14395 | } |
|
| 14396 | else { |
|
| 14397 | return false; |
|
| 14398 | } |
|
| 14399 | }); |
|
| 14400 | } |
|
| 14401 | if( $.isFunction( found ) ) { |
|
| 14402 | response = found.apply(context, passedArguments); |
|
| 14403 | } |
|
| 14404 | else if(found !== undefined) { |
|
| 14405 | response = found; |
|
| 14406 | } |
|
| 14407 | if($.isArray(returnedValue)) { |
|
| 14408 | returnedValue.push(response); |
|
| 14409 | } |
|
| 14410 | else if(returnedValue !== undefined) { |
|
| 14411 | returnedValue = [returnedValue, response]; |
|
| 14412 | } |
|
| 14413 | else if(response !== undefined) { |
|
| 14414 | returnedValue = response; |
|
| 14415 | } |
|
| 14416 | return found; |
|
| 14417 | } |
|
| 14418 | }; |
|
| 14419 | if(methodInvoked) { |
|
| 14420 | if(instance === undefined) { |
|
| 14421 | module.initialize(); |
|
| 14422 | } |
|
| 14423 | module.invoke(query); |
|
| 14424 | } |
|
| 14425 | else { |
|
| 14426 | if(instance !== undefined) { |
|
| 14427 | instance.invoke('destroy'); |
|
| 14428 | } |
|
| 14429 | module.initialize(); |
|
| 14430 | } |
|
| 14431 | ||
| 14432 | }) |
|
| 14433 | ; |
|
| 14434 | ||
| 14435 | return (returnedValue !== undefined) |
|
| 14436 | ? returnedValue |
|
| 14437 | : this |
|
| 14438 | ; |
|
| 14439 | }; |
|
| 14440 | ||
| 14441 | $.fn.search.settings = { |
|
| 14442 | ||
| 14443 | name : 'Search', |
|
| 14444 | namespace : 'search', |
|
| 14445 | ||
| 14446 | silent : false, |
|
| 14447 | debug : false, |
|
| 14448 | verbose : false, |
|
| 14449 | performance : true, |
|
| 14450 | ||
| 14451 | // template to use (specified in settings.templates) |
|
| 14452 | type : 'standard', |
|
| 14453 | ||
| 14454 | // minimum characters required to search |
|
| 14455 | minCharacters : 1, |
|
| 14456 | ||
| 14457 | // whether to select first result after searching automatically |
|
| 14458 | selectFirstResult : false, |
|
| 14459 | ||
| 14460 | // API config |
|
| 14461 | apiSettings : false, |
|
| 14462 | ||
| 14463 | // object to search |
|
| 14464 | source : false, |
|
| 14465 | ||
| 14466 | // Whether search should query current term on focus |
|
| 14467 | searchOnFocus : true, |
|
| 14468 | ||
| 14469 | // fields to search |
|
| 14470 | searchFields : [ |
|
| 14471 | 'title', |
|
| 14472 | 'description' |
|
| 14473 | ], |
|
| 14474 | ||
| 14475 | // field to display in standard results template |
|
| 14476 | displayField : '', |
|
| 14477 | ||
| 14478 | // whether to include fuzzy results in local search |
|
| 14479 | searchFullText : true, |
|
| 14480 | ||
| 14481 | // whether to add events to prompt automatically |
|
| 14482 | automatic : true, |
|
| 14483 | ||
| 14484 | // delay before hiding menu after blur |
|
| 14485 | hideDelay : 0, |
|
| 14486 | ||
| 14487 | // delay before searching |
|
| 14488 | searchDelay : 200, |
|
| 14489 | ||
| 14490 | // maximum results returned from local |
|
| 14491 | maxResults : 7, |
|
| 14492 | ||
| 14493 | // whether to store lookups in local cache |
|
| 14494 | cache : true, |
|
| 14495 | ||
| 14496 | // whether no results errors should be shown |
|
| 14497 | showNoResults : true, |
|
| 14498 | ||
| 14499 | // transition settings |
|
| 14500 | transition : 'scale', |
|
| 14501 | duration : 200, |
|
| 14502 | easing : 'easeOutExpo', |
|
| 14503 | ||
| 14504 | // callbacks |
|
| 14505 | onSelect : false, |
|
| 14506 | onResultsAdd : false, |
|
| 14507 | ||
| 14508 | onSearchQuery : function(query){}, |
|
| 14509 | onResults : function(response){}, |
|
| 14510 | ||
| 14511 | onResultsOpen : function(){}, |
|
| 14512 | onResultsClose : function(){}, |
|
| 14513 | ||
| 14514 | className: { |
|
| 14515 | animating : 'animating', |
|
| 14516 | active : 'active', |
|
| 14517 | empty : 'empty', |
|
| 14518 | focus : 'focus', |
|
| 14519 | hidden : 'hidden', |
|
| 14520 | loading : 'loading', |
|
| 14521 | results : 'results', |
|
| 14522 | pressed : 'down' |
|
| 14523 | }, |
|
| 14524 | ||
| 14525 | error : { |
|
| 14526 | source : 'Cannot search. No source used, and Semantic API module was not included', |
|
| 14527 | noResults : 'Your search returned no results', |
|
| 14528 | logging : 'Error in debug logging, exiting.', |
|
| 14529 | noEndpoint : 'No search endpoint was specified', |
|
| 14530 | noTemplate : 'A valid template name was not specified.', |
|
| 14531 | serverError : 'There was an issue querying the server.', |
|
| 14532 | maxResults : 'Results must be an array to use maxResults setting', |
|
| 14533 | method : 'The method you called is not defined.' |
|
| 14534 | }, |
|
| 14535 | ||
| 14536 | metadata: { |
|
| 14537 | cache : 'cache', |
|
| 14538 | results : 'results', |
|
| 14539 | result : 'result' |
|
| 14540 | }, |
|
| 14541 | ||
| 14542 | regExp: { |
|
| 14543 | escape : /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, |
|
| 14544 | beginsWith : '(?:\s|^)' |
|
| 14545 | }, |
|
| 14546 | ||
| 14547 | // maps api response attributes to internal representation |
|
| 14548 | fields: { |
|
| 14549 | categories : 'results', // array of categories (category view) |
|
| 14550 | categoryName : 'name', // name of category (category view) |
|
| 14551 | categoryResults : 'results', // array of results (category view) |
|
| 14552 | description : 'description', // result description |
|
| 14553 | image : 'image', // result image |
|
| 14554 | price : 'price', // result price |
|
| 14555 | results : 'results', // array of results (standard) |
|
| 14556 | title : 'title', // result title |
|
| 14557 | url : 'url', // result url |
|
| 14558 | action : 'action', // "view more" object name |
|
| 14559 | actionText : 'text', // "view more" text |
|
| 14560 | actionURL : 'url' // "view more" url |
|
| 14561 | }, |
|
| 14562 | ||
| 14563 | selector : { |
|
| 14564 | prompt : '.prompt', |
|
| 14565 | searchButton : '.search.button', |
|
| 14566 | results : '.results', |
|
| 14567 | message : '.results > .message', |
|
| 14568 | category : '.category', |
|
| 14569 | result : '.result', |
|
| 14570 | title : '.title, .name' |
|
| 14571 | }, |
|
| 14572 | ||
| 14573 | templates: { |
|
| 14574 | escape: function(string) { |
|
| 14575 | var |
|
| 14576 | badChars = /[&<>"'`]/g, |
|
| 14577 | shouldEscape = /[&<>"'`]/, |
|
| 14578 | escape = { |
|
| 14579 | "&": "&", |
|
| 14580 | "<": "<", |
|
| 14581 | ">": ">", |
|
| 14582 | '"': """, |
|
| 14583 | "'": "'", |
|
| 14584 | "`": "`" |
|
| 14585 | }, |
|
| 14586 | escapedChar = function(chr) { |
|
| 14587 | return escape[chr]; |
|
| 14588 | } |
|
| 14589 | ; |
|
| 14590 | if(shouldEscape.test(string)) { |
|
| 14591 | return string.replace(badChars, escapedChar); |
|
| 14592 | } |
|
| 14593 | return string; |
|
| 14594 | }, |
|
| 14595 | message: function(message, type) { |
|
| 14596 | var |
|
| 14597 | html = '' |
|
| 14598 | ; |
|
| 14599 | if(message !== undefined && type !== undefined) { |
|
| 14600 | html += '' |
|
| 14601 | + '<div class="message ' + type + '">' |
|
| 14602 | ; |
|
| 14603 | // message type |
|
| 14604 | if(type == 'empty') { |
|
| 14605 | html += '' |
|
| 14606 | + '<div class="header">No Results</div class="header">' |
|
| 14607 | + '<div class="description">' + message + '</div class="description">' |
|
| 14608 | ; |
|
| 14609 | } |
|
| 14610 | else { |
|
| 14611 | html += ' <div class="description">' + message + '</div>'; |
|
| 14612 | } |
|
| 14613 | html += '</div>'; |
|
| 14614 | } |
|
| 14615 | return html; |
|
| 14616 | }, |
|
| 14617 | category: function(response, fields) { |
|
| 14618 | var |
|
| 14619 | html = '', |
|
| 14620 | escape = $.fn.search.settings.templates.escape |
|
| 14621 | ; |
|
| 14622 | if(response[fields.categoryResults] !== undefined) { |
|
| 14623 | ||
| 14624 | // each category |
|
| 14625 | $.each(response[fields.categoryResults], function(index, category) { |
|
| 14626 | if(category[fields.results] !== undefined && category.results.length > 0) { |
|
| 14627 | ||
| 14628 | html += '<div class="category">'; |
|
| 14629 | ||
| 14630 | if(category[fields.categoryName] !== undefined) { |
|
| 14631 | html += '<div class="name">' + category[fields.categoryName] + '</div>'; |
|
| 14632 | } |
|
| 14633 | ||
| 14634 | // each item inside category |
|
| 14635 | $.each(category.results, function(index, result) { |
|
| 14636 | if(result[fields.url]) { |
|
| 14637 | html += '<a class="result" href="' + result[fields.url] + '">'; |
|
| 14638 | } |
|
| 14639 | else { |
|
| 14640 | html += '<a class="result">'; |
|
| 14641 | } |
|
| 14642 | if(result[fields.image] !== undefined) { |
|
| 14643 | html += '' |
|
| 14644 | + '<div class="image">' |
|
| 14645 | + ' <img src="' + result[fields.image] + '">' |
|
| 14646 | + '</div>' |
|
| 14647 | ; |
|
| 14648 | } |
|
| 14649 | html += '<div class="content">'; |
|
| 14650 | if(result[fields.price] !== undefined) { |
|
| 14651 | html += '<div class="price">' + result[fields.price] + '</div>'; |
|
| 14652 | } |
|
| 14653 | if(result[fields.title] !== undefined) { |
|
| 14654 | html += '<div class="title">' + result[fields.title] + '</div>'; |
|
| 14655 | } |
|
| 14656 | if(result[fields.description] !== undefined) { |
|
| 14657 | html += '<div class="description">' + result[fields.description] + '</div>'; |
|
| 14658 | } |
|
| 14659 | html += '' |
|
| 14660 | + '</div>' |
|
| 14661 | ; |
|
| 14662 | html += '</a>'; |
|
| 14663 | }); |
|
| 14664 | html += '' |
|
| 14665 | + '</div>' |
|
| 14666 | ; |
|
| 14667 | } |
|
| 14668 | }); |
|
| 14669 | if(response[fields.action]) { |
|
| 14670 | html += '' |
|
| 14671 | + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">' |
|
| 14672 | + response[fields.action][fields.actionText] |
|
| 14673 | + '</a>'; |
|
| 14674 | } |
|
| 14675 | return html; |
|
| 14676 | } |
|
| 14677 | return false; |
|
| 14678 | }, |
|
| 14679 | standard: function(response, fields) { |
|
| 14680 | var |
|
| 14681 | html = '' |
|
| 14682 | ; |
|
| 14683 | if(response[fields.results] !== undefined) { |
|
| 14684 | ||
| 14685 | // each result |
|
| 14686 | $.each(response[fields.results], function(index, result) { |
|
| 14687 | if(result[fields.url]) { |
|
| 14688 | html += '<a class="result" href="' + result[fields.url] + '">'; |
|
| 14689 | } |
|
| 14690 | else { |
|
| 14691 | html += '<a class="result">'; |
|
| 14692 | } |
|
| 14693 | if(result[fields.image] !== undefined) { |
|
| 14694 | html += '' |
|
| 14695 | + '<div class="image">' |
|
| 14696 | + ' <img src="' + result[fields.image] + '">' |
|
| 14697 | + '</div>' |
|
| 14698 | ; |
|
| 14699 | } |
|
| 14700 | html += '<div class="content">'; |
|
| 14701 | if(result[fields.price] !== undefined) { |
|
| 14702 | html += '<div class="price">' + result[fields.price] + '</div>'; |
|
| 14703 | } |
|
| 14704 | if(result[fields.title] !== undefined) { |
|
| 14705 | html += '<div class="title">' + result[fields.title] + '</div>'; |
|
| 14706 | } |
|
| 14707 | if(result[fields.description] !== undefined) { |
|
| 14708 | html += '<div class="description">' + result[fields.description] + '</div>'; |
|
| 14709 | } |
|
| 14710 | html += '' |
|
| 14711 | + '</div>' |
|
| 14712 | ; |
|
| 14713 | html += '</a>'; |
|
| 14714 | }); |
|
| 14715 | ||
| 14716 | if(response[fields.action]) { |
|
| 14717 | html += '' |
|
| 14718 | + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">' |
|
| 14719 | + response[fields.action][fields.actionText] |
|
| 14720 | + '</a>'; |
|
| 14721 | } |
|
| 14722 | return html; |
|
| 14723 | } |
|
| 14724 | return false; |
|
| 14725 | } |
|
| 14726 | } |
|
| 14727 | }; |
|
| 14728 | ||
| 14729 | })( jQuery, window, document ); |
|
| 14730 | ||
| 14731 | /*! |
|
| 14732 | * # Semantic UI 2.2.11 - Shape |
|
| @@ 11-1451 (lines=1441) @@ | ||
| 8 | * |
|
| 9 | */ |
|
| 10 | ||
| 11 | ;(function ($, window, document, undefined) { |
|
| 12 | ||
| 13 | "use strict"; |
|
| 14 | ||
| 15 | window = (typeof window != 'undefined' && window.Math == Math) |
|
| 16 | ? window |
|
| 17 | : (typeof self != 'undefined' && self.Math == Math) |
|
| 18 | ? self |
|
| 19 | : Function('return this')() |
|
| 20 | ; |
|
| 21 | ||
| 22 | $.fn.search = function(parameters) { |
|
| 23 | var |
|
| 24 | $allModules = $(this), |
|
| 25 | moduleSelector = $allModules.selector || '', |
|
| 26 | ||
| 27 | time = new Date().getTime(), |
|
| 28 | performance = [], |
|
| 29 | ||
| 30 | query = arguments[0], |
|
| 31 | methodInvoked = (typeof query == 'string'), |
|
| 32 | queryArguments = [].slice.call(arguments, 1), |
|
| 33 | returnedValue |
|
| 34 | ; |
|
| 35 | $(this) |
|
| 36 | .each(function() { |
|
| 37 | var |
|
| 38 | settings = ( $.isPlainObject(parameters) ) |
|
| 39 | ? $.extend(true, {}, $.fn.search.settings, parameters) |
|
| 40 | : $.extend({}, $.fn.search.settings), |
|
| 41 | ||
| 42 | className = settings.className, |
|
| 43 | metadata = settings.metadata, |
|
| 44 | regExp = settings.regExp, |
|
| 45 | fields = settings.fields, |
|
| 46 | selector = settings.selector, |
|
| 47 | error = settings.error, |
|
| 48 | namespace = settings.namespace, |
|
| 49 | ||
| 50 | eventNamespace = '.' + namespace, |
|
| 51 | moduleNamespace = namespace + '-module', |
|
| 52 | ||
| 53 | $module = $(this), |
|
| 54 | $prompt = $module.find(selector.prompt), |
|
| 55 | $searchButton = $module.find(selector.searchButton), |
|
| 56 | $results = $module.find(selector.results), |
|
| 57 | $result = $module.find(selector.result), |
|
| 58 | $category = $module.find(selector.category), |
|
| 59 | ||
| 60 | element = this, |
|
| 61 | instance = $module.data(moduleNamespace), |
|
| 62 | ||
| 63 | disabledBubbled = false, |
|
| 64 | resultsDismissed = false, |
|
| 65 | ||
| 66 | module |
|
| 67 | ; |
|
| 68 | ||
| 69 | module = { |
|
| 70 | ||
| 71 | initialize: function() { |
|
| 72 | module.verbose('Initializing module'); |
|
| 73 | module.determine.searchFields(); |
|
| 74 | module.bind.events(); |
|
| 75 | module.set.type(); |
|
| 76 | module.create.results(); |
|
| 77 | module.instantiate(); |
|
| 78 | }, |
|
| 79 | instantiate: function() { |
|
| 80 | module.verbose('Storing instance of module', module); |
|
| 81 | instance = module; |
|
| 82 | $module |
|
| 83 | .data(moduleNamespace, module) |
|
| 84 | ; |
|
| 85 | }, |
|
| 86 | destroy: function() { |
|
| 87 | module.verbose('Destroying instance'); |
|
| 88 | $module |
|
| 89 | .off(eventNamespace) |
|
| 90 | .removeData(moduleNamespace) |
|
| 91 | ; |
|
| 92 | }, |
|
| 93 | ||
| 94 | refresh: function() { |
|
| 95 | module.debug('Refreshing selector cache'); |
|
| 96 | $prompt = $module.find(selector.prompt); |
|
| 97 | $searchButton = $module.find(selector.searchButton); |
|
| 98 | $category = $module.find(selector.category); |
|
| 99 | $results = $module.find(selector.results); |
|
| 100 | $result = $module.find(selector.result); |
|
| 101 | }, |
|
| 102 | ||
| 103 | refreshResults: function() { |
|
| 104 | $results = $module.find(selector.results); |
|
| 105 | $result = $module.find(selector.result); |
|
| 106 | }, |
|
| 107 | ||
| 108 | bind: { |
|
| 109 | events: function() { |
|
| 110 | module.verbose('Binding events to search'); |
|
| 111 | if(settings.automatic) { |
|
| 112 | $module |
|
| 113 | .on(module.get.inputEvent() + eventNamespace, selector.prompt, module.event.input) |
|
| 114 | ; |
|
| 115 | $prompt |
|
| 116 | .attr('autocomplete', 'off') |
|
| 117 | ; |
|
| 118 | } |
|
| 119 | $module |
|
| 120 | // prompt |
|
| 121 | .on('focus' + eventNamespace, selector.prompt, module.event.focus) |
|
| 122 | .on('blur' + eventNamespace, selector.prompt, module.event.blur) |
|
| 123 | .on('keydown' + eventNamespace, selector.prompt, module.handleKeyboard) |
|
| 124 | // search button |
|
| 125 | .on('click' + eventNamespace, selector.searchButton, module.query) |
|
| 126 | // results |
|
| 127 | .on('mousedown' + eventNamespace, selector.results, module.event.result.mousedown) |
|
| 128 | .on('mouseup' + eventNamespace, selector.results, module.event.result.mouseup) |
|
| 129 | .on('click' + eventNamespace, selector.result, module.event.result.click) |
|
| 130 | ; |
|
| 131 | } |
|
| 132 | }, |
|
| 133 | ||
| 134 | determine: { |
|
| 135 | searchFields: function() { |
|
| 136 | // this makes sure $.extend does not add specified search fields to default fields |
|
| 137 | // this is the only setting which should not extend defaults |
|
| 138 | if(parameters && parameters.searchFields !== undefined) { |
|
| 139 | settings.searchFields = parameters.searchFields; |
|
| 140 | } |
|
| 141 | } |
|
| 142 | }, |
|
| 143 | ||
| 144 | event: { |
|
| 145 | input: function() { |
|
| 146 | if(settings.searchDelay) { |
|
| 147 | clearTimeout(module.timer); |
|
| 148 | module.timer = setTimeout(function() { |
|
| 149 | if(module.is.focused()) { |
|
| 150 | module.query(); |
|
| 151 | } |
|
| 152 | }, settings.searchDelay); |
|
| 153 | } |
|
| 154 | else { |
|
| 155 | module.query(); |
|
| 156 | } |
|
| 157 | }, |
|
| 158 | focus: function() { |
|
| 159 | module.set.focus(); |
|
| 160 | if(settings.searchOnFocus && module.has.minimumCharacters() ) { |
|
| 161 | module.query(function() { |
|
| 162 | if(module.can.show() ) { |
|
| 163 | module.showResults(); |
|
| 164 | } |
|
| 165 | }); |
|
| 166 | } |
|
| 167 | }, |
|
| 168 | blur: function(event) { |
|
| 169 | var |
|
| 170 | pageLostFocus = (document.activeElement === this), |
|
| 171 | callback = function() { |
|
| 172 | module.cancel.query(); |
|
| 173 | module.remove.focus(); |
|
| 174 | module.timer = setTimeout(module.hideResults, settings.hideDelay); |
|
| 175 | } |
|
| 176 | ; |
|
| 177 | if(pageLostFocus) { |
|
| 178 | return; |
|
| 179 | } |
|
| 180 | resultsDismissed = false; |
|
| 181 | if(module.resultsClicked) { |
|
| 182 | module.debug('Determining if user action caused search to close'); |
|
| 183 | $module |
|
| 184 | .one('click.close' + eventNamespace, selector.results, function(event) { |
|
| 185 | if(module.is.inMessage(event) || disabledBubbled) { |
|
| 186 | $prompt.focus(); |
|
| 187 | return; |
|
| 188 | } |
|
| 189 | disabledBubbled = false; |
|
| 190 | if( !module.is.animating() && !module.is.hidden()) { |
|
| 191 | callback(); |
|
| 192 | } |
|
| 193 | }) |
|
| 194 | ; |
|
| 195 | } |
|
| 196 | else { |
|
| 197 | module.debug('Input blurred without user action, closing results'); |
|
| 198 | callback(); |
|
| 199 | } |
|
| 200 | }, |
|
| 201 | result: { |
|
| 202 | mousedown: function() { |
|
| 203 | module.resultsClicked = true; |
|
| 204 | }, |
|
| 205 | mouseup: function() { |
|
| 206 | module.resultsClicked = false; |
|
| 207 | }, |
|
| 208 | click: function(event) { |
|
| 209 | module.debug('Search result selected'); |
|
| 210 | var |
|
| 211 | $result = $(this), |
|
| 212 | $title = $result.find(selector.title).eq(0), |
|
| 213 | $link = $result.is('a[href]') |
|
| 214 | ? $result |
|
| 215 | : $result.find('a[href]').eq(0), |
|
| 216 | href = $link.attr('href') || false, |
|
| 217 | target = $link.attr('target') || false, |
|
| 218 | title = $title.html(), |
|
| 219 | // title is used for result lookup |
|
| 220 | value = ($title.length > 0) |
|
| 221 | ? $title.text() |
|
| 222 | : false, |
|
| 223 | results = module.get.results(), |
|
| 224 | result = $result.data(metadata.result) || module.get.result(value, results), |
|
| 225 | returnedValue |
|
| 226 | ; |
|
| 227 | if( $.isFunction(settings.onSelect) ) { |
|
| 228 | if(settings.onSelect.call(element, result, results) === false) { |
|
| 229 | module.debug('Custom onSelect callback cancelled default select action'); |
|
| 230 | disabledBubbled = true; |
|
| 231 | return; |
|
| 232 | } |
|
| 233 | } |
|
| 234 | module.hideResults(); |
|
| 235 | if(value) { |
|
| 236 | module.set.value(value); |
|
| 237 | } |
|
| 238 | if(href) { |
|
| 239 | module.verbose('Opening search link found in result', $link); |
|
| 240 | if(target == '_blank' || event.ctrlKey) { |
|
| 241 | window.open(href); |
|
| 242 | } |
|
| 243 | else { |
|
| 244 | window.location.href = (href); |
|
| 245 | } |
|
| 246 | } |
|
| 247 | } |
|
| 248 | } |
|
| 249 | }, |
|
| 250 | handleKeyboard: function(event) { |
|
| 251 | var |
|
| 252 | // force selector refresh |
|
| 253 | $result = $module.find(selector.result), |
|
| 254 | $category = $module.find(selector.category), |
|
| 255 | $activeResult = $result.filter('.' + className.active), |
|
| 256 | currentIndex = $result.index( $activeResult ), |
|
| 257 | resultSize = $result.length, |
|
| 258 | hasActiveResult = $activeResult.length > 0, |
|
| 259 | ||
| 260 | keyCode = event.which, |
|
| 261 | keys = { |
|
| 262 | backspace : 8, |
|
| 263 | enter : 13, |
|
| 264 | escape : 27, |
|
| 265 | upArrow : 38, |
|
| 266 | downArrow : 40 |
|
| 267 | }, |
|
| 268 | newIndex |
|
| 269 | ; |
|
| 270 | // search shortcuts |
|
| 271 | if(keyCode == keys.escape) { |
|
| 272 | module.verbose('Escape key pressed, blurring search field'); |
|
| 273 | module.hideResults(); |
|
| 274 | resultsDismissed = true; |
|
| 275 | } |
|
| 276 | if( module.is.visible() ) { |
|
| 277 | if(keyCode == keys.enter) { |
|
| 278 | module.verbose('Enter key pressed, selecting active result'); |
|
| 279 | if( $result.filter('.' + className.active).length > 0 ) { |
|
| 280 | module.event.result.click.call($result.filter('.' + className.active), event); |
|
| 281 | event.preventDefault(); |
|
| 282 | return false; |
|
| 283 | } |
|
| 284 | } |
|
| 285 | else if(keyCode == keys.upArrow && hasActiveResult) { |
|
| 286 | module.verbose('Up key pressed, changing active result'); |
|
| 287 | newIndex = (currentIndex - 1 < 0) |
|
| 288 | ? currentIndex |
|
| 289 | : currentIndex - 1 |
|
| 290 | ; |
|
| 291 | $category |
|
| 292 | .removeClass(className.active) |
|
| 293 | ; |
|
| 294 | $result |
|
| 295 | .removeClass(className.active) |
|
| 296 | .eq(newIndex) |
|
| 297 | .addClass(className.active) |
|
| 298 | .closest($category) |
|
| 299 | .addClass(className.active) |
|
| 300 | ; |
|
| 301 | event.preventDefault(); |
|
| 302 | } |
|
| 303 | else if(keyCode == keys.downArrow) { |
|
| 304 | module.verbose('Down key pressed, changing active result'); |
|
| 305 | newIndex = (currentIndex + 1 >= resultSize) |
|
| 306 | ? currentIndex |
|
| 307 | : currentIndex + 1 |
|
| 308 | ; |
|
| 309 | $category |
|
| 310 | .removeClass(className.active) |
|
| 311 | ; |
|
| 312 | $result |
|
| 313 | .removeClass(className.active) |
|
| 314 | .eq(newIndex) |
|
| 315 | .addClass(className.active) |
|
| 316 | .closest($category) |
|
| 317 | .addClass(className.active) |
|
| 318 | ; |
|
| 319 | event.preventDefault(); |
|
| 320 | } |
|
| 321 | } |
|
| 322 | else { |
|
| 323 | // query shortcuts |
|
| 324 | if(keyCode == keys.enter) { |
|
| 325 | module.verbose('Enter key pressed, executing query'); |
|
| 326 | module.query(); |
|
| 327 | module.set.buttonPressed(); |
|
| 328 | $prompt.one('keyup', module.remove.buttonFocus); |
|
| 329 | } |
|
| 330 | } |
|
| 331 | }, |
|
| 332 | ||
| 333 | setup: { |
|
| 334 | api: function(searchTerm, callback) { |
|
| 335 | var |
|
| 336 | apiSettings = { |
|
| 337 | debug : settings.debug, |
|
| 338 | on : false, |
|
| 339 | cache : true, |
|
| 340 | action : 'search', |
|
| 341 | urlData : { |
|
| 342 | query : searchTerm |
|
| 343 | }, |
|
| 344 | onSuccess : function(response) { |
|
| 345 | module.parse.response.call(element, response, searchTerm); |
|
| 346 | callback(); |
|
| 347 | }, |
|
| 348 | onFailure : function() { |
|
| 349 | module.displayMessage(error.serverError); |
|
| 350 | callback(); |
|
| 351 | }, |
|
| 352 | onAbort : function(response) { |
|
| 353 | }, |
|
| 354 | onError : module.error |
|
| 355 | }, |
|
| 356 | searchHTML |
|
| 357 | ; |
|
| 358 | $.extend(true, apiSettings, settings.apiSettings); |
|
| 359 | module.verbose('Setting up API request', apiSettings); |
|
| 360 | $module.api(apiSettings); |
|
| 361 | } |
|
| 362 | }, |
|
| 363 | ||
| 364 | can: { |
|
| 365 | useAPI: function() { |
|
| 366 | return $.fn.api !== undefined; |
|
| 367 | }, |
|
| 368 | show: function() { |
|
| 369 | return module.is.focused() && !module.is.visible() && !module.is.empty(); |
|
| 370 | }, |
|
| 371 | transition: function() { |
|
| 372 | return settings.transition && $.fn.transition !== undefined && $module.transition('is supported'); |
|
| 373 | } |
|
| 374 | }, |
|
| 375 | ||
| 376 | is: { |
|
| 377 | animating: function() { |
|
| 378 | return $results.hasClass(className.animating); |
|
| 379 | }, |
|
| 380 | hidden: function() { |
|
| 381 | return $results.hasClass(className.hidden); |
|
| 382 | }, |
|
| 383 | inMessage: function(event) { |
|
| 384 | if(!event.target) { |
|
| 385 | return; |
|
| 386 | } |
|
| 387 | var |
|
| 388 | $target = $(event.target), |
|
| 389 | isInDOM = $.contains(document.documentElement, event.target) |
|
| 390 | ; |
|
| 391 | return (isInDOM && $target.closest(selector.message).length > 0); |
|
| 392 | }, |
|
| 393 | empty: function() { |
|
| 394 | return ($results.html() === ''); |
|
| 395 | }, |
|
| 396 | visible: function() { |
|
| 397 | return ($results.filter(':visible').length > 0); |
|
| 398 | }, |
|
| 399 | focused: function() { |
|
| 400 | return ($prompt.filter(':focus').length > 0); |
|
| 401 | } |
|
| 402 | }, |
|
| 403 | ||
| 404 | get: { |
|
| 405 | inputEvent: function() { |
|
| 406 | var |
|
| 407 | prompt = $prompt[0], |
|
| 408 | inputEvent = (prompt !== undefined && prompt.oninput !== undefined) |
|
| 409 | ? 'input' |
|
| 410 | : (prompt !== undefined && prompt.onpropertychange !== undefined) |
|
| 411 | ? 'propertychange' |
|
| 412 | : 'keyup' |
|
| 413 | ; |
|
| 414 | return inputEvent; |
|
| 415 | }, |
|
| 416 | value: function() { |
|
| 417 | return $prompt.val(); |
|
| 418 | }, |
|
| 419 | results: function() { |
|
| 420 | var |
|
| 421 | results = $module.data(metadata.results) |
|
| 422 | ; |
|
| 423 | return results; |
|
| 424 | }, |
|
| 425 | result: function(value, results) { |
|
| 426 | var |
|
| 427 | lookupFields = ['title', 'id'], |
|
| 428 | result = false |
|
| 429 | ; |
|
| 430 | value = (value !== undefined) |
|
| 431 | ? value |
|
| 432 | : module.get.value() |
|
| 433 | ; |
|
| 434 | results = (results !== undefined) |
|
| 435 | ? results |
|
| 436 | : module.get.results() |
|
| 437 | ; |
|
| 438 | if(settings.type === 'category') { |
|
| 439 | module.debug('Finding result that matches', value); |
|
| 440 | $.each(results, function(index, category) { |
|
| 441 | if($.isArray(category.results)) { |
|
| 442 | result = module.search.object(value, category.results, lookupFields)[0]; |
|
| 443 | // don't continue searching if a result is found |
|
| 444 | if(result) { |
|
| 445 | return false; |
|
| 446 | } |
|
| 447 | } |
|
| 448 | }); |
|
| 449 | } |
|
| 450 | else { |
|
| 451 | module.debug('Finding result in results object', value); |
|
| 452 | result = module.search.object(value, results, lookupFields)[0]; |
|
| 453 | } |
|
| 454 | return result || false; |
|
| 455 | }, |
|
| 456 | }, |
|
| 457 | ||
| 458 | select: { |
|
| 459 | firstResult: function() { |
|
| 460 | module.verbose('Selecting first result'); |
|
| 461 | $result.first().addClass(className.active); |
|
| 462 | } |
|
| 463 | }, |
|
| 464 | ||
| 465 | set: { |
|
| 466 | focus: function() { |
|
| 467 | $module.addClass(className.focus); |
|
| 468 | }, |
|
| 469 | loading: function() { |
|
| 470 | $module.addClass(className.loading); |
|
| 471 | }, |
|
| 472 | value: function(value) { |
|
| 473 | module.verbose('Setting search input value', value); |
|
| 474 | $prompt |
|
| 475 | .val(value) |
|
| 476 | ; |
|
| 477 | }, |
|
| 478 | type: function(type) { |
|
| 479 | type = type || settings.type; |
|
| 480 | if(settings.type == 'category') { |
|
| 481 | $module.addClass(settings.type); |
|
| 482 | } |
|
| 483 | }, |
|
| 484 | buttonPressed: function() { |
|
| 485 | $searchButton.addClass(className.pressed); |
|
| 486 | } |
|
| 487 | }, |
|
| 488 | ||
| 489 | remove: { |
|
| 490 | loading: function() { |
|
| 491 | $module.removeClass(className.loading); |
|
| 492 | }, |
|
| 493 | focus: function() { |
|
| 494 | $module.removeClass(className.focus); |
|
| 495 | }, |
|
| 496 | buttonPressed: function() { |
|
| 497 | $searchButton.removeClass(className.pressed); |
|
| 498 | } |
|
| 499 | }, |
|
| 500 | ||
| 501 | query: function(callback) { |
|
| 502 | callback = $.isFunction(callback) |
|
| 503 | ? callback |
|
| 504 | : function(){} |
|
| 505 | ; |
|
| 506 | var |
|
| 507 | searchTerm = module.get.value(), |
|
| 508 | cache = module.read.cache(searchTerm) |
|
| 509 | ; |
|
| 510 | callback = callback || function() {}; |
|
| 511 | if( module.has.minimumCharacters() ) { |
|
| 512 | if(cache) { |
|
| 513 | module.debug('Reading result from cache', searchTerm); |
|
| 514 | module.save.results(cache.results); |
|
| 515 | module.addResults(cache.html); |
|
| 516 | module.inject.id(cache.results); |
|
| 517 | callback(); |
|
| 518 | } |
|
| 519 | else { |
|
| 520 | module.debug('Querying for', searchTerm); |
|
| 521 | if($.isPlainObject(settings.source) || $.isArray(settings.source)) { |
|
| 522 | module.search.local(searchTerm); |
|
| 523 | callback(); |
|
| 524 | } |
|
| 525 | else if( module.can.useAPI() ) { |
|
| 526 | module.search.remote(searchTerm, callback); |
|
| 527 | } |
|
| 528 | else { |
|
| 529 | module.error(error.source); |
|
| 530 | callback(); |
|
| 531 | } |
|
| 532 | } |
|
| 533 | settings.onSearchQuery.call(element, searchTerm); |
|
| 534 | } |
|
| 535 | else { |
|
| 536 | module.hideResults(); |
|
| 537 | } |
|
| 538 | }, |
|
| 539 | ||
| 540 | search: { |
|
| 541 | local: function(searchTerm) { |
|
| 542 | var |
|
| 543 | results = module.search.object(searchTerm, settings.content), |
|
| 544 | searchHTML |
|
| 545 | ; |
|
| 546 | module.set.loading(); |
|
| 547 | module.save.results(results); |
|
| 548 | module.debug('Returned local search results', results); |
|
| 549 | ||
| 550 | searchHTML = module.generateResults({ |
|
| 551 | results: results |
|
| 552 | }); |
|
| 553 | module.remove.loading(); |
|
| 554 | module.addResults(searchHTML); |
|
| 555 | module.inject.id(results); |
|
| 556 | module.write.cache(searchTerm, { |
|
| 557 | html : searchHTML, |
|
| 558 | results : results |
|
| 559 | }); |
|
| 560 | }, |
|
| 561 | remote: function(searchTerm, callback) { |
|
| 562 | callback = $.isFunction(callback) |
|
| 563 | ? callback |
|
| 564 | : function(){} |
|
| 565 | ; |
|
| 566 | if($module.api('is loading')) { |
|
| 567 | $module.api('abort'); |
|
| 568 | } |
|
| 569 | module.setup.api(searchTerm, callback); |
|
| 570 | $module |
|
| 571 | .api('query') |
|
| 572 | ; |
|
| 573 | }, |
|
| 574 | object: function(searchTerm, source, searchFields) { |
|
| 575 | var |
|
| 576 | results = [], |
|
| 577 | fuzzyResults = [], |
|
| 578 | searchExp = searchTerm.toString().replace(regExp.escape, '\\$&'), |
|
| 579 | matchRegExp = new RegExp(regExp.beginsWith + searchExp, 'i'), |
|
| 580 | ||
| 581 | // avoid duplicates when pushing results |
|
| 582 | addResult = function(array, result) { |
|
| 583 | var |
|
| 584 | notResult = ($.inArray(result, results) == -1), |
|
| 585 | notFuzzyResult = ($.inArray(result, fuzzyResults) == -1) |
|
| 586 | ; |
|
| 587 | if(notResult && notFuzzyResult) { |
|
| 588 | array.push(result); |
|
| 589 | } |
|
| 590 | } |
|
| 591 | ; |
|
| 592 | source = source || settings.source; |
|
| 593 | searchFields = (searchFields !== undefined) |
|
| 594 | ? searchFields |
|
| 595 | : settings.searchFields |
|
| 596 | ; |
|
| 597 | ||
| 598 | // search fields should be array to loop correctly |
|
| 599 | if(!$.isArray(searchFields)) { |
|
| 600 | searchFields = [searchFields]; |
|
| 601 | } |
|
| 602 | ||
| 603 | // exit conditions if no source |
|
| 604 | if(source === undefined || source === false) { |
|
| 605 | module.error(error.source); |
|
| 606 | return []; |
|
| 607 | } |
|
| 608 | ||
| 609 | // iterate through search fields looking for matches |
|
| 610 | $.each(searchFields, function(index, field) { |
|
| 611 | $.each(source, function(label, content) { |
|
| 612 | var |
|
| 613 | fieldExists = (typeof content[field] == 'string') |
|
| 614 | ; |
|
| 615 | if(fieldExists) { |
|
| 616 | if( content[field].search(matchRegExp) !== -1) { |
|
| 617 | // content starts with value (first in results) |
|
| 618 | addResult(results, content); |
|
| 619 | } |
|
| 620 | else if(settings.searchFullText && module.fuzzySearch(searchTerm, content[field]) ) { |
|
| 621 | // content fuzzy matches (last in results) |
|
| 622 | addResult(fuzzyResults, content); |
|
| 623 | } |
|
| 624 | } |
|
| 625 | }); |
|
| 626 | }); |
|
| 627 | return $.merge(results, fuzzyResults); |
|
| 628 | } |
|
| 629 | }, |
|
| 630 | ||
| 631 | fuzzySearch: function(query, term) { |
|
| 632 | var |
|
| 633 | termLength = term.length, |
|
| 634 | queryLength = query.length |
|
| 635 | ; |
|
| 636 | if(typeof query !== 'string') { |
|
| 637 | return false; |
|
| 638 | } |
|
| 639 | query = query.toLowerCase(); |
|
| 640 | term = term.toLowerCase(); |
|
| 641 | if(queryLength > termLength) { |
|
| 642 | return false; |
|
| 643 | } |
|
| 644 | if(queryLength === termLength) { |
|
| 645 | return (query === term); |
|
| 646 | } |
|
| 647 | search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) { |
|
| 648 | var |
|
| 649 | queryCharacter = query.charCodeAt(characterIndex) |
|
| 650 | ; |
|
| 651 | while(nextCharacterIndex < termLength) { |
|
| 652 | if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) { |
|
| 653 | continue search; |
|
| 654 | } |
|
| 655 | } |
|
| 656 | return false; |
|
| 657 | } |
|
| 658 | return true; |
|
| 659 | }, |
|
| 660 | ||
| 661 | parse: { |
|
| 662 | response: function(response, searchTerm) { |
|
| 663 | var |
|
| 664 | searchHTML = module.generateResults(response) |
|
| 665 | ; |
|
| 666 | module.verbose('Parsing server response', response); |
|
| 667 | if(response !== undefined) { |
|
| 668 | if(searchTerm !== undefined && response[fields.results] !== undefined) { |
|
| 669 | module.addResults(searchHTML); |
|
| 670 | module.inject.id(response[fields.results]); |
|
| 671 | module.write.cache(searchTerm, { |
|
| 672 | html : searchHTML, |
|
| 673 | results : response[fields.results] |
|
| 674 | }); |
|
| 675 | module.save.results(response[fields.results]); |
|
| 676 | } |
|
| 677 | } |
|
| 678 | } |
|
| 679 | }, |
|
| 680 | ||
| 681 | cancel: { |
|
| 682 | query: function() { |
|
| 683 | if( module.can.useAPI() ) { |
|
| 684 | $module.api('abort'); |
|
| 685 | } |
|
| 686 | } |
|
| 687 | }, |
|
| 688 | ||
| 689 | has: { |
|
| 690 | minimumCharacters: function() { |
|
| 691 | var |
|
| 692 | searchTerm = module.get.value(), |
|
| 693 | numCharacters = searchTerm.length |
|
| 694 | ; |
|
| 695 | return (numCharacters >= settings.minCharacters); |
|
| 696 | }, |
|
| 697 | results: function() { |
|
| 698 | if($results.length === 0) { |
|
| 699 | return false; |
|
| 700 | } |
|
| 701 | var |
|
| 702 | html = $results.html() |
|
| 703 | ; |
|
| 704 | return html != ''; |
|
| 705 | } |
|
| 706 | }, |
|
| 707 | ||
| 708 | clear: { |
|
| 709 | cache: function(value) { |
|
| 710 | var |
|
| 711 | cache = $module.data(metadata.cache) |
|
| 712 | ; |
|
| 713 | if(!value) { |
|
| 714 | module.debug('Clearing cache', value); |
|
| 715 | $module.removeData(metadata.cache); |
|
| 716 | } |
|
| 717 | else if(value && cache && cache[value]) { |
|
| 718 | module.debug('Removing value from cache', value); |
|
| 719 | delete cache[value]; |
|
| 720 | $module.data(metadata.cache, cache); |
|
| 721 | } |
|
| 722 | } |
|
| 723 | }, |
|
| 724 | ||
| 725 | read: { |
|
| 726 | cache: function(name) { |
|
| 727 | var |
|
| 728 | cache = $module.data(metadata.cache) |
|
| 729 | ; |
|
| 730 | if(settings.cache) { |
|
| 731 | module.verbose('Checking cache for generated html for query', name); |
|
| 732 | return (typeof cache == 'object') && (cache[name] !== undefined) |
|
| 733 | ? cache[name] |
|
| 734 | : false |
|
| 735 | ; |
|
| 736 | } |
|
| 737 | return false; |
|
| 738 | } |
|
| 739 | }, |
|
| 740 | ||
| 741 | create: { |
|
| 742 | id: function(resultIndex, categoryIndex) { |
|
| 743 | var |
|
| 744 | resultID = (resultIndex + 1), // not zero indexed |
|
| 745 | categoryID = (categoryIndex + 1), |
|
| 746 | firstCharCode, |
|
| 747 | letterID, |
|
| 748 | id |
|
| 749 | ; |
|
| 750 | if(categoryIndex !== undefined) { |
|
| 751 | // start char code for "A" |
|
| 752 | letterID = String.fromCharCode(97 + categoryIndex); |
|
| 753 | id = letterID + resultID; |
|
| 754 | module.verbose('Creating category result id', id); |
|
| 755 | } |
|
| 756 | else { |
|
| 757 | id = resultID; |
|
| 758 | module.verbose('Creating result id', id); |
|
| 759 | } |
|
| 760 | return id; |
|
| 761 | }, |
|
| 762 | results: function() { |
|
| 763 | if($results.length === 0) { |
|
| 764 | $results = $('<div />') |
|
| 765 | .addClass(className.results) |
|
| 766 | .appendTo($module) |
|
| 767 | ; |
|
| 768 | } |
|
| 769 | } |
|
| 770 | }, |
|
| 771 | ||
| 772 | inject: { |
|
| 773 | result: function(result, resultIndex, categoryIndex) { |
|
| 774 | module.verbose('Injecting result into results'); |
|
| 775 | var |
|
| 776 | $selectedResult = (categoryIndex !== undefined) |
|
| 777 | ? $results |
|
| 778 | .children().eq(categoryIndex) |
|
| 779 | .children(selector.result).eq(resultIndex) |
|
| 780 | : $results |
|
| 781 | .children(selector.result).eq(resultIndex) |
|
| 782 | ; |
|
| 783 | module.verbose('Injecting results metadata', $selectedResult); |
|
| 784 | $selectedResult |
|
| 785 | .data(metadata.result, result) |
|
| 786 | ; |
|
| 787 | }, |
|
| 788 | id: function(results) { |
|
| 789 | module.debug('Injecting unique ids into results'); |
|
| 790 | var |
|
| 791 | // since results may be object, we must use counters |
|
| 792 | categoryIndex = 0, |
|
| 793 | resultIndex = 0 |
|
| 794 | ; |
|
| 795 | if(settings.type === 'category') { |
|
| 796 | // iterate through each category result |
|
| 797 | $.each(results, function(index, category) { |
|
| 798 | resultIndex = 0; |
|
| 799 | $.each(category.results, function(index, value) { |
|
| 800 | var |
|
| 801 | result = category.results[index] |
|
| 802 | ; |
|
| 803 | if(result.id === undefined) { |
|
| 804 | result.id = module.create.id(resultIndex, categoryIndex); |
|
| 805 | } |
|
| 806 | module.inject.result(result, resultIndex, categoryIndex); |
|
| 807 | resultIndex++; |
|
| 808 | }); |
|
| 809 | categoryIndex++; |
|
| 810 | }); |
|
| 811 | } |
|
| 812 | else { |
|
| 813 | // top level |
|
| 814 | $.each(results, function(index, value) { |
|
| 815 | var |
|
| 816 | result = results[index] |
|
| 817 | ; |
|
| 818 | if(result.id === undefined) { |
|
| 819 | result.id = module.create.id(resultIndex); |
|
| 820 | } |
|
| 821 | module.inject.result(result, resultIndex); |
|
| 822 | resultIndex++; |
|
| 823 | }); |
|
| 824 | } |
|
| 825 | return results; |
|
| 826 | } |
|
| 827 | }, |
|
| 828 | ||
| 829 | save: { |
|
| 830 | results: function(results) { |
|
| 831 | module.verbose('Saving current search results to metadata', results); |
|
| 832 | $module.data(metadata.results, results); |
|
| 833 | } |
|
| 834 | }, |
|
| 835 | ||
| 836 | write: { |
|
| 837 | cache: function(name, value) { |
|
| 838 | var |
|
| 839 | cache = ($module.data(metadata.cache) !== undefined) |
|
| 840 | ? $module.data(metadata.cache) |
|
| 841 | : {} |
|
| 842 | ; |
|
| 843 | if(settings.cache) { |
|
| 844 | module.verbose('Writing generated html to cache', name, value); |
|
| 845 | cache[name] = value; |
|
| 846 | $module |
|
| 847 | .data(metadata.cache, cache) |
|
| 848 | ; |
|
| 849 | } |
|
| 850 | } |
|
| 851 | }, |
|
| 852 | ||
| 853 | addResults: function(html) { |
|
| 854 | if( $.isFunction(settings.onResultsAdd) ) { |
|
| 855 | if( settings.onResultsAdd.call($results, html) === false ) { |
|
| 856 | module.debug('onResultsAdd callback cancelled default action'); |
|
| 857 | return false; |
|
| 858 | } |
|
| 859 | } |
|
| 860 | if(html) { |
|
| 861 | $results |
|
| 862 | .html(html) |
|
| 863 | ; |
|
| 864 | module.refreshResults(); |
|
| 865 | if(settings.selectFirstResult) { |
|
| 866 | module.select.firstResult(); |
|
| 867 | } |
|
| 868 | module.showResults(); |
|
| 869 | } |
|
| 870 | else { |
|
| 871 | module.hideResults(function() { |
|
| 872 | $results.empty(); |
|
| 873 | }); |
|
| 874 | } |
|
| 875 | }, |
|
| 876 | ||
| 877 | showResults: function(callback) { |
|
| 878 | callback = $.isFunction(callback) |
|
| 879 | ? callback |
|
| 880 | : function(){} |
|
| 881 | ; |
|
| 882 | if(resultsDismissed) { |
|
| 883 | return; |
|
| 884 | } |
|
| 885 | if(!module.is.visible() && module.has.results()) { |
|
| 886 | if( module.can.transition() ) { |
|
| 887 | module.debug('Showing results with css animations'); |
|
| 888 | $results |
|
| 889 | .transition({ |
|
| 890 | animation : settings.transition + ' in', |
|
| 891 | debug : settings.debug, |
|
| 892 | verbose : settings.verbose, |
|
| 893 | duration : settings.duration, |
|
| 894 | onComplete : function() { |
|
| 895 | callback(); |
|
| 896 | }, |
|
| 897 | queue : true |
|
| 898 | }) |
|
| 899 | ; |
|
| 900 | } |
|
| 901 | else { |
|
| 902 | module.debug('Showing results with javascript'); |
|
| 903 | $results |
|
| 904 | .stop() |
|
| 905 | .fadeIn(settings.duration, settings.easing) |
|
| 906 | ; |
|
| 907 | } |
|
| 908 | settings.onResultsOpen.call($results); |
|
| 909 | } |
|
| 910 | }, |
|
| 911 | hideResults: function(callback) { |
|
| 912 | callback = $.isFunction(callback) |
|
| 913 | ? callback |
|
| 914 | : function(){} |
|
| 915 | ; |
|
| 916 | if( module.is.visible() ) { |
|
| 917 | if( module.can.transition() ) { |
|
| 918 | module.debug('Hiding results with css animations'); |
|
| 919 | $results |
|
| 920 | .transition({ |
|
| 921 | animation : settings.transition + ' out', |
|
| 922 | debug : settings.debug, |
|
| 923 | verbose : settings.verbose, |
|
| 924 | duration : settings.duration, |
|
| 925 | onComplete : function() { |
|
| 926 | callback(); |
|
| 927 | }, |
|
| 928 | queue : true |
|
| 929 | }) |
|
| 930 | ; |
|
| 931 | } |
|
| 932 | else { |
|
| 933 | module.debug('Hiding results with javascript'); |
|
| 934 | $results |
|
| 935 | .stop() |
|
| 936 | .fadeOut(settings.duration, settings.easing) |
|
| 937 | ; |
|
| 938 | } |
|
| 939 | settings.onResultsClose.call($results); |
|
| 940 | } |
|
| 941 | }, |
|
| 942 | ||
| 943 | generateResults: function(response) { |
|
| 944 | module.debug('Generating html from response', response); |
|
| 945 | var |
|
| 946 | template = settings.templates[settings.type], |
|
| 947 | isProperObject = ($.isPlainObject(response[fields.results]) && !$.isEmptyObject(response[fields.results])), |
|
| 948 | isProperArray = ($.isArray(response[fields.results]) && response[fields.results].length > 0), |
|
| 949 | html = '' |
|
| 950 | ; |
|
| 951 | if(isProperObject || isProperArray ) { |
|
| 952 | if(settings.maxResults > 0) { |
|
| 953 | if(isProperObject) { |
|
| 954 | if(settings.type == 'standard') { |
|
| 955 | module.error(error.maxResults); |
|
| 956 | } |
|
| 957 | } |
|
| 958 | else { |
|
| 959 | response[fields.results] = response[fields.results].slice(0, settings.maxResults); |
|
| 960 | } |
|
| 961 | } |
|
| 962 | if($.isFunction(template)) { |
|
| 963 | html = template(response, fields); |
|
| 964 | } |
|
| 965 | else { |
|
| 966 | module.error(error.noTemplate, false); |
|
| 967 | } |
|
| 968 | } |
|
| 969 | else if(settings.showNoResults) { |
|
| 970 | html = module.displayMessage(error.noResults, 'empty'); |
|
| 971 | } |
|
| 972 | settings.onResults.call(element, response); |
|
| 973 | return html; |
|
| 974 | }, |
|
| 975 | ||
| 976 | displayMessage: function(text, type) { |
|
| 977 | type = type || 'standard'; |
|
| 978 | module.debug('Displaying message', text, type); |
|
| 979 | module.addResults( settings.templates.message(text, type) ); |
|
| 980 | return settings.templates.message(text, type); |
|
| 981 | }, |
|
| 982 | ||
| 983 | setting: function(name, value) { |
|
| 984 | if( $.isPlainObject(name) ) { |
|
| 985 | $.extend(true, settings, name); |
|
| 986 | } |
|
| 987 | else if(value !== undefined) { |
|
| 988 | settings[name] = value; |
|
| 989 | } |
|
| 990 | else { |
|
| 991 | return settings[name]; |
|
| 992 | } |
|
| 993 | }, |
|
| 994 | internal: function(name, value) { |
|
| 995 | if( $.isPlainObject(name) ) { |
|
| 996 | $.extend(true, module, name); |
|
| 997 | } |
|
| 998 | else if(value !== undefined) { |
|
| 999 | module[name] = value; |
|
| 1000 | } |
|
| 1001 | else { |
|
| 1002 | return module[name]; |
|
| 1003 | } |
|
| 1004 | }, |
|
| 1005 | debug: function() { |
|
| 1006 | if(!settings.silent && settings.debug) { |
|
| 1007 | if(settings.performance) { |
|
| 1008 | module.performance.log(arguments); |
|
| 1009 | } |
|
| 1010 | else { |
|
| 1011 | module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); |
|
| 1012 | module.debug.apply(console, arguments); |
|
| 1013 | } |
|
| 1014 | } |
|
| 1015 | }, |
|
| 1016 | verbose: function() { |
|
| 1017 | if(!settings.silent && settings.verbose && settings.debug) { |
|
| 1018 | if(settings.performance) { |
|
| 1019 | module.performance.log(arguments); |
|
| 1020 | } |
|
| 1021 | else { |
|
| 1022 | module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); |
|
| 1023 | module.verbose.apply(console, arguments); |
|
| 1024 | } |
|
| 1025 | } |
|
| 1026 | }, |
|
| 1027 | error: function() { |
|
| 1028 | if(!settings.silent) { |
|
| 1029 | module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); |
|
| 1030 | module.error.apply(console, arguments); |
|
| 1031 | } |
|
| 1032 | }, |
|
| 1033 | performance: { |
|
| 1034 | log: function(message) { |
|
| 1035 | var |
|
| 1036 | currentTime, |
|
| 1037 | executionTime, |
|
| 1038 | previousTime |
|
| 1039 | ; |
|
| 1040 | if(settings.performance) { |
|
| 1041 | currentTime = new Date().getTime(); |
|
| 1042 | previousTime = time || currentTime; |
|
| 1043 | executionTime = currentTime - previousTime; |
|
| 1044 | time = currentTime; |
|
| 1045 | performance.push({ |
|
| 1046 | 'Name' : message[0], |
|
| 1047 | 'Arguments' : [].slice.call(message, 1) || '', |
|
| 1048 | 'Element' : element, |
|
| 1049 | 'Execution Time' : executionTime |
|
| 1050 | }); |
|
| 1051 | } |
|
| 1052 | clearTimeout(module.performance.timer); |
|
| 1053 | module.performance.timer = setTimeout(module.performance.display, 500); |
|
| 1054 | }, |
|
| 1055 | display: function() { |
|
| 1056 | var |
|
| 1057 | title = settings.name + ':', |
|
| 1058 | totalTime = 0 |
|
| 1059 | ; |
|
| 1060 | time = false; |
|
| 1061 | clearTimeout(module.performance.timer); |
|
| 1062 | $.each(performance, function(index, data) { |
|
| 1063 | totalTime += data['Execution Time']; |
|
| 1064 | }); |
|
| 1065 | title += ' ' + totalTime + 'ms'; |
|
| 1066 | if(moduleSelector) { |
|
| 1067 | title += ' \'' + moduleSelector + '\''; |
|
| 1068 | } |
|
| 1069 | if($allModules.length > 1) { |
|
| 1070 | title += ' ' + '(' + $allModules.length + ')'; |
|
| 1071 | } |
|
| 1072 | if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { |
|
| 1073 | console.groupCollapsed(title); |
|
| 1074 | if(console.table) { |
|
| 1075 | console.table(performance); |
|
| 1076 | } |
|
| 1077 | else { |
|
| 1078 | $.each(performance, function(index, data) { |
|
| 1079 | console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); |
|
| 1080 | }); |
|
| 1081 | } |
|
| 1082 | console.groupEnd(); |
|
| 1083 | } |
|
| 1084 | performance = []; |
|
| 1085 | } |
|
| 1086 | }, |
|
| 1087 | invoke: function(query, passedArguments, context) { |
|
| 1088 | var |
|
| 1089 | object = instance, |
|
| 1090 | maxDepth, |
|
| 1091 | found, |
|
| 1092 | response |
|
| 1093 | ; |
|
| 1094 | passedArguments = passedArguments || queryArguments; |
|
| 1095 | context = element || context; |
|
| 1096 | if(typeof query == 'string' && object !== undefined) { |
|
| 1097 | query = query.split(/[\. ]/); |
|
| 1098 | maxDepth = query.length - 1; |
|
| 1099 | $.each(query, function(depth, value) { |
|
| 1100 | var camelCaseValue = (depth != maxDepth) |
|
| 1101 | ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) |
|
| 1102 | : query |
|
| 1103 | ; |
|
| 1104 | if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { |
|
| 1105 | object = object[camelCaseValue]; |
|
| 1106 | } |
|
| 1107 | else if( object[camelCaseValue] !== undefined ) { |
|
| 1108 | found = object[camelCaseValue]; |
|
| 1109 | return false; |
|
| 1110 | } |
|
| 1111 | else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { |
|
| 1112 | object = object[value]; |
|
| 1113 | } |
|
| 1114 | else if( object[value] !== undefined ) { |
|
| 1115 | found = object[value]; |
|
| 1116 | return false; |
|
| 1117 | } |
|
| 1118 | else { |
|
| 1119 | return false; |
|
| 1120 | } |
|
| 1121 | }); |
|
| 1122 | } |
|
| 1123 | if( $.isFunction( found ) ) { |
|
| 1124 | response = found.apply(context, passedArguments); |
|
| 1125 | } |
|
| 1126 | else if(found !== undefined) { |
|
| 1127 | response = found; |
|
| 1128 | } |
|
| 1129 | if($.isArray(returnedValue)) { |
|
| 1130 | returnedValue.push(response); |
|
| 1131 | } |
|
| 1132 | else if(returnedValue !== undefined) { |
|
| 1133 | returnedValue = [returnedValue, response]; |
|
| 1134 | } |
|
| 1135 | else if(response !== undefined) { |
|
| 1136 | returnedValue = response; |
|
| 1137 | } |
|
| 1138 | return found; |
|
| 1139 | } |
|
| 1140 | }; |
|
| 1141 | if(methodInvoked) { |
|
| 1142 | if(instance === undefined) { |
|
| 1143 | module.initialize(); |
|
| 1144 | } |
|
| 1145 | module.invoke(query); |
|
| 1146 | } |
|
| 1147 | else { |
|
| 1148 | if(instance !== undefined) { |
|
| 1149 | instance.invoke('destroy'); |
|
| 1150 | } |
|
| 1151 | module.initialize(); |
|
| 1152 | } |
|
| 1153 | ||
| 1154 | }) |
|
| 1155 | ; |
|
| 1156 | ||
| 1157 | return (returnedValue !== undefined) |
|
| 1158 | ? returnedValue |
|
| 1159 | : this |
|
| 1160 | ; |
|
| 1161 | }; |
|
| 1162 | ||
| 1163 | $.fn.search.settings = { |
|
| 1164 | ||
| 1165 | name : 'Search', |
|
| 1166 | namespace : 'search', |
|
| 1167 | ||
| 1168 | silent : false, |
|
| 1169 | debug : false, |
|
| 1170 | verbose : false, |
|
| 1171 | performance : true, |
|
| 1172 | ||
| 1173 | // template to use (specified in settings.templates) |
|
| 1174 | type : 'standard', |
|
| 1175 | ||
| 1176 | // minimum characters required to search |
|
| 1177 | minCharacters : 1, |
|
| 1178 | ||
| 1179 | // whether to select first result after searching automatically |
|
| 1180 | selectFirstResult : false, |
|
| 1181 | ||
| 1182 | // API config |
|
| 1183 | apiSettings : false, |
|
| 1184 | ||
| 1185 | // object to search |
|
| 1186 | source : false, |
|
| 1187 | ||
| 1188 | // Whether search should query current term on focus |
|
| 1189 | searchOnFocus : true, |
|
| 1190 | ||
| 1191 | // fields to search |
|
| 1192 | searchFields : [ |
|
| 1193 | 'title', |
|
| 1194 | 'description' |
|
| 1195 | ], |
|
| 1196 | ||
| 1197 | // field to display in standard results template |
|
| 1198 | displayField : '', |
|
| 1199 | ||
| 1200 | // whether to include fuzzy results in local search |
|
| 1201 | searchFullText : true, |
|
| 1202 | ||
| 1203 | // whether to add events to prompt automatically |
|
| 1204 | automatic : true, |
|
| 1205 | ||
| 1206 | // delay before hiding menu after blur |
|
| 1207 | hideDelay : 0, |
|
| 1208 | ||
| 1209 | // delay before searching |
|
| 1210 | searchDelay : 200, |
|
| 1211 | ||
| 1212 | // maximum results returned from local |
|
| 1213 | maxResults : 7, |
|
| 1214 | ||
| 1215 | // whether to store lookups in local cache |
|
| 1216 | cache : true, |
|
| 1217 | ||
| 1218 | // whether no results errors should be shown |
|
| 1219 | showNoResults : true, |
|
| 1220 | ||
| 1221 | // transition settings |
|
| 1222 | transition : 'scale', |
|
| 1223 | duration : 200, |
|
| 1224 | easing : 'easeOutExpo', |
|
| 1225 | ||
| 1226 | // callbacks |
|
| 1227 | onSelect : false, |
|
| 1228 | onResultsAdd : false, |
|
| 1229 | ||
| 1230 | onSearchQuery : function(query){}, |
|
| 1231 | onResults : function(response){}, |
|
| 1232 | ||
| 1233 | onResultsOpen : function(){}, |
|
| 1234 | onResultsClose : function(){}, |
|
| 1235 | ||
| 1236 | className: { |
|
| 1237 | animating : 'animating', |
|
| 1238 | active : 'active', |
|
| 1239 | empty : 'empty', |
|
| 1240 | focus : 'focus', |
|
| 1241 | hidden : 'hidden', |
|
| 1242 | loading : 'loading', |
|
| 1243 | results : 'results', |
|
| 1244 | pressed : 'down' |
|
| 1245 | }, |
|
| 1246 | ||
| 1247 | error : { |
|
| 1248 | source : 'Cannot search. No source used, and Semantic API module was not included', |
|
| 1249 | noResults : 'Your search returned no results', |
|
| 1250 | logging : 'Error in debug logging, exiting.', |
|
| 1251 | noEndpoint : 'No search endpoint was specified', |
|
| 1252 | noTemplate : 'A valid template name was not specified.', |
|
| 1253 | serverError : 'There was an issue querying the server.', |
|
| 1254 | maxResults : 'Results must be an array to use maxResults setting', |
|
| 1255 | method : 'The method you called is not defined.' |
|
| 1256 | }, |
|
| 1257 | ||
| 1258 | metadata: { |
|
| 1259 | cache : 'cache', |
|
| 1260 | results : 'results', |
|
| 1261 | result : 'result' |
|
| 1262 | }, |
|
| 1263 | ||
| 1264 | regExp: { |
|
| 1265 | escape : /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, |
|
| 1266 | beginsWith : '(?:\s|^)' |
|
| 1267 | }, |
|
| 1268 | ||
| 1269 | // maps api response attributes to internal representation |
|
| 1270 | fields: { |
|
| 1271 | categories : 'results', // array of categories (category view) |
|
| 1272 | categoryName : 'name', // name of category (category view) |
|
| 1273 | categoryResults : 'results', // array of results (category view) |
|
| 1274 | description : 'description', // result description |
|
| 1275 | image : 'image', // result image |
|
| 1276 | price : 'price', // result price |
|
| 1277 | results : 'results', // array of results (standard) |
|
| 1278 | title : 'title', // result title |
|
| 1279 | url : 'url', // result url |
|
| 1280 | action : 'action', // "view more" object name |
|
| 1281 | actionText : 'text', // "view more" text |
|
| 1282 | actionURL : 'url' // "view more" url |
|
| 1283 | }, |
|
| 1284 | ||
| 1285 | selector : { |
|
| 1286 | prompt : '.prompt', |
|
| 1287 | searchButton : '.search.button', |
|
| 1288 | results : '.results', |
|
| 1289 | message : '.results > .message', |
|
| 1290 | category : '.category', |
|
| 1291 | result : '.result', |
|
| 1292 | title : '.title, .name' |
|
| 1293 | }, |
|
| 1294 | ||
| 1295 | templates: { |
|
| 1296 | escape: function(string) { |
|
| 1297 | var |
|
| 1298 | badChars = /[&<>"'`]/g, |
|
| 1299 | shouldEscape = /[&<>"'`]/, |
|
| 1300 | escape = { |
|
| 1301 | "&": "&", |
|
| 1302 | "<": "<", |
|
| 1303 | ">": ">", |
|
| 1304 | '"': """, |
|
| 1305 | "'": "'", |
|
| 1306 | "`": "`" |
|
| 1307 | }, |
|
| 1308 | escapedChar = function(chr) { |
|
| 1309 | return escape[chr]; |
|
| 1310 | } |
|
| 1311 | ; |
|
| 1312 | if(shouldEscape.test(string)) { |
|
| 1313 | return string.replace(badChars, escapedChar); |
|
| 1314 | } |
|
| 1315 | return string; |
|
| 1316 | }, |
|
| 1317 | message: function(message, type) { |
|
| 1318 | var |
|
| 1319 | html = '' |
|
| 1320 | ; |
|
| 1321 | if(message !== undefined && type !== undefined) { |
|
| 1322 | html += '' |
|
| 1323 | + '<div class="message ' + type + '">' |
|
| 1324 | ; |
|
| 1325 | // message type |
|
| 1326 | if(type == 'empty') { |
|
| 1327 | html += '' |
|
| 1328 | + '<div class="header">No Results</div class="header">' |
|
| 1329 | + '<div class="description">' + message + '</div class="description">' |
|
| 1330 | ; |
|
| 1331 | } |
|
| 1332 | else { |
|
| 1333 | html += ' <div class="description">' + message + '</div>'; |
|
| 1334 | } |
|
| 1335 | html += '</div>'; |
|
| 1336 | } |
|
| 1337 | return html; |
|
| 1338 | }, |
|
| 1339 | category: function(response, fields) { |
|
| 1340 | var |
|
| 1341 | html = '', |
|
| 1342 | escape = $.fn.search.settings.templates.escape |
|
| 1343 | ; |
|
| 1344 | if(response[fields.categoryResults] !== undefined) { |
|
| 1345 | ||
| 1346 | // each category |
|
| 1347 | $.each(response[fields.categoryResults], function(index, category) { |
|
| 1348 | if(category[fields.results] !== undefined && category.results.length > 0) { |
|
| 1349 | ||
| 1350 | html += '<div class="category">'; |
|
| 1351 | ||
| 1352 | if(category[fields.categoryName] !== undefined) { |
|
| 1353 | html += '<div class="name">' + category[fields.categoryName] + '</div>'; |
|
| 1354 | } |
|
| 1355 | ||
| 1356 | // each item inside category |
|
| 1357 | $.each(category.results, function(index, result) { |
|
| 1358 | if(result[fields.url]) { |
|
| 1359 | html += '<a class="result" href="' + result[fields.url] + '">'; |
|
| 1360 | } |
|
| 1361 | else { |
|
| 1362 | html += '<a class="result">'; |
|
| 1363 | } |
|
| 1364 | if(result[fields.image] !== undefined) { |
|
| 1365 | html += '' |
|
| 1366 | + '<div class="image">' |
|
| 1367 | + ' <img src="' + result[fields.image] + '">' |
|
| 1368 | + '</div>' |
|
| 1369 | ; |
|
| 1370 | } |
|
| 1371 | html += '<div class="content">'; |
|
| 1372 | if(result[fields.price] !== undefined) { |
|
| 1373 | html += '<div class="price">' + result[fields.price] + '</div>'; |
|
| 1374 | } |
|
| 1375 | if(result[fields.title] !== undefined) { |
|
| 1376 | html += '<div class="title">' + result[fields.title] + '</div>'; |
|
| 1377 | } |
|
| 1378 | if(result[fields.description] !== undefined) { |
|
| 1379 | html += '<div class="description">' + result[fields.description] + '</div>'; |
|
| 1380 | } |
|
| 1381 | html += '' |
|
| 1382 | + '</div>' |
|
| 1383 | ; |
|
| 1384 | html += '</a>'; |
|
| 1385 | }); |
|
| 1386 | html += '' |
|
| 1387 | + '</div>' |
|
| 1388 | ; |
|
| 1389 | } |
|
| 1390 | }); |
|
| 1391 | if(response[fields.action]) { |
|
| 1392 | html += '' |
|
| 1393 | + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">' |
|
| 1394 | + response[fields.action][fields.actionText] |
|
| 1395 | + '</a>'; |
|
| 1396 | } |
|
| 1397 | return html; |
|
| 1398 | } |
|
| 1399 | return false; |
|
| 1400 | }, |
|
| 1401 | standard: function(response, fields) { |
|
| 1402 | var |
|
| 1403 | html = '' |
|
| 1404 | ; |
|
| 1405 | if(response[fields.results] !== undefined) { |
|
| 1406 | ||
| 1407 | // each result |
|
| 1408 | $.each(response[fields.results], function(index, result) { |
|
| 1409 | if(result[fields.url]) { |
|
| 1410 | html += '<a class="result" href="' + result[fields.url] + '">'; |
|
| 1411 | } |
|
| 1412 | else { |
|
| 1413 | html += '<a class="result">'; |
|
| 1414 | } |
|
| 1415 | if(result[fields.image] !== undefined) { |
|
| 1416 | html += '' |
|
| 1417 | + '<div class="image">' |
|
| 1418 | + ' <img src="' + result[fields.image] + '">' |
|
| 1419 | + '</div>' |
|
| 1420 | ; |
|
| 1421 | } |
|
| 1422 | html += '<div class="content">'; |
|
| 1423 | if(result[fields.price] !== undefined) { |
|
| 1424 | html += '<div class="price">' + result[fields.price] + '</div>'; |
|
| 1425 | } |
|
| 1426 | if(result[fields.title] !== undefined) { |
|
| 1427 | html += '<div class="title">' + result[fields.title] + '</div>'; |
|
| 1428 | } |
|
| 1429 | if(result[fields.description] !== undefined) { |
|
| 1430 | html += '<div class="description">' + result[fields.description] + '</div>'; |
|
| 1431 | } |
|
| 1432 | html += '' |
|
| 1433 | + '</div>' |
|
| 1434 | ; |
|
| 1435 | html += '</a>'; |
|
| 1436 | }); |
|
| 1437 | ||
| 1438 | if(response[fields.action]) { |
|
| 1439 | html += '' |
|
| 1440 | + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">' |
|
| 1441 | + response[fields.action][fields.actionText] |
|
| 1442 | + '</a>'; |
|
| 1443 | } |
|
| 1444 | return html; |
|
| 1445 | } |
|
| 1446 | return false; |
|
| 1447 | } |
|
| 1448 | } |
|
| 1449 | }; |
|
| 1450 | ||
| 1451 | })( jQuery, window, document ); |
|
| 1452 | ||