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.

1307 lines
41 KiB

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