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.

841 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
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, thing) {
  273. if(typeof thing[field] == 'string' && ($.inArray(thing, results) == -1) && ($.inArray(thing, fullTextResults) == -1) ) {
  274. if( searchRegExp.test( thing[field] ) ) {
  275. results.push(thing);
  276. }
  277. else if( fullTextRegExp.test( thing[field] ) ) {
  278. fullTextResults.push(thing);
  279. }
  280. }
  281. });
  282. });
  283. searchHTML = module.results.generate({
  284. results: $.merge(results, fullTextResults)
  285. });
  286. $module
  287. .removeClass(className.loading)
  288. ;
  289. module.search.cache.write(searchTerm, searchHTML);
  290. module.results.add(searchHTML);
  291. },
  292. remote: function(searchTerm) {
  293. var
  294. apiSettings = {
  295. stateContext : $module,
  296. urlData: {
  297. query: searchTerm
  298. },
  299. onSuccess : function(response) {
  300. searchHTML = module.results.generate(response);
  301. module.search.cache.write(searchTerm, searchHTML);
  302. module.results.add(searchHTML);
  303. },
  304. failure : module.error
  305. },
  306. searchHTML
  307. ;
  308. module.search.cancel();
  309. module.debug('Executing search');
  310. $.extend(true, apiSettings, settings.apiSettings);
  311. $.api(apiSettings);
  312. },
  313. cache: {
  314. read: function(name) {
  315. var
  316. cache = $module.data('cache')
  317. ;
  318. return (settings.cache && (typeof cache == 'object') && (cache[name] !== undefined) )
  319. ? cache[name]
  320. : false
  321. ;
  322. },
  323. write: function(name, value) {
  324. var
  325. cache = ($module.data('cache') !== undefined)
  326. ? $module.data('cache')
  327. : {}
  328. ;
  329. cache[name] = value;
  330. $module
  331. .data('cache', cache)
  332. ;
  333. }
  334. }
  335. },
  336. results: {
  337. generate: function(response) {
  338. module.debug('Generating html from response', response);
  339. var
  340. template = settings.templates[settings.type],
  341. html = ''
  342. ;
  343. if(($.isPlainObject(response.results) && !$.isEmptyObject(response.results)) || ($.isArray(response.results) && response.results.length > 0) ) {
  344. if(settings.maxResults > 0) {
  345. response.results = $.makeArray(response.results).slice(0, settings.maxResults);
  346. }
  347. if($.isFunction(template)) {
  348. html = template(response);
  349. }
  350. else {
  351. module.error(error.noTemplate, false);
  352. }
  353. }
  354. else {
  355. html = module.message(error.noResults, 'empty');
  356. }
  357. $.proxy(settings.onResults, $module)(response);
  358. return html;
  359. },
  360. add: function(html) {
  361. if(settings.onResultsAdd == 'default' || $.proxy(settings.onResultsAdd, $results)(html) == 'default') {
  362. $results
  363. .html(html)
  364. ;
  365. }
  366. module.results.show();
  367. },
  368. show: function() {
  369. if( ($results.filter(':visible').size() === 0) && ($prompt.filter(':focus').size() > 0) && $results.html() !== '') {
  370. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  371. module.debug('Showing results with css animations');
  372. $results
  373. .transition(settings.transition + ' in', settings.duration)
  374. ;
  375. }
  376. else {
  377. module.debug('Showing results with javascript');
  378. $results
  379. .stop()
  380. .fadeIn(settings.duration, settings.easing)
  381. ;
  382. }
  383. $.proxy(settings.onResultsOpen, $results)();
  384. }
  385. },
  386. hide: function() {
  387. if($results.filter(':visible').size() > 0) {
  388. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  389. module.debug('Hiding results with css animations');
  390. $results
  391. .transition(settings.transition + ' out', settings.duration)
  392. ;
  393. }
  394. else {
  395. module.debug('Hiding results with javascript');
  396. $results
  397. .stop()
  398. .fadeIn(settings.duration, settings.easing)
  399. ;
  400. }
  401. $.proxy(settings.onResultsClose, $results)();
  402. }
  403. },
  404. select: function(event) {
  405. module.debug('Search result selected');
  406. var
  407. $result = $(this),
  408. $title = $result.find('.title'),
  409. title = $title.html()
  410. ;
  411. if(settings.onSelect == 'default' || $.proxy(settings.onSelect, this)(event) == 'default') {
  412. var
  413. $link = $result.find('a[href]').eq(0),
  414. href = $link.attr('href') || false,
  415. target = $link.attr('target') || false
  416. ;
  417. module.results.hide();
  418. if(href) {
  419. if(target == '_blank' || event.ctrlKey) {
  420. window.open(href);
  421. }
  422. else {
  423. window.location.href = (href);
  424. }
  425. }
  426. }
  427. }
  428. },
  429. // displays mesage visibly in search results
  430. message: function(text, type) {
  431. type = type || 'standard';
  432. module.results.add( settings.templates.message(text, type) );
  433. return settings.templates.message(text, type);
  434. },
  435. setting: function(name, value) {
  436. if( $.isPlainObject(name) ) {
  437. $.extend(true, settings, name);
  438. }
  439. else if(value !== undefined) {
  440. settings[name] = value;
  441. }
  442. else {
  443. return settings[name];
  444. }
  445. },
  446. internal: function(name, value) {
  447. if( $.isPlainObject(name) ) {
  448. $.extend(true, module, name);
  449. }
  450. else if(value !== undefined) {
  451. module[name] = value;
  452. }
  453. else {
  454. return module[name];
  455. }
  456. },
  457. debug: function() {
  458. if(settings.debug) {
  459. if(settings.performance) {
  460. module.performance.log(arguments);
  461. }
  462. else {
  463. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  464. module.debug.apply(console, arguments);
  465. }
  466. }
  467. },
  468. verbose: function() {
  469. if(settings.verbose && settings.debug) {
  470. if(settings.performance) {
  471. module.performance.log(arguments);
  472. }
  473. else {
  474. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  475. module.verbose.apply(console, arguments);
  476. }
  477. }
  478. },
  479. error: function() {
  480. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  481. module.error.apply(console, arguments);
  482. },
  483. performance: {
  484. log: function(message) {
  485. var
  486. currentTime,
  487. executionTime,
  488. previousTime
  489. ;
  490. if(settings.performance) {
  491. currentTime = new Date().getTime();
  492. previousTime = time || currentTime;
  493. executionTime = currentTime - previousTime;
  494. time = currentTime;
  495. performance.push({
  496. 'Element' : element,
  497. 'Name' : message[0],
  498. 'Arguments' : [].slice.call(message, 1) || '',
  499. 'Execution Time' : executionTime
  500. });
  501. }
  502. clearTimeout(module.performance.timer);
  503. module.performance.timer = setTimeout(module.performance.display, 100);
  504. },
  505. display: function() {
  506. var
  507. title = settings.name + ':',
  508. totalTime = 0
  509. ;
  510. time = false;
  511. clearTimeout(module.performance.timer);
  512. $.each(performance, function(index, data) {
  513. totalTime += data['Execution Time'];
  514. });
  515. title += ' ' + totalTime + 'ms';
  516. if(moduleSelector) {
  517. title += ' \'' + moduleSelector + '\'';
  518. }
  519. if($allModules.size() > 1) {
  520. title += ' ' + '(' + $allModules.size() + ')';
  521. }
  522. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  523. console.groupCollapsed(title);
  524. if(console.table) {
  525. console.table(performance);
  526. }
  527. else {
  528. $.each(performance, function(index, data) {
  529. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  530. });
  531. }
  532. console.groupEnd();
  533. }
  534. performance = [];
  535. }
  536. },
  537. invoke: function(query, passedArguments, context) {
  538. var
  539. object = instance,
  540. maxDepth,
  541. found,
  542. response
  543. ;
  544. passedArguments = passedArguments || queryArguments;
  545. context = element || context;
  546. if(typeof query == 'string' && object !== undefined) {
  547. query = query.split(/[\. ]/);
  548. maxDepth = query.length - 1;
  549. $.each(query, function(depth, value) {
  550. var camelCaseValue = (depth != maxDepth)
  551. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  552. : query
  553. ;
  554. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  555. object = object[camelCaseValue];
  556. }
  557. else if( object[camelCaseValue] !== undefined ) {
  558. found = object[camelCaseValue];
  559. return false;
  560. }
  561. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  562. object = object[value];
  563. }
  564. else if( object[value] !== undefined ) {
  565. found = object[value];
  566. return false;
  567. }
  568. else {
  569. return false;
  570. }
  571. });
  572. }
  573. if ( $.isFunction( found ) ) {
  574. response = found.apply(context, passedArguments);
  575. }
  576. else if(found !== undefined) {
  577. response = found;
  578. }
  579. if($.isArray(returnedValue)) {
  580. returnedValue.push(response);
  581. }
  582. else if(returnedValue !== undefined) {
  583. returnedValue = [returnedValue, response];
  584. }
  585. else if(response !== undefined) {
  586. returnedValue = response;
  587. }
  588. return found;
  589. }
  590. };
  591. if(methodInvoked) {
  592. if(instance === undefined) {
  593. module.initialize();
  594. }
  595. module.invoke(query);
  596. }
  597. else {
  598. if(instance !== undefined) {
  599. module.destroy();
  600. }
  601. module.initialize();
  602. }
  603. })
  604. ;
  605. return (returnedValue !== undefined)
  606. ? returnedValue
  607. : this
  608. ;
  609. };
  610. $.fn.search.settings = {
  611. name : 'Search Module',
  612. namespace : 'search',
  613. debug : false,
  614. verbose : true,
  615. performance : true,
  616. // onSelect default action is defined in module
  617. onSelect : 'default',
  618. onResultsAdd : 'default',
  619. onSearchQuery : function(){},
  620. onResults : function(response){},
  621. onResultsOpen : function(){},
  622. onResultsClose : function(){},
  623. source : false,
  624. automatic : 'true',
  625. type : 'simple',
  626. hideDelay : 300,
  627. minCharacters : 3,
  628. searchThrottle : 300,
  629. maxResults : 7,
  630. cache : true,
  631. searchFields : [
  632. 'title',
  633. 'description'
  634. ],
  635. transition : 'scale',
  636. duration : 300,
  637. easing : 'easeOutExpo',
  638. // api config
  639. apiSettings: false,
  640. className: {
  641. active : 'active',
  642. down : 'down',
  643. focus : 'focus',
  644. empty : 'empty',
  645. loading : 'loading'
  646. },
  647. error : {
  648. source : 'No source or api action specified',
  649. noResults : 'Your search returned no results',
  650. logging : 'Error in debug logging, exiting.',
  651. noTemplate : 'A valid template name was not specified.',
  652. serverError : 'There was an issue with querying the server.',
  653. method : 'The method you called is not defined.'
  654. },
  655. selector : {
  656. prompt : '.prompt',
  657. searchButton : '.search.button',
  658. results : '.results',
  659. category : '.category',
  660. result : '.result'
  661. },
  662. templates: {
  663. escape: function(string) {
  664. var
  665. badChars = /[&<>"'`]/g,
  666. shouldEscape = /[&<>"'`]/,
  667. escape = {
  668. "&": "&amp;",
  669. "<": "&lt;",
  670. ">": "&gt;",
  671. '"': "&quot;",
  672. "'": "&#x27;",
  673. "`": "&#x60;"
  674. },
  675. escapedChar = function(chr) {
  676. return escape[chr];
  677. }
  678. ;
  679. if(shouldEscape.test(string)) {
  680. return string.replace(badChars, escapedChar);
  681. }
  682. return string;
  683. },
  684. message: function(message, type) {
  685. var
  686. html = ''
  687. ;
  688. if(message !== undefined && type !== undefined) {
  689. html += ''
  690. + '<div class="message ' + type + '">'
  691. ;
  692. // message type
  693. if(type == 'empty') {
  694. html += ''
  695. + '<div class="header">No Results</div class="header">'
  696. + '<div class="description">' + message + '</div class="description">'
  697. ;
  698. }
  699. else {
  700. html += ' <div class="description">' + message + '</div>';
  701. }
  702. html += '</div>';
  703. }
  704. return html;
  705. },
  706. categories: function(response) {
  707. var
  708. html = '',
  709. escape = $.fn.search.settings.templates.escape
  710. ;
  711. if(response.results !== undefined) {
  712. // each category
  713. $.each(response.results, function(index, category) {
  714. if(category.results !== undefined && category.results.length > 0) {
  715. html += ''
  716. + '<div class="category">'
  717. + '<div class="name">' + category.name + '</div>'
  718. ;
  719. // each item inside category
  720. $.each(category.results, function(index, result) {
  721. html += '<div class="result">';
  722. html += '<a href="' + result.url + '"></a>';
  723. if(result.image !== undefined) {
  724. result.image = escape(result.image);
  725. html += ''
  726. + '<div class="image">'
  727. + ' <img src="' + result.image + '" alt="">'
  728. + '</div>'
  729. ;
  730. }
  731. html += '<div class="content">';
  732. if(result.price !== undefined) {
  733. result.price = escape(result.price);
  734. html += '<div class="price">' + result.price + '</div>';
  735. }
  736. if(result.title !== undefined) {
  737. result.title = escape(result.title);
  738. html += '<div class="title">' + result.title + '</div>';
  739. }
  740. if(result.description !== undefined) {
  741. html += '<div class="description">' + result.description + '</div>';
  742. }
  743. html += ''
  744. + '</div>'
  745. + '</div>'
  746. ;
  747. });
  748. html += ''
  749. + '</div>'
  750. ;
  751. }
  752. });
  753. if(response.resultPage) {
  754. html += ''
  755. + '<a href="' + response.resultPage.url + '" class="all">'
  756. + response.resultPage.text
  757. + '</a>';
  758. }
  759. return html;
  760. }
  761. return false;
  762. },
  763. simple: function(response) {
  764. var
  765. html = ''
  766. ;
  767. if(response.results !== undefined) {
  768. // each result
  769. $.each(response.results, function(index, result) {
  770. html += '<a class="result" href="' + result.url + '">';
  771. if(result.image !== undefined) {
  772. html += ''
  773. + '<div class="image">'
  774. + ' <img src="' + result.image + '">'
  775. + '</div>'
  776. ;
  777. }
  778. html += '<div class="content">';
  779. if(result.price !== undefined) {
  780. html += '<div class="price">' + result.price + '</div>';
  781. }
  782. if(result.title !== undefined) {
  783. html += '<div class="title">' + result.title + '</div>';
  784. }
  785. if(result.description !== undefined) {
  786. html += '<div class="description">' + result.description + '</div>';
  787. }
  788. html += ''
  789. + '</div>'
  790. + '</a>'
  791. ;
  792. });
  793. if(response.resultPage) {
  794. html += ''
  795. + '<a href="' + response.resultPage.url + '" class="all">'
  796. + response.resultPage.text
  797. + '</a>';
  798. }
  799. return html;
  800. }
  801. return false;
  802. }
  803. }
  804. };
  805. })( jQuery, window , document );