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.

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