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.

768 lines
24 KiB

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