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.

839 lines
26 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
  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. $.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) || $.isArray(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. escape: function(string) {
  663. var
  664. badChars = /[&<>"'`]/g,
  665. shouldEscape = /[&<>"'`]/,
  666. escape = {
  667. "&": "&amp;",
  668. "<": "&lt;",
  669. ">": "&gt;",
  670. '"': "&quot;",
  671. "'": "&#x27;",
  672. "`": "&#x60;"
  673. },
  674. escapedChar = function(chr) {
  675. return escape[chr];
  676. }
  677. ;
  678. if(shouldEscape.test(string)) {
  679. return string.replace(badChars, escapedChar);
  680. }
  681. return string;
  682. },
  683. message: function(message, type) {
  684. var
  685. html = ''
  686. ;
  687. if(message !== undefined && type !== undefined) {
  688. html += ''
  689. + '<div class="message ' + type + '">'
  690. ;
  691. // message type
  692. if(type == 'empty') {
  693. html += ''
  694. + '<div class="header">No Results</div class="header">'
  695. + '<div class="description">' + message + '</div class="description">'
  696. ;
  697. }
  698. else {
  699. html += ' <div class="description">' + message + '</div>';
  700. }
  701. html += '</div>';
  702. }
  703. return html;
  704. },
  705. categories: function(response) {
  706. var
  707. html = '',
  708. escape = $.fn.search.settings.templates.escape
  709. ;
  710. if(response.results !== undefined) {
  711. // each category
  712. $.each(response.results, function(index, category) {
  713. if(category.results !== undefined && category.results.length > 0) {
  714. html += ''
  715. + '<div class="category">'
  716. + '<div class="name">' + category.name + '</div>'
  717. ;
  718. // each item inside category
  719. $.each(category.results, function(index, result) {
  720. html += '<div class="result">';
  721. html += '<a href="' + result.url + '"></a>';
  722. if(result.image !== undefined) {
  723. result.image = escape(result.image);
  724. html += ''
  725. + '<div class="image">'
  726. + ' <img src="' + result.image + '" alt="">'
  727. + '</div>'
  728. ;
  729. }
  730. html += '<div class="content">';
  731. if(result.price !== undefined) {
  732. result.price = escape(result.price);
  733. html += '<div class="price">' + result.price + '</div>';
  734. }
  735. if(result.title !== undefined) {
  736. result.title = escape(result.title);
  737. html += '<div class="title">' + result.title + '</div>';
  738. }
  739. if(result.description !== undefined) {
  740. html += '<div class="description">' + result.description + '</div>';
  741. }
  742. html += ''
  743. + '</div>'
  744. + '</div>'
  745. ;
  746. });
  747. html += ''
  748. + '</div>'
  749. ;
  750. }
  751. });
  752. if(response.resultPage) {
  753. html += ''
  754. + '<a href="' + response.resultPage.url + '" class="all">'
  755. + response.resultPage.text
  756. + '</a>';
  757. }
  758. return html;
  759. }
  760. return false;
  761. },
  762. simple: function(response) {
  763. var
  764. html = ''
  765. ;
  766. if(response.results !== undefined) {
  767. // each result
  768. $.each(response.results, function(index, result) {
  769. html += '<a class="result" href="' + result.url + '">';
  770. if(result.image !== undefined) {
  771. html += ''
  772. + '<div class="image">'
  773. + ' <img src="' + result.image + '">'
  774. + '</div>'
  775. ;
  776. }
  777. html += '<div class="content">';
  778. if(result.price !== undefined) {
  779. html += '<div class="price">' + result.price + '</div>';
  780. }
  781. if(result.title !== undefined) {
  782. html += '<div class="title">' + result.title + '</div>';
  783. }
  784. if(result.description !== undefined) {
  785. html += '<div class="description">' + result.description + '</div>';
  786. }
  787. html += ''
  788. + '</div>'
  789. + '</a>'
  790. ;
  791. });
  792. if(response.resultPage) {
  793. html += ''
  794. + '<a href="' + response.resultPage.url + '" class="all">'
  795. + response.resultPage.text
  796. + '</a>';
  797. }
  798. return html;
  799. }
  800. return false;
  801. }
  802. }
  803. };
  804. })( jQuery, window , document );