You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1320 lines
42 KiB

  1. /*!
  2. * # Semantic UI 2.1.6 - Search
  3. * http://github.com/semantic-org/semantic-ui/
  4. *
  5. *
  6. * Copyright 2015 Contributors
  7. * Released under the MIT license
  8. * http://opensource.org/licenses/MIT
  9. *
  10. */
  11. ;(function ($, window, document, undefined) {
  12. "use strict";
  13. $.fn.search = function(parameters) {
  14. var
  15. $allModules = $(this),
  16. moduleSelector = $allModules.selector || '',
  17. time = new Date().getTime(),
  18. performance = [],
  19. query = arguments[0],
  20. methodInvoked = (typeof query == 'string'),
  21. queryArguments = [].slice.call(arguments, 1),
  22. returnedValue
  23. ;
  24. $(this)
  25. .each(function() {
  26. var
  27. settings = ( $.isPlainObject(parameters) )
  28. ? $.extend(true, {}, $.fn.search.settings, parameters)
  29. : $.extend({}, $.fn.search.settings),
  30. className = settings.className,
  31. metadata = settings.metadata,
  32. regExp = settings.regExp,
  33. fields = settings.fields,
  34. selector = settings.selector,
  35. error = settings.error,
  36. namespace = settings.namespace,
  37. eventNamespace = '.' + namespace,
  38. moduleNamespace = namespace + '-module',
  39. $module = $(this),
  40. $prompt = $module.find(selector.prompt),
  41. $searchButton = $module.find(selector.searchButton),
  42. $results = $module.find(selector.results),
  43. $result = $module.find(selector.result),
  44. $category = $module.find(selector.category),
  45. element = this,
  46. instance = $module.data(moduleNamespace),
  47. module
  48. ;
  49. module = {
  50. initialize: function() {
  51. module.verbose('Initializing module');
  52. module.determine.searchFields();
  53. module.bind.events();
  54. module.set.type();
  55. module.create.results();
  56. module.instantiate();
  57. },
  58. instantiate: function() {
  59. module.verbose('Storing instance of module', module);
  60. instance = module;
  61. $module
  62. .data(moduleNamespace, module)
  63. ;
  64. },
  65. destroy: function() {
  66. module.verbose('Destroying instance');
  67. $module
  68. .off(eventNamespace)
  69. .removeData(moduleNamespace)
  70. ;
  71. },
  72. bind: {
  73. events: function() {
  74. module.verbose('Binding events to search');
  75. if(settings.automatic) {
  76. $module
  77. .on(module.get.inputEvent() + eventNamespace, selector.prompt, module.event.input)
  78. ;
  79. $prompt
  80. .attr('autocomplete', 'off')
  81. ;
  82. }
  83. $module
  84. // prompt
  85. .on('focus' + eventNamespace, selector.prompt, module.event.focus)
  86. .on('blur' + eventNamespace, selector.prompt, module.event.blur)
  87. .on('keydown' + eventNamespace, selector.prompt, module.handleKeyboard)
  88. // search button
  89. .on('click' + eventNamespace, selector.searchButton, module.query)
  90. // results
  91. .on('mousedown' + eventNamespace, selector.results, module.event.result.mousedown)
  92. .on('mouseup' + eventNamespace, selector.results, module.event.result.mouseup)
  93. .on('click' + eventNamespace, selector.result, module.event.result.click)
  94. ;
  95. }
  96. },
  97. determine: {
  98. searchFields: function() {
  99. // this makes sure $.extend does not add specified search fields to default fields
  100. // this is the only setting which should not extend defaults
  101. if(parameters && parameters.searchFields !== undefined) {
  102. settings.searchFields = parameters.searchFields;
  103. }
  104. }
  105. },
  106. event: {
  107. input: function() {
  108. clearTimeout(module.timer);
  109. module.timer = setTimeout(module.query, settings.searchDelay);
  110. },
  111. focus: function() {
  112. module.set.focus();
  113. if( module.has.minimumCharacters() ) {
  114. module.query();
  115. if( module.can.show() ) {
  116. module.showResults();
  117. }
  118. }
  119. },
  120. blur: function(event) {
  121. var
  122. pageLostFocus = (document.activeElement === this)
  123. ;
  124. if(!pageLostFocus && !module.resultsClicked) {
  125. module.cancel.query();
  126. module.remove.focus();
  127. module.timer = setTimeout(module.hideResults, settings.hideDelay);
  128. }
  129. },
  130. result: {
  131. mousedown: function() {
  132. module.resultsClicked = true;
  133. },
  134. mouseup: function() {
  135. module.resultsClicked = false;
  136. },
  137. click: function(event) {
  138. module.debug('Search result selected');
  139. var
  140. $result = $(this),
  141. $title = $result.find(selector.title).eq(0),
  142. $link = $result.find('a[href]').eq(0),
  143. href = $link.attr('href') || false,
  144. target = $link.attr('target') || false,
  145. title = $title.html(),
  146. // title is used for result lookup
  147. value = ($title.length > 0)
  148. ? $title.text()
  149. : false,
  150. results = module.get.results(),
  151. result = $result.data(metadata.result) || module.get.result(value, results),
  152. returnedValue
  153. ;
  154. if( $.isFunction(settings.onSelect) ) {
  155. if(settings.onSelect.call(element, result, results) === false) {
  156. module.debug('Custom onSelect callback cancelled default select action');
  157. return;
  158. }
  159. }
  160. module.hideResults();
  161. if(value) {
  162. module.set.value(value);
  163. }
  164. if(href) {
  165. module.verbose('Opening search link found in result', $link);
  166. if(target == '_blank' || event.ctrlKey) {
  167. window.open(href);
  168. }
  169. else {
  170. window.location.href = (href);
  171. }
  172. }
  173. }
  174. }
  175. },
  176. handleKeyboard: function(event) {
  177. var
  178. // force selector refresh
  179. $result = $module.find(selector.result),
  180. $category = $module.find(selector.category),
  181. currentIndex = $result.index( $result.filter('.' + className.active) ),
  182. resultSize = $result.length,
  183. keyCode = event.which,
  184. keys = {
  185. backspace : 8,
  186. enter : 13,
  187. escape : 27,
  188. upArrow : 38,
  189. downArrow : 40
  190. },
  191. newIndex
  192. ;
  193. // search shortcuts
  194. if(keyCode == keys.escape) {
  195. module.verbose('Escape key pressed, blurring search field');
  196. module.trigger.blur();
  197. }
  198. if( module.is.visible() ) {
  199. if(keyCode == keys.enter) {
  200. module.verbose('Enter key pressed, selecting active result');
  201. if( $result.filter('.' + className.active).length > 0 ) {
  202. module.event.result.click.call($result.filter('.' + className.active), event);
  203. event.preventDefault();
  204. return false;
  205. }
  206. }
  207. else if(keyCode == keys.upArrow) {
  208. module.verbose('Up key pressed, changing active result');
  209. newIndex = (currentIndex - 1 < 0)
  210. ? currentIndex
  211. : currentIndex - 1
  212. ;
  213. $category
  214. .removeClass(className.active)
  215. ;
  216. $result
  217. .removeClass(className.active)
  218. .eq(newIndex)
  219. .addClass(className.active)
  220. .closest($category)
  221. .addClass(className.active)
  222. ;
  223. event.preventDefault();
  224. }
  225. else if(keyCode == keys.downArrow) {
  226. module.verbose('Down key pressed, changing active result');
  227. newIndex = (currentIndex + 1 >= resultSize)
  228. ? currentIndex
  229. : currentIndex + 1
  230. ;
  231. $category
  232. .removeClass(className.active)
  233. ;
  234. $result
  235. .removeClass(className.active)
  236. .eq(newIndex)
  237. .addClass(className.active)
  238. .closest($category)
  239. .addClass(className.active)
  240. ;
  241. event.preventDefault();
  242. }
  243. }
  244. else {
  245. // query shortcuts
  246. if(keyCode == keys.enter) {
  247. module.verbose('Enter key pressed, executing query');
  248. module.query();
  249. module.set.buttonPressed();
  250. $prompt.one('keyup', module.remove.buttonFocus);
  251. }
  252. }
  253. },
  254. setup: {
  255. api: function() {
  256. var
  257. apiSettings = {
  258. debug : settings.debug,
  259. on : false,
  260. cache : 'local',
  261. action : 'search',
  262. onError : module.error
  263. },
  264. searchHTML
  265. ;
  266. module.verbose('First request, initializing API');
  267. $module.api(apiSettings);
  268. }
  269. },
  270. can: {
  271. useAPI: function() {
  272. return $.fn.api !== undefined;
  273. },
  274. show: function() {
  275. return module.is.focused() && !module.is.visible() && !module.is.empty();
  276. },
  277. transition: function() {
  278. return settings.transition && $.fn.transition !== undefined && $module.transition('is supported');
  279. }
  280. },
  281. is: {
  282. empty: function() {
  283. return ($results.html() === '');
  284. },
  285. visible: function() {
  286. return ($results.filter(':visible').length > 0);
  287. },
  288. focused: function() {
  289. return ($prompt.filter(':focus').length > 0);
  290. }
  291. },
  292. trigger: {
  293. blur: function() {
  294. var
  295. events = document.createEvent('HTMLEvents'),
  296. promptElement = $prompt[0]
  297. ;
  298. if(promptElement) {
  299. module.verbose('Triggering native blur event');
  300. events.initEvent('blur', false, false);
  301. promptElement.dispatchEvent(events);
  302. }
  303. }
  304. },
  305. get: {
  306. inputEvent: function() {
  307. var
  308. prompt = $prompt[0],
  309. inputEvent = (prompt !== undefined && prompt.oninput !== undefined)
  310. ? 'input'
  311. : (prompt !== undefined && prompt.onpropertychange !== undefined)
  312. ? 'propertychange'
  313. : 'keyup'
  314. ;
  315. return inputEvent;
  316. },
  317. value: function() {
  318. return $prompt.val();
  319. },
  320. results: function() {
  321. var
  322. results = $module.data(metadata.results)
  323. ;
  324. return results;
  325. },
  326. result: function(value, results) {
  327. var
  328. lookupFields = ['title', 'id'],
  329. result = false
  330. ;
  331. value = (value !== undefined)
  332. ? value
  333. : module.get.value()
  334. ;
  335. results = (results !== undefined)
  336. ? results
  337. : module.get.results()
  338. ;
  339. if(settings.type === 'category') {
  340. module.debug('Finding result that matches', value);
  341. $.each(results, function(index, category) {
  342. if($.isArray(category.results)) {
  343. result = module.search.object(value, category.results, lookupFields)[0];
  344. // don't continue searching if a result is found
  345. if(result) {
  346. return false;
  347. }
  348. }
  349. });
  350. }
  351. else {
  352. module.debug('Finding result in results object', value);
  353. result = module.search.object(value, results, lookupFields)[0];
  354. }
  355. return result || false;
  356. },
  357. },
  358. set: {
  359. focus: function() {
  360. $module.addClass(className.focus);
  361. },
  362. loading: function() {
  363. $module.addClass(className.loading);
  364. },
  365. value: function(value) {
  366. module.verbose('Setting search input value', value);
  367. $prompt
  368. .val(value)
  369. ;
  370. },
  371. type: function(type) {
  372. type = type || settings.type;
  373. if(settings.type == 'category') {
  374. $module.addClass(settings.type);
  375. }
  376. },
  377. buttonPressed: function() {
  378. $searchButton.addClass(className.pressed);
  379. }
  380. },
  381. remove: {
  382. loading: function() {
  383. $module.removeClass(className.loading);
  384. },
  385. focus: function() {
  386. $module.removeClass(className.focus);
  387. },
  388. buttonPressed: function() {
  389. $searchButton.removeClass(className.pressed);
  390. }
  391. },
  392. query: function() {
  393. var
  394. searchTerm = module.get.value(),
  395. cache = module.read.cache(searchTerm)
  396. ;
  397. if( module.has.minimumCharacters() ) {
  398. if(cache) {
  399. module.debug('Reading result from cache', searchTerm);
  400. module.save.results(cache.results);
  401. module.addResults(cache.html);
  402. module.inject.id(cache.results);
  403. }
  404. else {
  405. module.debug('Querying for', searchTerm);
  406. if($.isPlainObject(settings.source) || $.isArray(settings.source)) {
  407. module.search.local(searchTerm);
  408. }
  409. else if( module.can.useAPI() ) {
  410. module.search.remote(searchTerm);
  411. }
  412. else {
  413. module.error(error.source);
  414. }
  415. }
  416. settings.onSearchQuery.call(element, searchTerm);
  417. }
  418. else {
  419. module.hideResults();
  420. }
  421. },
  422. search: {
  423. local: function(searchTerm) {
  424. var
  425. results = module.search.object(searchTerm, settings.content),
  426. searchHTML
  427. ;
  428. module.set.loading();
  429. module.save.results(results);
  430. module.debug('Returned local search results', results);
  431. searchHTML = module.generateResults({
  432. results: results
  433. });
  434. module.remove.loading();
  435. module.addResults(searchHTML);
  436. module.inject.id(results);
  437. module.write.cache(searchTerm, {
  438. html : searchHTML,
  439. results : results
  440. });
  441. },
  442. remote: function(searchTerm) {
  443. var
  444. apiSettings = {
  445. onSuccess : function(response) {
  446. module.parse.response.call(element, response, searchTerm);
  447. },
  448. onFailure: function() {
  449. module.displayMessage(error.serverError);
  450. },
  451. urlData: {
  452. query: searchTerm
  453. }
  454. }
  455. ;
  456. if( !$module.api('get request') ) {
  457. module.setup.api();
  458. }
  459. $.extend(true, apiSettings, settings.apiSettings);
  460. module.debug('Executing search', apiSettings);
  461. module.cancel.query();
  462. $module
  463. .api('setting', apiSettings)
  464. .api('query')
  465. ;
  466. },
  467. object: function(searchTerm, source, searchFields) {
  468. var
  469. results = [],
  470. fuzzyResults = [],
  471. searchExp = searchTerm.toString().replace(regExp.escape, '\\$&'),
  472. matchRegExp = new RegExp(regExp.beginsWith + searchExp, 'i'),
  473. // avoid duplicates when pushing results
  474. addResult = function(array, result) {
  475. var
  476. notResult = ($.inArray(result, results) == -1),
  477. notFuzzyResult = ($.inArray(result, fuzzyResults) == -1)
  478. ;
  479. if(notResult && notFuzzyResult) {
  480. array.push(result);
  481. }
  482. }
  483. ;
  484. source = source || settings.source;
  485. searchFields = (searchFields !== undefined)
  486. ? searchFields
  487. : settings.searchFields
  488. ;
  489. // search fields should be array to loop correctly
  490. if(!$.isArray(searchFields)) {
  491. searchFields = [searchFields];
  492. }
  493. // exit conditions if no source
  494. if(source === undefined || source === false) {
  495. module.error(error.source);
  496. return [];
  497. }
  498. // iterate through search fields looking for matches
  499. $.each(searchFields, function(index, field) {
  500. $.each(source, function(label, content) {
  501. var
  502. fieldExists = (typeof content[field] == 'string')
  503. ;
  504. if(fieldExists) {
  505. if( content[field].search(matchRegExp) !== -1) {
  506. // content starts with value (first in results)
  507. addResult(results, content);
  508. }
  509. else if(settings.searchFullText && module.fuzzySearch(searchTerm, content[field]) ) {
  510. // content fuzzy matches (last in results)
  511. addResult(fuzzyResults, content);
  512. }
  513. }
  514. });
  515. });
  516. return $.merge(results, fuzzyResults);
  517. }
  518. },
  519. fuzzySearch: function(query, term) {
  520. var
  521. termLength = term.length,
  522. queryLength = query.length
  523. ;
  524. if(typeof query !== 'string') {
  525. return false;
  526. }
  527. query = query.toLowerCase();
  528. term = term.toLowerCase();
  529. if(queryLength > termLength) {
  530. return false;
  531. }
  532. if(queryLength === termLength) {
  533. return (query === term);
  534. }
  535. search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
  536. var
  537. queryCharacter = query.charCodeAt(characterIndex)
  538. ;
  539. while(nextCharacterIndex < termLength) {
  540. if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
  541. continue search;
  542. }
  543. }
  544. return false;
  545. }
  546. return true;
  547. },
  548. parse: {
  549. response: function(response, searchTerm) {
  550. var
  551. searchHTML = module.generateResults(response)
  552. ;
  553. module.verbose('Parsing server response', response);
  554. if(response !== undefined) {
  555. if(searchTerm !== undefined && response[fields.results] !== undefined) {
  556. module.addResults(searchHTML);
  557. module.inject.id(response[fields.results]);
  558. module.write.cache(searchTerm, {
  559. html : searchHTML,
  560. results : response[fields.results]
  561. });
  562. module.save.results(response[fields.results]);
  563. }
  564. }
  565. }
  566. },
  567. cancel: {
  568. query: function() {
  569. if( module.can.useAPI() ) {
  570. $module.api('abort');
  571. }
  572. }
  573. },
  574. has: {
  575. minimumCharacters: function() {
  576. var
  577. searchTerm = module.get.value(),
  578. numCharacters = searchTerm.length
  579. ;
  580. return (numCharacters >= settings.minCharacters);
  581. }
  582. },
  583. clear: {
  584. cache: function(value) {
  585. var
  586. cache = $module.data(metadata.cache)
  587. ;
  588. if(!value) {
  589. module.debug('Clearing cache', value);
  590. $module.removeData(metadata.cache);
  591. }
  592. else if(value && cache && cache[value]) {
  593. module.debug('Removing value from cache', value);
  594. delete cache[value];
  595. $module.data(metadata.cache, cache);
  596. }
  597. }
  598. },
  599. read: {
  600. cache: function(name) {
  601. var
  602. cache = $module.data(metadata.cache)
  603. ;
  604. if(settings.cache) {
  605. module.verbose('Checking cache for generated html for query', name);
  606. return (typeof cache == 'object') && (cache[name] !== undefined)
  607. ? cache[name]
  608. : false
  609. ;
  610. }
  611. return false;
  612. }
  613. },
  614. create: {
  615. id: function(resultIndex, categoryIndex) {
  616. var
  617. resultID = (resultIndex + 1), // not zero indexed
  618. categoryID = (categoryIndex + 1),
  619. firstCharCode,
  620. letterID,
  621. id
  622. ;
  623. if(categoryIndex !== undefined) {
  624. // start char code for "A"
  625. letterID = String.fromCharCode(97 + categoryIndex);
  626. id = letterID + resultID;
  627. module.verbose('Creating category result id', id);
  628. }
  629. else {
  630. id = resultID;
  631. module.verbose('Creating result id', id);
  632. }
  633. return id;
  634. },
  635. results: function() {
  636. if($results.length === 0) {
  637. $results = $('<div />')
  638. .addClass(className.results)
  639. .appendTo($module)
  640. ;
  641. }
  642. }
  643. },
  644. inject: {
  645. result: function(result, resultIndex, categoryIndex) {
  646. module.verbose('Injecting result into results');
  647. var
  648. $selectedResult = (categoryIndex !== undefined)
  649. ? $results
  650. .children().eq(categoryIndex)
  651. .children(selector.result).eq(resultIndex)
  652. : $results
  653. .children(selector.result).eq(resultIndex)
  654. ;
  655. module.verbose('Injecting results metadata', $selectedResult);
  656. $selectedResult
  657. .data(metadata.result, result)
  658. ;
  659. },
  660. id: function(results) {
  661. module.debug('Injecting unique ids into results');
  662. var
  663. // since results may be object, we must use counters
  664. categoryIndex = 0,
  665. resultIndex = 0
  666. ;
  667. if(settings.type === 'category') {
  668. // iterate through each category result
  669. $.each(results, function(index, category) {
  670. resultIndex = 0;
  671. $.each(category.results, function(index, value) {
  672. var
  673. result = category.results[index]
  674. ;
  675. if(result.id === undefined) {
  676. result.id = module.create.id(resultIndex, categoryIndex);
  677. }
  678. module.inject.result(result, resultIndex, categoryIndex);
  679. resultIndex++;
  680. });
  681. categoryIndex++;
  682. });
  683. }
  684. else {
  685. // top level
  686. $.each(results, function(index, value) {
  687. var
  688. result = results[index]
  689. ;
  690. if(result.id === undefined) {
  691. result.id = module.create.id(resultIndex);
  692. }
  693. module.inject.result(result, resultIndex);
  694. resultIndex++;
  695. });
  696. }
  697. return results;
  698. }
  699. },
  700. save: {
  701. results: function(results) {
  702. module.verbose('Saving current search results to metadata', results);
  703. $module.data(metadata.results, results);
  704. }
  705. },
  706. write: {
  707. cache: function(name, value) {
  708. var
  709. cache = ($module.data(metadata.cache) !== undefined)
  710. ? $module.data(metadata.cache)
  711. : {}
  712. ;
  713. if(settings.cache) {
  714. module.verbose('Writing generated html to cache', name, value);
  715. cache[name] = value;
  716. $module
  717. .data(metadata.cache, cache)
  718. ;
  719. }
  720. }
  721. },
  722. addResults: function(html) {
  723. if( $.isFunction(settings.onResultsAdd) ) {
  724. if( settings.onResultsAdd.call($results, html) === false ) {
  725. module.debug('onResultsAdd callback cancelled default action');
  726. return false;
  727. }
  728. }
  729. $results
  730. .html(html)
  731. ;
  732. if( module.can.show() ) {
  733. module.showResults();
  734. }
  735. },
  736. showResults: function() {
  737. if(!module.is.visible()) {
  738. if( module.can.transition() ) {
  739. module.debug('Showing results with css animations');
  740. $results
  741. .transition({
  742. animation : settings.transition + ' in',
  743. debug : settings.debug,
  744. verbose : settings.verbose,
  745. duration : settings.duration,
  746. queue : true
  747. })
  748. ;
  749. }
  750. else {
  751. module.debug('Showing results with javascript');
  752. $results
  753. .stop()
  754. .fadeIn(settings.duration, settings.easing)
  755. ;
  756. }
  757. settings.onResultsOpen.call($results);
  758. }
  759. },
  760. hideResults: function() {
  761. if( module.is.visible() ) {
  762. if( module.can.transition() ) {
  763. module.debug('Hiding results with css animations');
  764. $results
  765. .transition({
  766. animation : settings.transition + ' out',
  767. debug : settings.debug,
  768. verbose : settings.verbose,
  769. duration : settings.duration,
  770. queue : true
  771. })
  772. ;
  773. }
  774. else {
  775. module.debug('Hiding results with javascript');
  776. $results
  777. .stop()
  778. .fadeOut(settings.duration, settings.easing)
  779. ;
  780. }
  781. settings.onResultsClose.call($results);
  782. }
  783. },
  784. generateResults: function(response) {
  785. module.debug('Generating html from response', response);
  786. var
  787. template = settings.templates[settings.type],
  788. isProperObject = ($.isPlainObject(response[fields.results]) && !$.isEmptyObject(response[fields.results])),
  789. isProperArray = ($.isArray(response[fields.results]) && response[fields.results].length > 0),
  790. html = ''
  791. ;
  792. if(isProperObject || isProperArray ) {
  793. if(settings.maxResults > 0) {
  794. if(isProperObject) {
  795. if(settings.type == 'standard') {
  796. module.error(error.maxResults);
  797. }
  798. }
  799. else {
  800. response[fields.results] = response[fields.results].slice(0, settings.maxResults);
  801. }
  802. }
  803. if($.isFunction(template)) {
  804. html = template(response, fields);
  805. }
  806. else {
  807. module.error(error.noTemplate, false);
  808. }
  809. }
  810. else {
  811. html = module.displayMessage(error.noResults, 'empty');
  812. }
  813. settings.onResults.call(element, response);
  814. return html;
  815. },
  816. displayMessage: function(text, type) {
  817. type = type || 'standard';
  818. module.debug('Displaying message', text, type);
  819. module.addResults( settings.templates.message(text, type) );
  820. return settings.templates.message(text, type);
  821. },
  822. setting: function(name, value) {
  823. if( $.isPlainObject(name) ) {
  824. $.extend(true, settings, name);
  825. }
  826. else if(value !== undefined) {
  827. settings[name] = value;
  828. }
  829. else {
  830. return settings[name];
  831. }
  832. },
  833. internal: function(name, value) {
  834. if( $.isPlainObject(name) ) {
  835. $.extend(true, module, name);
  836. }
  837. else if(value !== undefined) {
  838. module[name] = value;
  839. }
  840. else {
  841. return module[name];
  842. }
  843. },
  844. debug: function() {
  845. if(settings.debug) {
  846. if(settings.performance) {
  847. module.performance.log(arguments);
  848. }
  849. else {
  850. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  851. module.debug.apply(console, arguments);
  852. }
  853. }
  854. },
  855. verbose: function() {
  856. if(settings.verbose && settings.debug) {
  857. if(settings.performance) {
  858. module.performance.log(arguments);
  859. }
  860. else {
  861. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  862. module.verbose.apply(console, arguments);
  863. }
  864. }
  865. },
  866. error: function() {
  867. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  868. module.error.apply(console, arguments);
  869. },
  870. performance: {
  871. log: function(message) {
  872. var
  873. currentTime,
  874. executionTime,
  875. previousTime
  876. ;
  877. if(settings.performance) {
  878. currentTime = new Date().getTime();
  879. previousTime = time || currentTime;
  880. executionTime = currentTime - previousTime;
  881. time = currentTime;
  882. performance.push({
  883. 'Name' : message[0],
  884. 'Arguments' : [].slice.call(message, 1) || '',
  885. 'Element' : element,
  886. 'Execution Time' : executionTime
  887. });
  888. }
  889. clearTimeout(module.performance.timer);
  890. module.performance.timer = setTimeout(module.performance.display, 500);
  891. },
  892. display: function() {
  893. var
  894. title = settings.name + ':',
  895. totalTime = 0
  896. ;
  897. time = false;
  898. clearTimeout(module.performance.timer);
  899. $.each(performance, function(index, data) {
  900. totalTime += data['Execution Time'];
  901. });
  902. title += ' ' + totalTime + 'ms';
  903. if(moduleSelector) {
  904. title += ' \'' + moduleSelector + '\'';
  905. }
  906. if($allModules.length > 1) {
  907. title += ' ' + '(' + $allModules.length + ')';
  908. }
  909. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  910. console.groupCollapsed(title);
  911. if(console.table) {
  912. console.table(performance);
  913. }
  914. else {
  915. $.each(performance, function(index, data) {
  916. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  917. });
  918. }
  919. console.groupEnd();
  920. }
  921. performance = [];
  922. }
  923. },
  924. invoke: function(query, passedArguments, context) {
  925. var
  926. object = instance,
  927. maxDepth,
  928. found,
  929. response
  930. ;
  931. passedArguments = passedArguments || queryArguments;
  932. context = element || context;
  933. if(typeof query == 'string' && object !== undefined) {
  934. query = query.split(/[\. ]/);
  935. maxDepth = query.length - 1;
  936. $.each(query, function(depth, value) {
  937. var camelCaseValue = (depth != maxDepth)
  938. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  939. : query
  940. ;
  941. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  942. object = object[camelCaseValue];
  943. }
  944. else if( object[camelCaseValue] !== undefined ) {
  945. found = object[camelCaseValue];
  946. return false;
  947. }
  948. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  949. object = object[value];
  950. }
  951. else if( object[value] !== undefined ) {
  952. found = object[value];
  953. return false;
  954. }
  955. else {
  956. return false;
  957. }
  958. });
  959. }
  960. if( $.isFunction( found ) ) {
  961. response = found.apply(context, passedArguments);
  962. }
  963. else if(found !== undefined) {
  964. response = found;
  965. }
  966. if($.isArray(returnedValue)) {
  967. returnedValue.push(response);
  968. }
  969. else if(returnedValue !== undefined) {
  970. returnedValue = [returnedValue, response];
  971. }
  972. else if(response !== undefined) {
  973. returnedValue = response;
  974. }
  975. return found;
  976. }
  977. };
  978. if(methodInvoked) {
  979. if(instance === undefined) {
  980. module.initialize();
  981. }
  982. module.invoke(query);
  983. }
  984. else {
  985. if(instance !== undefined) {
  986. instance.invoke('destroy');
  987. }
  988. module.initialize();
  989. }
  990. })
  991. ;
  992. return (returnedValue !== undefined)
  993. ? returnedValue
  994. : this
  995. ;
  996. };
  997. $.fn.search.settings = {
  998. name : 'Search',
  999. namespace : 'search',
  1000. debug : false,
  1001. verbose : false,
  1002. performance : true,
  1003. type : 'standard',
  1004. // template to use (specified in settings.templates)
  1005. minCharacters : 1,
  1006. // minimum characters required to search
  1007. apiSettings : false,
  1008. // API config
  1009. source : false,
  1010. // object to search
  1011. searchFields : [
  1012. 'title',
  1013. 'description'
  1014. ],
  1015. // fields to search
  1016. displayField : '',
  1017. // field to display in standard results template
  1018. searchFullText : true,
  1019. // whether to include fuzzy results in local search
  1020. automatic : true,
  1021. // whether to add events to prompt automatically
  1022. hideDelay : 0,
  1023. // delay before hiding menu after blur
  1024. searchDelay : 200,
  1025. // delay before searching
  1026. maxResults : 7,
  1027. // maximum results returned from local
  1028. cache : true,
  1029. // whether to store lookups in local cache
  1030. // transition settings
  1031. transition : 'scale',
  1032. duration : 200,
  1033. easing : 'easeOutExpo',
  1034. // callbacks
  1035. onSelect : false,
  1036. onResultsAdd : false,
  1037. onSearchQuery : function(query){},
  1038. onResults : function(response){},
  1039. onResultsOpen : function(){},
  1040. onResultsClose : function(){},
  1041. className: {
  1042. active : 'active',
  1043. empty : 'empty',
  1044. focus : 'focus',
  1045. loading : 'loading',
  1046. results : 'results',
  1047. pressed : 'down'
  1048. },
  1049. error : {
  1050. source : 'Cannot search. No source used, and Semantic API module was not included',
  1051. noResults : 'Your search returned no results',
  1052. logging : 'Error in debug logging, exiting.',
  1053. noEndpoint : 'No search endpoint was specified',
  1054. noTemplate : 'A valid template name was not specified.',
  1055. serverError : 'There was an issue querying the server.',
  1056. maxResults : 'Results must be an array to use maxResults setting',
  1057. method : 'The method you called is not defined.'
  1058. },
  1059. metadata: {
  1060. cache : 'cache',
  1061. results : 'results',
  1062. result : 'result'
  1063. },
  1064. regExp: {
  1065. escape : /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,
  1066. beginsWith : '(?:\s|^)'
  1067. },
  1068. // maps api response attributes to internal representation
  1069. fields: {
  1070. categories : 'results', // array of categories (category view)
  1071. categoryName : 'name', // name of category (category view)
  1072. categoryResults : 'results', // array of results (category view)
  1073. description : 'description', // result description
  1074. image : 'image', // result image
  1075. price : 'price', // result price
  1076. results : 'results', // array of results (standard)
  1077. title : 'title', // result title
  1078. url : 'url', // result url
  1079. action : 'action', // "view more" object name
  1080. actionText : 'text', // "view more" text
  1081. actionURL : 'url' // "view more" url
  1082. },
  1083. selector : {
  1084. prompt : '.prompt',
  1085. searchButton : '.search.button',
  1086. results : '.results',
  1087. category : '.category',
  1088. result : '.result',
  1089. title : '.title, .name'
  1090. },
  1091. templates: {
  1092. escape: function(string) {
  1093. var
  1094. badChars = /[&<>"'`]/g,
  1095. shouldEscape = /[&<>"'`]/,
  1096. escape = {
  1097. "&": "&amp;",
  1098. "<": "&lt;",
  1099. ">": "&gt;",
  1100. '"': "&quot;",
  1101. "'": "&#x27;",
  1102. "`": "&#x60;"
  1103. },
  1104. escapedChar = function(chr) {
  1105. return escape[chr];
  1106. }
  1107. ;
  1108. if(shouldEscape.test(string)) {
  1109. return string.replace(badChars, escapedChar);
  1110. }
  1111. return string;
  1112. },
  1113. message: function(message, type) {
  1114. var
  1115. html = ''
  1116. ;
  1117. if(message !== undefined && type !== undefined) {
  1118. html += ''
  1119. + '<div class="message ' + type + '">'
  1120. ;
  1121. // message type
  1122. if(type == 'empty') {
  1123. html += ''
  1124. + '<div class="header">No Results</div class="header">'
  1125. + '<div class="description">' + message + '</div class="description">'
  1126. ;
  1127. }
  1128. else {
  1129. html += ' <div class="description">' + message + '</div>';
  1130. }
  1131. html += '</div>';
  1132. }
  1133. return html;
  1134. },
  1135. category: function(response, fields) {
  1136. var
  1137. html = '',
  1138. escape = $.fn.search.settings.templates.escape
  1139. ;
  1140. if(response[fields.categoryResults] !== undefined) {
  1141. // each category
  1142. $.each(response[fields.categoryResults], function(index, category) {
  1143. if(category[fields.results] !== undefined && category.results.length > 0) {
  1144. html += '<div class="category">';
  1145. if(category[fields.categoryName] !== undefined) {
  1146. html += '<div class="name">' + category[fields.categoryName] + '</div>';
  1147. }
  1148. // each item inside category
  1149. $.each(category.results, function(index, result) {
  1150. if(result[fields.url]) {
  1151. html += '<a class="result" href="' + result[fields.url] + '">';
  1152. }
  1153. else {
  1154. html += '<a class="result">';
  1155. }
  1156. if(result[fields.image] !== undefined) {
  1157. html += ''
  1158. + '<div class="image">'
  1159. + ' <img src="' + result[fields.image] + '">'
  1160. + '</div>'
  1161. ;
  1162. }
  1163. html += '<div class="content">';
  1164. if(result[fields.price] !== undefined) {
  1165. html += '<div class="price">' + result[fields.price] + '</div>';
  1166. }
  1167. if(result[fields.title] !== undefined) {
  1168. html += '<div class="title">' + result[fields.title] + '</div>';
  1169. }
  1170. if(result[fields.description] !== undefined) {
  1171. html += '<div class="description">' + result[fields.description] + '</div>';
  1172. }
  1173. html += ''
  1174. + '</div>'
  1175. ;
  1176. html += '</a>';
  1177. });
  1178. html += ''
  1179. + '</div>'
  1180. ;
  1181. }
  1182. });
  1183. if(response[fields.action]) {
  1184. html += ''
  1185. + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">'
  1186. + response[fields.action][fields.actionText]
  1187. + '</a>';
  1188. }
  1189. return html;
  1190. }
  1191. return false;
  1192. },
  1193. standard: function(response, fields) {
  1194. var
  1195. html = ''
  1196. ;
  1197. if(response[fields.results] !== undefined) {
  1198. // each result
  1199. $.each(response[fields.results], function(index, result) {
  1200. if(result[fields.url]) {
  1201. html += '<a class="result" href="' + result[fields.url] + '">';
  1202. }
  1203. else {
  1204. html += '<a class="result">';
  1205. }
  1206. if(result[fields.image] !== undefined) {
  1207. html += ''
  1208. + '<div class="image">'
  1209. + ' <img src="' + result[fields.image] + '">'
  1210. + '</div>'
  1211. ;
  1212. }
  1213. html += '<div class="content">';
  1214. if(result[fields.price] !== undefined) {
  1215. html += '<div class="price">' + result[fields.price] + '</div>';
  1216. }
  1217. if(result[fields.title] !== undefined) {
  1218. html += '<div class="title">' + result[fields.title] + '</div>';
  1219. }
  1220. if(result[fields.description] !== undefined) {
  1221. html += '<div class="description">' + result[fields.description] + '</div>';
  1222. }
  1223. html += ''
  1224. + '</div>'
  1225. ;
  1226. html += '</a>';
  1227. });
  1228. if(response[fields.action]) {
  1229. html += ''
  1230. + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">'
  1231. + response[fields.action][fields.actionText]
  1232. + '</a>';
  1233. }
  1234. return html;
  1235. }
  1236. return false;
  1237. }
  1238. }
  1239. };
  1240. })( jQuery, window, document );