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.

1249 lines
39 KiB

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