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.

1278 lines
40 KiB

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