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.

784 lines
25 KiB

10 years ago
10 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(response.results.length > 0) {
  338. if($.isFunction(template)) {
  339. html = template(response);
  340. }
  341. else {
  342. module.error(error.noTemplate, false);
  343. }
  344. }
  345. }
  346. else {
  347. html = module.message(error.noResults, 'empty');
  348. }
  349. $.proxy(settings.onResults, $module)(response);
  350. return html;
  351. },
  352. add: function(html) {
  353. if(settings.onResultsAdd == 'default' || $.proxy(settings.onResultsAdd, $results)(html) == 'default') {
  354. $results
  355. .html(html)
  356. ;
  357. }
  358. module.results.show();
  359. },
  360. show: function() {
  361. if( ($results.filter(':visible').size() === 0) && ($prompt.filter(':focus').size() > 0) && $results.html() !== '') {
  362. $results
  363. .stop()
  364. .fadeIn(200)
  365. ;
  366. $.proxy(settings.onResultsOpen, $results)();
  367. }
  368. },
  369. hide: function() {
  370. if($results.filter(':visible').size() > 0) {
  371. $results
  372. .stop()
  373. .fadeOut(200)
  374. ;
  375. $.proxy(settings.onResultsClose, $results)();
  376. }
  377. },
  378. select: function(event) {
  379. module.debug('Search result selected');
  380. var
  381. $result = $(this),
  382. $title = $result.find('.title'),
  383. title = $title.html()
  384. ;
  385. if(settings.onSelect == 'default' || $.proxy(settings.onSelect, this)(event) == 'default') {
  386. var
  387. $link = $result.find('a[href]').eq(0),
  388. href = $link.attr('href') || false,
  389. target = $link.attr('target') || false
  390. ;
  391. module.results.hide();
  392. if(href) {
  393. if(target == '_blank' || event.ctrlKey) {
  394. window.open(href);
  395. }
  396. else {
  397. window.location.href = (href);
  398. }
  399. }
  400. }
  401. }
  402. },
  403. // displays mesage visibly in search results
  404. message: function(text, type) {
  405. type = type || 'standard';
  406. module.results.add( settings.templates.message(text, type) );
  407. return settings.templates.message(text, type);
  408. },
  409. setting: function(name, value) {
  410. if( $.isPlainObject(name) ) {
  411. $.extend(true, settings, name);
  412. }
  413. else if(value !== undefined) {
  414. settings[name] = value;
  415. }
  416. else {
  417. return settings[name];
  418. }
  419. },
  420. internal: function(name, value) {
  421. if( $.isPlainObject(name) ) {
  422. $.extend(true, module, name);
  423. }
  424. else if(value !== undefined) {
  425. module[name] = value;
  426. }
  427. else {
  428. return module[name];
  429. }
  430. },
  431. debug: function() {
  432. if(settings.debug) {
  433. if(settings.performance) {
  434. module.performance.log(arguments);
  435. }
  436. else {
  437. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  438. module.debug.apply(console, arguments);
  439. }
  440. }
  441. },
  442. verbose: function() {
  443. if(settings.verbose && settings.debug) {
  444. if(settings.performance) {
  445. module.performance.log(arguments);
  446. }
  447. else {
  448. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  449. module.verbose.apply(console, arguments);
  450. }
  451. }
  452. },
  453. error: function() {
  454. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  455. module.error.apply(console, arguments);
  456. },
  457. performance: {
  458. log: function(message) {
  459. var
  460. currentTime,
  461. executionTime,
  462. previousTime
  463. ;
  464. if(settings.performance) {
  465. currentTime = new Date().getTime();
  466. previousTime = time || currentTime;
  467. executionTime = currentTime - previousTime;
  468. time = currentTime;
  469. performance.push({
  470. 'Element' : element,
  471. 'Name' : message[0],
  472. 'Arguments' : [].slice.call(message, 1) || '',
  473. 'Execution Time' : executionTime
  474. });
  475. }
  476. clearTimeout(module.performance.timer);
  477. module.performance.timer = setTimeout(module.performance.display, 100);
  478. },
  479. display: function() {
  480. var
  481. title = settings.name + ':',
  482. totalTime = 0
  483. ;
  484. time = false;
  485. clearTimeout(module.performance.timer);
  486. $.each(performance, function(index, data) {
  487. totalTime += data['Execution Time'];
  488. });
  489. title += ' ' + totalTime + 'ms';
  490. if(moduleSelector) {
  491. title += ' \'' + moduleSelector + '\'';
  492. }
  493. if($allModules.size() > 1) {
  494. title += ' ' + '(' + $allModules.size() + ')';
  495. }
  496. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  497. console.groupCollapsed(title);
  498. if(console.table) {
  499. console.table(performance);
  500. }
  501. else {
  502. $.each(performance, function(index, data) {
  503. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  504. });
  505. }
  506. console.groupEnd();
  507. }
  508. performance = [];
  509. }
  510. },
  511. invoke: function(query, passedArguments, context) {
  512. var
  513. object = instance,
  514. maxDepth,
  515. found,
  516. response
  517. ;
  518. passedArguments = passedArguments || queryArguments;
  519. context = element || context;
  520. if(typeof query == 'string' && object !== undefined) {
  521. query = query.split(/[\. ]/);
  522. maxDepth = query.length - 1;
  523. $.each(query, function(depth, value) {
  524. var camelCaseValue = (depth != maxDepth)
  525. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  526. : query
  527. ;
  528. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  529. object = object[camelCaseValue];
  530. }
  531. else if( object[camelCaseValue] !== undefined ) {
  532. found = object[camelCaseValue];
  533. return false;
  534. }
  535. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  536. object = object[value];
  537. }
  538. else if( object[value] !== undefined ) {
  539. found = object[value];
  540. return false;
  541. }
  542. else {
  543. return false;
  544. }
  545. });
  546. }
  547. if ( $.isFunction( found ) ) {
  548. response = found.apply(context, passedArguments);
  549. }
  550. else if(found !== undefined) {
  551. response = found;
  552. }
  553. if($.isArray(returnedValue)) {
  554. returnedValue.push(response);
  555. }
  556. else if(returnedValue !== undefined) {
  557. returnedValue = [returnedValue, response];
  558. }
  559. else if(response !== undefined) {
  560. returnedValue = response;
  561. }
  562. return found;
  563. }
  564. };
  565. if(methodInvoked) {
  566. if(instance === undefined) {
  567. module.initialize();
  568. }
  569. module.invoke(query);
  570. }
  571. else {
  572. if(instance !== undefined) {
  573. module.destroy();
  574. }
  575. module.initialize();
  576. }
  577. })
  578. ;
  579. return (returnedValue !== undefined)
  580. ? returnedValue
  581. : this
  582. ;
  583. };
  584. $.fn.search.settings = {
  585. name : 'Search Module',
  586. namespace : 'search',
  587. debug : false,
  588. verbose : true,
  589. performance : true,
  590. // onSelect default action is defined in module
  591. onSelect : 'default',
  592. onResultsAdd : 'default',
  593. onSearchQuery : function(){},
  594. onResults : function(response){},
  595. onResultsOpen : function(){},
  596. onResultsClose : function(){},
  597. source : false,
  598. automatic : 'true',
  599. type : 'simple',
  600. hideDelay : 600,
  601. minCharacters : 3,
  602. searchThrottle : 300,
  603. maxResults : 7,
  604. cache : true,
  605. searchFields : [
  606. 'title',
  607. 'description'
  608. ],
  609. // api config
  610. apiSettings: false,
  611. className: {
  612. active : 'active',
  613. down : 'down',
  614. focus : 'focus',
  615. empty : 'empty',
  616. loading : 'loading'
  617. },
  618. error : {
  619. source : 'No source or api action specified',
  620. noResults : 'Your search returned no results',
  621. logging : 'Error in debug logging, exiting.',
  622. noTemplate : 'A valid template name was not specified.',
  623. serverError : 'There was an issue with querying the server.',
  624. method : 'The method you called is not defined.'
  625. },
  626. selector : {
  627. prompt : '.prompt',
  628. searchButton : '.search.button',
  629. results : '.results',
  630. category : '.category',
  631. result : '.result'
  632. },
  633. templates: {
  634. message: function(message, type) {
  635. var
  636. html = ''
  637. ;
  638. if(message !== undefined && type !== undefined) {
  639. html += ''
  640. + '<div class="message ' + type +'">'
  641. ;
  642. // message type
  643. if(type == 'empty') {
  644. html += ''
  645. + '<div class="header">No Results</div class="header">'
  646. + '<div class="description">' + message + '</div class="description">'
  647. ;
  648. }
  649. else {
  650. html += ' <div class="description">' + message + '</div>';
  651. }
  652. html += '</div>';
  653. }
  654. return html;
  655. },
  656. categories: function(response) {
  657. var
  658. html = ''
  659. ;
  660. if(response.results !== undefined) {
  661. // each category
  662. $.each(response.results, function(index, category) {
  663. if(category.results !== undefined && category.results.length > 0) {
  664. html += ''
  665. + '<div class="category">'
  666. + '<div class="name">' + category.name + '</div>'
  667. ;
  668. // each item inside category
  669. $.each(category.results, function(index, result) {
  670. html += '<div class="result">';
  671. html += '<a href="' + result.url + '"></a>';
  672. if(result.image !== undefined) {
  673. html+= ''
  674. + '<div class="image">'
  675. + ' <img src="' + result.image + '">'
  676. + '</div>'
  677. ;
  678. }
  679. html += '<div class="content">';
  680. if(result.price !== undefined) {
  681. html+= '<div class="price">' + result.price + '</div>';
  682. }
  683. if(result.title !== undefined) {
  684. html+= '<div class="title">' + result.title + '</div>';
  685. }
  686. if(result.description !== undefined) {
  687. html+= '<div class="description">' + result.description + '</div>';
  688. }
  689. html += ''
  690. + '</div>'
  691. + '</div>'
  692. ;
  693. });
  694. html += ''
  695. + '</div>'
  696. ;
  697. }
  698. });
  699. if(response.resultPage) {
  700. html += ''
  701. + '<a href="' + response.resultPage.url + '" class="all">'
  702. + response.resultPage.text
  703. + '</a>';
  704. }
  705. return html;
  706. }
  707. return false;
  708. },
  709. simple: function(response) {
  710. var
  711. html = ''
  712. ;
  713. if(response.results !== undefined) {
  714. // each result
  715. $.each(response.results, function(index, result) {
  716. html += '<a class="result" href="' + result.url + '">';
  717. if(result.image !== undefined) {
  718. html+= ''
  719. + '<div class="image">'
  720. + ' <img src="' + result.image + '">'
  721. + '</div>'
  722. ;
  723. }
  724. html += '<div class="content">';
  725. if(result.price !== undefined) {
  726. html+= '<div class="price">' + result.price + '</div>';
  727. }
  728. if(result.title !== undefined) {
  729. html+= '<div class="title">' + result.title + '</div>';
  730. }
  731. if(result.description !== undefined) {
  732. html+= '<div class="description">' + result.description + '</div>';
  733. }
  734. html += ''
  735. + '</div>'
  736. + '</a>'
  737. ;
  738. });
  739. if(response.resultPage) {
  740. html += ''
  741. + '<a href="' + response.resultPage.url + '" class="all">'
  742. + response.resultPage.text
  743. + '</a>';
  744. }
  745. return html;
  746. }
  747. return false;
  748. }
  749. }
  750. };
  751. })( jQuery, window , document );