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.

766 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.moduleName + ':',
  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( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  481. console.groupCollapsed(title);
  482. if(console.table) {
  483. console.table(performance);
  484. }
  485. else {
  486. $.each(performance, function(index, data) {
  487. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  488. });
  489. }
  490. console.groupEnd();
  491. }
  492. performance = [];
  493. }
  494. },
  495. invoke: function(query, passedArguments, context) {
  496. var
  497. maxDepth,
  498. found,
  499. response
  500. ;
  501. passedArguments = passedArguments || queryArguments;
  502. context = element || context;
  503. if(typeof query == 'string' && instance !== undefined) {
  504. query = query.split(/[\. ]/);
  505. maxDepth = query.length - 1;
  506. $.each(query, function(depth, value) {
  507. var camelCaseValue = (depth != maxDepth)
  508. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  509. : query
  510. ;
  511. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  512. instance = instance[value];
  513. }
  514. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  515. instance = instance[camelCaseValue];
  516. }
  517. else if( instance[value] !== undefined ) {
  518. found = instance[value];
  519. return false;
  520. }
  521. else if( instance[camelCaseValue] !== undefined ) {
  522. found = instance[camelCaseValue];
  523. return false;
  524. }
  525. else {
  526. module.error(error.method);
  527. return false;
  528. }
  529. });
  530. }
  531. if ( $.isFunction( found ) ) {
  532. response = found.apply(context, passedArguments);
  533. }
  534. else if(found !== undefined) {
  535. response = found;
  536. }
  537. if($.isArray(invokedResponse)) {
  538. invokedResponse.push(response);
  539. }
  540. else if(typeof invokedResponse == 'string') {
  541. invokedResponse = [invokedResponse, response];
  542. }
  543. else if(response !== undefined) {
  544. invokedResponse = response;
  545. }
  546. return found;
  547. }
  548. };
  549. if(methodInvoked) {
  550. if(instance === undefined) {
  551. module.initialize();
  552. }
  553. module.invoke(query);
  554. }
  555. else {
  556. if(instance !== undefined) {
  557. module.destroy();
  558. }
  559. module.initialize();
  560. }
  561. })
  562. ;
  563. return (invokedResponse !== undefined)
  564. ? invokedResponse
  565. : this
  566. ;
  567. };
  568. $.fn.search.settings = {
  569. moduleName : 'Search Module',
  570. namespace : 'search',
  571. debug : true,
  572. verbose : true,
  573. performance : true,
  574. // onSelect default action is defined in module
  575. onSelect : 'default',
  576. onResultsAdd : 'default',
  577. onSearchQuery : function(){},
  578. onResults : function(response){},
  579. onResultsOpen : function(){},
  580. onResultsClose : function(){},
  581. automatic : 'true',
  582. type : 'simple',
  583. minCharacters : 3,
  584. searchThrottle : 300,
  585. maxResults : 7,
  586. cache : true,
  587. searchFields : [
  588. 'title',
  589. 'description'
  590. ],
  591. // api config
  592. apiSettings: {
  593. },
  594. className: {
  595. active : 'active',
  596. down : 'down',
  597. focus : 'focus',
  598. empty : 'empty',
  599. loading : 'loading'
  600. },
  601. error : {
  602. noResults : 'Your search returned no results',
  603. logging : 'Error in debug logging, exiting.',
  604. noTemplate : 'A valid template name was not specified.',
  605. serverError : 'There was an issue with querying the server.',
  606. method : 'The method you called is not defined.'
  607. },
  608. selector : {
  609. prompt : '.prompt',
  610. searchButton : '.search.button',
  611. results : '.results',
  612. category : '.category',
  613. result : '.result'
  614. },
  615. templates: {
  616. message: function(message, type) {
  617. var
  618. html = ''
  619. ;
  620. if(message !== undefined && type !== undefined) {
  621. html += ''
  622. + '<div class="message ' + type +'">'
  623. ;
  624. // message type
  625. if(type == 'empty') {
  626. html += ''
  627. + '<div class="header">No Results</div class="header">'
  628. + '<div class="description">' + message + '</div class="description">'
  629. ;
  630. }
  631. else {
  632. html += ' <div class="description">' + message + '</div>';
  633. }
  634. html += '</div>';
  635. }
  636. return html;
  637. },
  638. categories: function(response) {
  639. var
  640. html = ''
  641. ;
  642. if(response.results !== undefined) {
  643. // each category
  644. $.each(response.results, function(index, category) {
  645. if(category.results !== undefined && category.results.length > 0) {
  646. html += ''
  647. + '<div class="category">'
  648. + '<div class="name">' + category.name + '</div>'
  649. ;
  650. // each item inside category
  651. $.each(category.results, function(index, result) {
  652. html += '<div class="result">';
  653. html += '<a href="' + result.url + '"></a>';
  654. if(result.image !== undefined) {
  655. html+= ''
  656. + '<div class="image">'
  657. + ' <img src="' + result.image + '">'
  658. + '</div>'
  659. ;
  660. }
  661. html += '<div class="info">';
  662. if(result.price !== undefined) {
  663. html+= '<div class="price">' + result.price + '</div>';
  664. }
  665. if(result.title !== undefined) {
  666. html+= '<div class="title">' + result.title + '</div>';
  667. }
  668. if(result.description !== undefined) {
  669. html+= '<div class="description">' + result.description + '</div>';
  670. }
  671. html += ''
  672. + '</div>'
  673. + '</div>'
  674. ;
  675. });
  676. html += ''
  677. + '</div>'
  678. ;
  679. }
  680. });
  681. if(response.resultPage) {
  682. html += ''
  683. + '<a href="' + response.resultPage.url + '" class="all">'
  684. + response.resultPage.text
  685. + '</a>';
  686. }
  687. return html;
  688. }
  689. return false;
  690. },
  691. simple: function(response) {
  692. var
  693. html = ''
  694. ;
  695. if(response.results !== undefined) {
  696. // each result
  697. $.each(response.results, function(index, result) {
  698. html += '<a class="result" href="' + result.url + '">';
  699. if(result.image !== undefined) {
  700. html+= ''
  701. + '<div class="image">'
  702. + ' <img src="' + result.image + '">'
  703. + '</div>'
  704. ;
  705. }
  706. html += '<div class="info">';
  707. if(result.price !== undefined) {
  708. html+= '<div class="price">' + result.price + '</div>';
  709. }
  710. if(result.title !== undefined) {
  711. html+= '<div class="title">' + result.title + '</div>';
  712. }
  713. if(result.description !== undefined) {
  714. html+= '<div class="description">' + result.description + '</div>';
  715. }
  716. html += ''
  717. + '</div>'
  718. + '</a>'
  719. ;
  720. });
  721. if(response.resultPage) {
  722. html += ''
  723. + '<a href="' + response.resultPage.url + '" class="all">'
  724. + response.resultPage.text
  725. + '</a>';
  726. }
  727. return html;
  728. }
  729. return false;
  730. }
  731. }
  732. };
  733. })( jQuery, window , document );