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.

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