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
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  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. 'Element' : element,
  501. 'Name' : message[0],
  502. 'Arguments' : [].slice.call(message, 1) || '',
  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 );