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.

813 lines
26 KiB

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