Code Duplication    Length = 1441-1441 lines in 2 locations

public/lib/semantic/semantic.js 1 location

@@ 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
          "&": "&amp;",
14580
          "<": "&lt;",
14581
          ">": "&gt;",
14582
          '"': "&quot;",
14583
          "'": "&#x27;",
14584
          "`": "&#x60;"
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

public/lib/semantic/components/search.js 1 location

@@ 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
          "&": "&amp;",
1302
          "<": "&lt;",
1303
          ">": "&gt;",
1304
          '"': "&quot;",
1305
          "'": "&#x27;",
1306
          "`": "&#x60;"
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