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.

1399 lines
45 KiB

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