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.

1505 lines
48 KiB

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