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.

765 lines
24 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  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. 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. returnedValue
  22. ;
  23. $(this)
  24. .each(function() {
  25. var
  26. settings = $.extend(true, {}, $.fn.search.settings, parameters),
  27. className = settings.className,
  28. selector = settings.selector,
  29. error = settings.error,
  30. namespace = settings.namespace,
  31. eventNamespace = '.' + namespace,
  32. moduleNamespace = namespace + '-module',
  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).size() > 0 ) {
  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. if( $.isPlainObject(name) ) {
  395. $.extend(true, settings, name);
  396. }
  397. else if(value !== undefined) {
  398. settings[name] = value;
  399. }
  400. else {
  401. return settings[name];
  402. }
  403. },
  404. internal: function(name, value) {
  405. if( $.isPlainObject(name) ) {
  406. $.extend(true, module, name);
  407. }
  408. else if(value !== undefined) {
  409. module[name] = value;
  410. }
  411. else {
  412. return module[name];
  413. }
  414. },
  415. debug: function() {
  416. if(settings.debug) {
  417. if(settings.performance) {
  418. module.performance.log(arguments);
  419. }
  420. else {
  421. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  422. module.debug.apply(console, arguments);
  423. }
  424. }
  425. },
  426. verbose: function() {
  427. if(settings.verbose && settings.debug) {
  428. if(settings.performance) {
  429. module.performance.log(arguments);
  430. }
  431. else {
  432. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  433. module.verbose.apply(console, arguments);
  434. }
  435. }
  436. },
  437. error: function() {
  438. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  439. module.error.apply(console, arguments);
  440. },
  441. performance: {
  442. log: function(message) {
  443. var
  444. currentTime,
  445. executionTime,
  446. previousTime
  447. ;
  448. if(settings.performance) {
  449. currentTime = new Date().getTime();
  450. previousTime = time || currentTime;
  451. executionTime = currentTime - previousTime;
  452. time = currentTime;
  453. performance.push({
  454. 'Element' : element,
  455. 'Name' : message[0],
  456. 'Arguments' : [].slice.call(message, 1) || '',
  457. 'Execution Time' : executionTime
  458. });
  459. }
  460. clearTimeout(module.performance.timer);
  461. module.performance.timer = setTimeout(module.performance.display, 100);
  462. },
  463. display: function() {
  464. var
  465. title = settings.name + ':',
  466. totalTime = 0
  467. ;
  468. time = false;
  469. clearTimeout(module.performance.timer);
  470. $.each(performance, function(index, data) {
  471. totalTime += data['Execution Time'];
  472. });
  473. title += ' ' + totalTime + 'ms';
  474. if(moduleSelector) {
  475. title += ' \'' + moduleSelector + '\'';
  476. }
  477. if($allModules.size() > 1) {
  478. title += ' ' + '(' + $allModules.size() + ')';
  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. object = instance,
  498. maxDepth,
  499. found,
  500. response
  501. ;
  502. passedArguments = passedArguments || queryArguments;
  503. context = element || context;
  504. if(typeof query == 'string' && object !== undefined) {
  505. query = query.split(/[\. ]/);
  506. maxDepth = query.length - 1;
  507. $.each(query, function(depth, value) {
  508. var camelCaseValue = (depth != maxDepth)
  509. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  510. : query
  511. ;
  512. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  513. object = object[camelCaseValue];
  514. }
  515. else if( object[camelCaseValue] !== undefined ) {
  516. found = object[camelCaseValue];
  517. return false;
  518. }
  519. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  520. object = object[value];
  521. }
  522. else if( object[value] !== undefined ) {
  523. found = object[value];
  524. return false;
  525. }
  526. else {
  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(returnedValue)) {
  538. returnedValue.push(response);
  539. }
  540. else if(returnedValue !== undefined) {
  541. returnedValue = [returnedValue, response];
  542. }
  543. else if(response !== undefined) {
  544. returnedValue = 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 (returnedValue !== undefined)
  564. ? returnedValue
  565. : this
  566. ;
  567. };
  568. $.fn.search.settings = {
  569. name : 'Search Module',
  570. namespace : 'search',
  571. debug : false,
  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 );