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.

771 lines
24 KiB

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