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.

804 lines
26 KiB

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