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.

769 lines
24 KiB

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