search.js 51 KB

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