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
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).exists() ) {
  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. 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[camelCaseValue] ) && (depth != maxDepth) ) {
  512. instance = instance[camelCaseValue];
  513. }
  514. else if( instance[camelCaseValue] !== undefined ) {
  515. found = instance[camelCaseValue];
  516. return false;
  517. }
  518. else if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  519. instance = instance[value];
  520. }
  521. else if( instance[value] !== undefined ) {
  522. found = instance[value];
  523. return false;
  524. }
  525. else {
  526. module.error(error.method, query);
  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 : 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 );