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.

844 lines
27 KiB

10 years ago
  1. /*
  2. * # Semantic - Search
  3. * http://github.com/semantic-org/semantic-ui/
  4. *
  5. *
  6. * Copyright 2014 Contributor
  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 = $.extend(true, {}, $.fn.search.settings, parameters),
  28. className = settings.className,
  29. selector = settings.selector,
  30. error = settings.error,
  31. namespace = settings.namespace,
  32. eventNamespace = '.' + namespace,
  33. moduleNamespace = namespace + '-module',
  34. $module = $(this),
  35. $prompt = $module.find(selector.prompt),
  36. $searchButton = $module.find(selector.searchButton),
  37. $results = $module.find(selector.results),
  38. $result = $module.find(selector.result),
  39. $category = $module.find(selector.category),
  40. element = this,
  41. instance = $module.data(moduleNamespace),
  42. module
  43. ;
  44. module = {
  45. initialize: function() {
  46. module.verbose('Initializing module');
  47. var
  48. prompt = $prompt[0],
  49. inputEvent = (prompt !== undefined && prompt.oninput !== undefined)
  50. ? 'input'
  51. : (prompt !== undefined && prompt.onpropertychange !== undefined)
  52. ? 'propertychange'
  53. : 'keyup'
  54. ;
  55. // attach events
  56. $prompt
  57. .on('focus' + eventNamespace, module.event.focus)
  58. .on('blur' + eventNamespace, module.event.blur)
  59. .on('keydown' + eventNamespace, module.handleKeyboard)
  60. ;
  61. if(settings.automatic) {
  62. $prompt
  63. .on(inputEvent + eventNamespace, module.search.throttle)
  64. ;
  65. }
  66. $searchButton
  67. .on('click' + eventNamespace, module.search.query)
  68. ;
  69. $results
  70. .on('click' + eventNamespace, selector.result, module.results.select)
  71. ;
  72. module.instantiate();
  73. },
  74. instantiate: function() {
  75. module.verbose('Storing instance of module', module);
  76. instance = module;
  77. $module
  78. .data(moduleNamespace, module)
  79. ;
  80. },
  81. destroy: function() {
  82. module.verbose('Destroying instance');
  83. $module
  84. .removeData(moduleNamespace)
  85. ;
  86. $prompt
  87. .off(eventNamespace)
  88. ;
  89. $searchButton
  90. .off(eventNamespace)
  91. ;
  92. $results
  93. .off(eventNamespace)
  94. ;
  95. },
  96. event: {
  97. focus: function() {
  98. $module
  99. .addClass(className.focus)
  100. ;
  101. clearTimeout(module.timer);
  102. module.search.throttle();
  103. module.results.show();
  104. },
  105. blur: function() {
  106. module.search.cancel();
  107. $module
  108. .removeClass(className.focus)
  109. ;
  110. module.timer = setTimeout(module.results.hide, settings.hideDelay);
  111. }
  112. },
  113. handleKeyboard: function(event) {
  114. var
  115. // force latest jq dom
  116. $result = $module.find(selector.result),
  117. $category = $module.find(selector.category),
  118. keyCode = event.which,
  119. keys = {
  120. backspace : 8,
  121. enter : 13,
  122. escape : 27,
  123. upArrow : 38,
  124. downArrow : 40
  125. },
  126. activeClass = className.active,
  127. currentIndex = $result.index( $result.filter('.' + activeClass) ),
  128. resultSize = $result.size(),
  129. newIndex
  130. ;
  131. // search shortcuts
  132. if(keyCode == keys.escape) {
  133. module.verbose('Escape key pressed, blurring search field');
  134. $prompt
  135. .trigger('blur')
  136. ;
  137. }
  138. // result shortcuts
  139. if($results.filter(':visible').size() > 0) {
  140. if(keyCode == keys.enter) {
  141. module.verbose('Enter key pressed, selecting active result');
  142. if( $result.filter('.' + activeClass).size() > 0 ) {
  143. $.proxy(module.results.select, $result.filter('.' + activeClass) )(event);
  144. event.preventDefault();
  145. return false;
  146. }
  147. }
  148. else if(keyCode == keys.upArrow) {
  149. module.verbose('Up key pressed, changing active result');
  150. newIndex = (currentIndex - 1 < 0)
  151. ? currentIndex
  152. : currentIndex - 1
  153. ;
  154. $category
  155. .removeClass(activeClass)
  156. ;
  157. $result
  158. .removeClass(activeClass)
  159. .eq(newIndex)
  160. .addClass(activeClass)
  161. .closest($category)
  162. .addClass(activeClass)
  163. ;
  164. event.preventDefault();
  165. }
  166. else if(keyCode == keys.downArrow) {
  167. module.verbose('Down key pressed, changing active result');
  168. newIndex = (currentIndex + 1 >= resultSize)
  169. ? currentIndex
  170. : currentIndex + 1
  171. ;
  172. $category
  173. .removeClass(activeClass)
  174. ;
  175. $result
  176. .removeClass(activeClass)
  177. .eq(newIndex)
  178. .addClass(activeClass)
  179. .closest($category)
  180. .addClass(activeClass)
  181. ;
  182. event.preventDefault();
  183. }
  184. }
  185. else {
  186. // query shortcuts
  187. if(keyCode == keys.enter) {
  188. module.verbose('Enter key pressed, executing query');
  189. module.search.query();
  190. $searchButton
  191. .addClass(className.down)
  192. ;
  193. $prompt
  194. .one('keyup', function(){
  195. $searchButton
  196. .removeClass(className.down)
  197. ;
  198. })
  199. ;
  200. }
  201. }
  202. },
  203. search: {
  204. cancel: function() {
  205. var
  206. xhr = $module.data('xhr') || false
  207. ;
  208. if( xhr && xhr.state() != 'resolved') {
  209. module.debug('Cancelling last search');
  210. xhr.abort();
  211. }
  212. },
  213. throttle: function() {
  214. var
  215. searchTerm = $prompt.val(),
  216. numCharacters = searchTerm.length
  217. ;
  218. clearTimeout(module.timer);
  219. if(numCharacters >= settings.minCharacters) {
  220. module.timer = setTimeout(module.search.query, settings.searchThrottle);
  221. }
  222. else {
  223. module.results.hide();
  224. }
  225. },
  226. query: function() {
  227. var
  228. searchTerm = $prompt.val(),
  229. cachedHTML = module.search.cache.read(searchTerm)
  230. ;
  231. if(cachedHTML) {
  232. module.debug("Reading result for '" + searchTerm + "' from cache");
  233. module.results.add(cachedHTML);
  234. }
  235. else {
  236. module.debug("Querying for '" + searchTerm + "'");
  237. if($.isPlainObject(settings.source) || $.isArray(settings.source)) {
  238. module.search.local(searchTerm);
  239. }
  240. else if(settings.apiSettings) {
  241. module.search.remote(searchTerm);
  242. }
  243. else if($.api !== undefined && $.api.settings.api.search !== undefined) {
  244. module.debug('Searching with default search API endpoint');
  245. settings.apiSettings = {
  246. action: 'search'
  247. };
  248. module.search.remote(searchTerm);
  249. }
  250. else {
  251. module.error(error.source);
  252. }
  253. $.proxy(settings.onSearchQuery, $module)(searchTerm);
  254. }
  255. },
  256. local: function(searchTerm) {
  257. var
  258. results = [],
  259. fullTextResults = [],
  260. searchFields = $.isArray(settings.searchFields)
  261. ? settings.searchFields
  262. : [settings.searchFields],
  263. searchRegExp = new RegExp('(?:\s|^)' + searchTerm, 'i'),
  264. fullTextRegExp = new RegExp(searchTerm, 'i'),
  265. searchHTML
  266. ;
  267. $module
  268. .addClass(className.loading)
  269. ;
  270. // iterate through search fields in array order
  271. $.each(searchFields, function(index, field) {
  272. $.each(settings.source, function(label, content) {
  273. var
  274. fieldExists = (typeof content[field] == 'string'),
  275. notAlreadyResult = ($.inArray(content, results) == -1 && $.inArray(content, fullTextResults) == -1)
  276. ;
  277. if(fieldExists && notAlreadyResult) {
  278. if( searchRegExp.test( content[field] ) ) {
  279. results.push(content);
  280. }
  281. else if( fullTextRegExp.test( content[field] ) ) {
  282. fullTextResults.push(content);
  283. }
  284. }
  285. });
  286. });
  287. searchHTML = module.results.generate({
  288. results: $.merge(results, fullTextResults)
  289. });
  290. $module
  291. .removeClass(className.loading)
  292. ;
  293. module.search.cache.write(searchTerm, searchHTML);
  294. module.results.add(searchHTML);
  295. },
  296. remote: function(searchTerm) {
  297. var
  298. apiSettings = {
  299. stateContext : $module,
  300. urlData: {
  301. query: searchTerm
  302. },
  303. onSuccess : function(response) {
  304. searchHTML = module.results.generate(response);
  305. module.search.cache.write(searchTerm, searchHTML);
  306. module.results.add(searchHTML);
  307. },
  308. failure : module.error
  309. },
  310. searchHTML
  311. ;
  312. module.search.cancel();
  313. module.debug('Executing search');
  314. $.extend(true, apiSettings, settings.apiSettings);
  315. $.api(apiSettings);
  316. },
  317. cache: {
  318. read: function(name) {
  319. var
  320. cache = $module.data('cache')
  321. ;
  322. return (settings.cache && (typeof cache == 'object') && (cache[name] !== undefined) )
  323. ? cache[name]
  324. : false
  325. ;
  326. },
  327. write: function(name, value) {
  328. var
  329. cache = ($module.data('cache') !== undefined)
  330. ? $module.data('cache')
  331. : {}
  332. ;
  333. cache[name] = value;
  334. $module
  335. .data('cache', cache)
  336. ;
  337. }
  338. }
  339. },
  340. results: {
  341. generate: function(response) {
  342. module.debug('Generating html from response', response);
  343. var
  344. template = settings.templates[settings.type],
  345. html = ''
  346. ;
  347. if(($.isPlainObject(response.results) && !$.isEmptyObject(response.results)) || ($.isArray(response.results) && response.results.length > 0) ) {
  348. if(settings.maxResults > 0) {
  349. response.results = $.makeArray(response.results).slice(0, settings.maxResults);
  350. }
  351. if($.isFunction(template)) {
  352. html = template(response);
  353. }
  354. else {
  355. module.error(error.noTemplate, false);
  356. }
  357. }
  358. else {
  359. html = module.message(error.noResults, 'empty');
  360. }
  361. $.proxy(settings.onResults, $module)(response);
  362. return html;
  363. },
  364. add: function(html) {
  365. if(settings.onResultsAdd == 'default' || $.proxy(settings.onResultsAdd, $results)(html) == 'default') {
  366. $results
  367. .html(html)
  368. ;
  369. }
  370. module.results.show();
  371. },
  372. show: function() {
  373. if( ($results.filter(':visible').size() === 0) && ($prompt.filter(':focus').size() > 0) && $results.html() !== '') {
  374. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  375. module.debug('Showing results with css animations');
  376. $results
  377. .transition(settings.transition + ' in', settings.duration)
  378. ;
  379. }
  380. else {
  381. module.debug('Showing results with javascript');
  382. $results
  383. .stop()
  384. .fadeIn(settings.duration, settings.easing)
  385. ;
  386. }
  387. $.proxy(settings.onResultsOpen, $results)();
  388. }
  389. },
  390. hide: function() {
  391. if($results.filter(':visible').size() > 0) {
  392. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  393. module.debug('Hiding results with css animations');
  394. $results
  395. .transition(settings.transition + ' out', settings.duration)
  396. ;
  397. }
  398. else {
  399. module.debug('Hiding results with javascript');
  400. $results
  401. .stop()
  402. .fadeIn(settings.duration, settings.easing)
  403. ;
  404. }
  405. $.proxy(settings.onResultsClose, $results)();
  406. }
  407. },
  408. select: function(event) {
  409. module.debug('Search result selected');
  410. var
  411. $result = $(this),
  412. $title = $result.find('.title'),
  413. title = $title.html()
  414. ;
  415. if(settings.onSelect == 'default' || $.proxy(settings.onSelect, this)(event) == 'default') {
  416. var
  417. $link = $result.find('a[href]').eq(0),
  418. href = $link.attr('href') || false,
  419. target = $link.attr('target') || false
  420. ;
  421. module.results.hide();
  422. if(href) {
  423. if(target == '_blank' || event.ctrlKey) {
  424. window.open(href);
  425. }
  426. else {
  427. window.location.href = (href);
  428. }
  429. }
  430. }
  431. }
  432. },
  433. // displays mesage visibly in search results
  434. message: function(text, type) {
  435. type = type || 'standard';
  436. module.results.add( settings.templates.message(text, type) );
  437. return settings.templates.message(text, type);
  438. },
  439. setting: function(name, value) {
  440. if( $.isPlainObject(name) ) {
  441. $.extend(true, settings, name);
  442. }
  443. else if(value !== undefined) {
  444. settings[name] = value;
  445. }
  446. else {
  447. return settings[name];
  448. }
  449. },
  450. internal: function(name, value) {
  451. if( $.isPlainObject(name) ) {
  452. $.extend(true, module, name);
  453. }
  454. else if(value !== undefined) {
  455. module[name] = value;
  456. }
  457. else {
  458. return module[name];
  459. }
  460. },
  461. debug: function() {
  462. if(settings.debug) {
  463. if(settings.performance) {
  464. module.performance.log(arguments);
  465. }
  466. else {
  467. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  468. module.debug.apply(console, arguments);
  469. }
  470. }
  471. },
  472. verbose: function() {
  473. if(settings.verbose && settings.debug) {
  474. if(settings.performance) {
  475. module.performance.log(arguments);
  476. }
  477. else {
  478. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  479. module.verbose.apply(console, arguments);
  480. }
  481. }
  482. },
  483. error: function() {
  484. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  485. module.error.apply(console, arguments);
  486. },
  487. performance: {
  488. log: function(message) {
  489. var
  490. currentTime,
  491. executionTime,
  492. previousTime
  493. ;
  494. if(settings.performance) {
  495. currentTime = new Date().getTime();
  496. previousTime = time || currentTime;
  497. executionTime = currentTime - previousTime;
  498. time = currentTime;
  499. performance.push({
  500. 'Name' : message[0],
  501. 'Arguments' : [].slice.call(message, 1) || '',
  502. 'Element' : element,
  503. 'Execution Time' : executionTime
  504. });
  505. }
  506. clearTimeout(module.performance.timer);
  507. module.performance.timer = setTimeout(module.performance.display, 100);
  508. },
  509. display: function() {
  510. var
  511. title = settings.name + ':',
  512. totalTime = 0
  513. ;
  514. time = false;
  515. clearTimeout(module.performance.timer);
  516. $.each(performance, function(index, data) {
  517. totalTime += data['Execution Time'];
  518. });
  519. title += ' ' + totalTime + 'ms';
  520. if(moduleSelector) {
  521. title += ' \'' + moduleSelector + '\'';
  522. }
  523. if($allModules.size() > 1) {
  524. title += ' ' + '(' + $allModules.size() + ')';
  525. }
  526. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  527. console.groupCollapsed(title);
  528. if(console.table) {
  529. console.table(performance);
  530. }
  531. else {
  532. $.each(performance, function(index, data) {
  533. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  534. });
  535. }
  536. console.groupEnd();
  537. }
  538. performance = [];
  539. }
  540. },
  541. invoke: function(query, passedArguments, context) {
  542. var
  543. object = instance,
  544. maxDepth,
  545. found,
  546. response
  547. ;
  548. passedArguments = passedArguments || queryArguments;
  549. context = element || context;
  550. if(typeof query == 'string' && object !== undefined) {
  551. query = query.split(/[\. ]/);
  552. maxDepth = query.length - 1;
  553. $.each(query, function(depth, value) {
  554. var camelCaseValue = (depth != maxDepth)
  555. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  556. : query
  557. ;
  558. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  559. object = object[camelCaseValue];
  560. }
  561. else if( object[camelCaseValue] !== undefined ) {
  562. found = object[camelCaseValue];
  563. return false;
  564. }
  565. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  566. object = object[value];
  567. }
  568. else if( object[value] !== undefined ) {
  569. found = object[value];
  570. return false;
  571. }
  572. else {
  573. return false;
  574. }
  575. });
  576. }
  577. if ( $.isFunction( found ) ) {
  578. response = found.apply(context, passedArguments);
  579. }
  580. else if(found !== undefined) {
  581. response = found;
  582. }
  583. if($.isArray(returnedValue)) {
  584. returnedValue.push(response);
  585. }
  586. else if(returnedValue !== undefined) {
  587. returnedValue = [returnedValue, response];
  588. }
  589. else if(response !== undefined) {
  590. returnedValue = response;
  591. }
  592. return found;
  593. }
  594. };
  595. if(methodInvoked) {
  596. if(instance === undefined) {
  597. module.initialize();
  598. }
  599. module.invoke(query);
  600. }
  601. else {
  602. if(instance !== undefined) {
  603. module.destroy();
  604. }
  605. module.initialize();
  606. }
  607. })
  608. ;
  609. return (returnedValue !== undefined)
  610. ? returnedValue
  611. : this
  612. ;
  613. };
  614. $.fn.search.settings = {
  615. name : 'Search Module',
  616. namespace : 'search',
  617. debug : false,
  618. verbose : true,
  619. performance : true,
  620. // onSelect default action is defined in module
  621. onSelect : 'default',
  622. onResultsAdd : 'default',
  623. onSearchQuery : function(){},
  624. onResults : function(response){},
  625. onResultsOpen : function(){},
  626. onResultsClose : function(){},
  627. source : false,
  628. automatic : 'true',
  629. type : 'simple',
  630. hideDelay : 300,
  631. minCharacters : 3,
  632. searchThrottle : 300,
  633. maxResults : 7,
  634. cache : true,
  635. searchFields : [
  636. 'title',
  637. 'description'
  638. ],
  639. transition : 'scale',
  640. duration : 300,
  641. easing : 'easeOutExpo',
  642. // api config
  643. apiSettings: false,
  644. className: {
  645. active : 'active',
  646. down : 'down',
  647. focus : 'focus',
  648. empty : 'empty',
  649. loading : 'loading'
  650. },
  651. error : {
  652. source : 'No source or api action specified',
  653. noResults : 'Your search returned no results',
  654. logging : 'Error in debug logging, exiting.',
  655. noTemplate : 'A valid template name was not specified.',
  656. serverError : 'There was an issue with querying the server.',
  657. method : 'The method you called is not defined.'
  658. },
  659. selector : {
  660. prompt : '.prompt',
  661. searchButton : '.search.button',
  662. results : '.results',
  663. category : '.category',
  664. result : '.result'
  665. },
  666. templates: {
  667. escape: function(string) {
  668. var
  669. badChars = /[&<>"'`]/g,
  670. shouldEscape = /[&<>"'`]/,
  671. escape = {
  672. "&": "&amp;",
  673. "<": "&lt;",
  674. ">": "&gt;",
  675. '"': "&quot;",
  676. "'": "&#x27;",
  677. "`": "&#x60;"
  678. },
  679. escapedChar = function(chr) {
  680. return escape[chr];
  681. }
  682. ;
  683. if(shouldEscape.test(string)) {
  684. return string.replace(badChars, escapedChar);
  685. }
  686. return string;
  687. },
  688. message: function(message, type) {
  689. var
  690. html = ''
  691. ;
  692. if(message !== undefined && type !== undefined) {
  693. html += ''
  694. + '<div class="message ' + type + '">'
  695. ;
  696. // message type
  697. if(type == 'empty') {
  698. html += ''
  699. + '<div class="header">No Results</div class="header">'
  700. + '<div class="description">' + message + '</div class="description">'
  701. ;
  702. }
  703. else {
  704. html += ' <div class="description">' + message + '</div>';
  705. }
  706. html += '</div>';
  707. }
  708. return html;
  709. },
  710. categories: function(response) {
  711. var
  712. html = '',
  713. escape = $.fn.search.settings.templates.escape
  714. ;
  715. if(response.results !== undefined) {
  716. // each category
  717. $.each(response.results, function(index, category) {
  718. if(category.results !== undefined && category.results.length > 0) {
  719. html += ''
  720. + '<div class="category">'
  721. + '<div class="name">' + category.name + '</div>'
  722. ;
  723. // each item inside category
  724. $.each(category.results, function(index, result) {
  725. html += '<div class="result">';
  726. html += '<a href="' + result.url + '"></a>';
  727. if(result.image !== undefined) {
  728. result.image = escape(result.image);
  729. html += ''
  730. + '<div class="image">'
  731. + ' <img src="' + result.image + '" alt="">'
  732. + '</div>'
  733. ;
  734. }
  735. html += '<div class="content">';
  736. if(result.price !== undefined) {
  737. result.price = escape(result.price);
  738. html += '<div class="price">' + result.price + '</div>';
  739. }
  740. if(result.title !== undefined) {
  741. result.title = escape(result.title);
  742. html += '<div class="title">' + result.title + '</div>';
  743. }
  744. if(result.description !== undefined) {
  745. html += '<div class="description">' + result.description + '</div>';
  746. }
  747. html += ''
  748. + '</div>'
  749. + '</div>'
  750. ;
  751. });
  752. html += ''
  753. + '</div>'
  754. ;
  755. }
  756. });
  757. if(response.resultPage) {
  758. html += ''
  759. + '<a href="' + response.resultPage.url + '" class="all">'
  760. + response.resultPage.text
  761. + '</a>';
  762. }
  763. return html;
  764. }
  765. return false;
  766. },
  767. simple: function(response) {
  768. var
  769. html = ''
  770. ;
  771. if(response.results !== undefined) {
  772. // each result
  773. $.each(response.results, function(index, result) {
  774. html += '<a class="result" href="' + result.url + '">';
  775. if(result.image !== undefined) {
  776. html += ''
  777. + '<div class="image">'
  778. + ' <img src="' + result.image + '">'
  779. + '</div>'
  780. ;
  781. }
  782. html += '<div class="content">';
  783. if(result.price !== undefined) {
  784. html += '<div class="price">' + result.price + '</div>';
  785. }
  786. if(result.title !== undefined) {
  787. html += '<div class="title">' + result.title + '</div>';
  788. }
  789. if(result.description !== undefined) {
  790. html += '<div class="description">' + result.description + '</div>';
  791. }
  792. html += ''
  793. + '</div>'
  794. + '</a>'
  795. ;
  796. });
  797. if(response.resultPage) {
  798. html += ''
  799. + '<a href="' + response.resultPage.url + '" class="all">'
  800. + response.resultPage.text
  801. + '</a>';
  802. }
  803. return html;
  804. }
  805. return false;
  806. }
  807. }
  808. };
  809. })( jQuery, window , document );