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.

777 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/semantic-org/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(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 !== undefined && prompt.oninput !== undefined)
  49. ? 'input'
  50. : (prompt !== undefined && 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($.isPlainObject(settings.source)) {
  226. module.search.local(searchTerm);
  227. }
  228. else if(settings.apiSettings) {
  229. module.search.remote(searchTerm);
  230. }
  231. else if($.api !== undefined && $.api.settings.api.search !== undefined) {
  232. module.debug('Searching with default search API endpoint');
  233. settings.apiSettings = {
  234. action: 'search'
  235. };
  236. module.search.remote(searchTerm);
  237. }
  238. else {
  239. module.error(error.source);
  240. }
  241. $.proxy(settings.onSearchQuery, $module)(searchTerm);
  242. }
  243. },
  244. local: function(searchTerm) {
  245. var
  246. results = [],
  247. fullTextResults = [],
  248. searchFields = $.isArray(settings.searchFields)
  249. ? settings.searchFields
  250. : [settings.searchFields],
  251. searchRegExp = new RegExp('(?:\s|^)' + searchTerm, 'i'),
  252. fullTextRegExp = new RegExp(searchTerm, 'i'),
  253. searchHTML
  254. ;
  255. $module
  256. .addClass(className.loading)
  257. ;
  258. // iterate through search fields in array order
  259. $.each(searchFields, function(index, field) {
  260. $.each(settings.source, function(label, thing) {
  261. if(typeof thing[field] == 'string' && ($.inArray(thing, results) == -1) && ($.inArray(thing, fullTextResults) == -1) ) {
  262. if( searchRegExp.test( thing[field] ) ) {
  263. results.push(thing);
  264. }
  265. else if( fullTextRegExp.test( thing[field] ) ) {
  266. fullTextResults.push(thing);
  267. }
  268. }
  269. });
  270. });
  271. searchHTML = module.results.generate({
  272. results: $.merge(results, fullTextResults)
  273. });
  274. $module
  275. .removeClass(className.loading)
  276. ;
  277. module.search.cache.write(searchTerm, searchHTML);
  278. module.results.add(searchHTML);
  279. },
  280. remote: function(searchTerm) {
  281. var
  282. apiSettings = {
  283. stateContext : $module,
  284. urlData: {
  285. query: searchTerm
  286. },
  287. success : function(response) {
  288. searchHTML = module.results.generate(response);
  289. module.search.cache.write(searchTerm, searchHTML);
  290. module.results.add(searchHTML);
  291. },
  292. failure : module.error
  293. },
  294. searchHTML
  295. ;
  296. module.search.cancel();
  297. module.debug('Executing search');
  298. $.extend(true, apiSettings, settings.apiSettings);
  299. $.api(apiSettings);
  300. },
  301. cache: {
  302. read: function(name) {
  303. var
  304. cache = $module.data('cache')
  305. ;
  306. return (settings.cache && (typeof cache == 'object') && (cache[name] !== undefined) )
  307. ? cache[name]
  308. : false
  309. ;
  310. },
  311. write: function(name, value) {
  312. var
  313. cache = ($module.data('cache') !== undefined)
  314. ? $module.data('cache')
  315. : {}
  316. ;
  317. cache[name] = value;
  318. $module
  319. .data('cache', cache)
  320. ;
  321. }
  322. }
  323. },
  324. results: {
  325. generate: function(response) {
  326. module.debug('Generating html from response', response);
  327. var
  328. template = settings.templates[settings.type],
  329. html = ''
  330. ;
  331. if(($.isPlainObject(response.results) && !$.isEmptyObject(response.results)) || ($.isArray(response.results) && response.results.length > 0) ) {
  332. if(settings.maxResults > 0) {
  333. response.results = $.makeArray(response.results).slice(0, settings.maxResults);
  334. }
  335. if(response.results.length > 0) {
  336. if($.isFunction(template)) {
  337. html = template(response);
  338. }
  339. else {
  340. module.error(error.noTemplate, false);
  341. }
  342. }
  343. }
  344. else {
  345. html = module.message(error.noResults, 'empty');
  346. }
  347. $.proxy(settings.onResults, $module)(response);
  348. return html;
  349. },
  350. add: function(html) {
  351. if(settings.onResultsAdd == 'default' || $.proxy(settings.onResultsAdd, $results)(html) == 'default') {
  352. $results
  353. .html(html)
  354. ;
  355. }
  356. module.results.show();
  357. },
  358. show: function() {
  359. if( ($results.filter(':visible').size() === 0) && ($prompt.filter(':focus').size() > 0) && $results.html() !== '') {
  360. $results
  361. .stop()
  362. .fadeIn(200)
  363. ;
  364. $.proxy(settings.onResultsOpen, $results)();
  365. }
  366. },
  367. hide: function() {
  368. if($results.filter(':visible').size() > 0) {
  369. $results
  370. .stop()
  371. .fadeOut(200)
  372. ;
  373. $.proxy(settings.onResultsClose, $results)();
  374. }
  375. },
  376. select: function(event) {
  377. module.debug('Search result selected');
  378. var
  379. $result = $(this),
  380. $title = $result.find('.title'),
  381. title = $title.html()
  382. ;
  383. if(settings.onSelect == 'default' || $.proxy(settings.onSelect, this)(event) == 'default') {
  384. var
  385. $link = $result.find('a[href]').eq(0),
  386. href = $link.attr('href') || false,
  387. target = $link.attr('target') || false
  388. ;
  389. module.results.hide();
  390. $prompt
  391. .val(title)
  392. ;
  393. if(href) {
  394. if(target == '_blank' || event.ctrlKey) {
  395. window.open(href);
  396. }
  397. else {
  398. window.location.href = (href);
  399. }
  400. }
  401. }
  402. }
  403. },
  404. setting: function(name, value) {
  405. if( $.isPlainObject(name) ) {
  406. $.extend(true, settings, name);
  407. }
  408. else if(value !== undefined) {
  409. settings[name] = value;
  410. }
  411. else {
  412. return settings[name];
  413. }
  414. },
  415. internal: function(name, value) {
  416. if( $.isPlainObject(name) ) {
  417. $.extend(true, module, name);
  418. }
  419. else if(value !== undefined) {
  420. module[name] = value;
  421. }
  422. else {
  423. return module[name];
  424. }
  425. },
  426. debug: function() {
  427. if(settings.debug) {
  428. if(settings.performance) {
  429. module.performance.log(arguments);
  430. }
  431. else {
  432. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  433. module.debug.apply(console, arguments);
  434. }
  435. }
  436. },
  437. verbose: function() {
  438. if(settings.verbose && settings.debug) {
  439. if(settings.performance) {
  440. module.performance.log(arguments);
  441. }
  442. else {
  443. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  444. module.verbose.apply(console, arguments);
  445. }
  446. }
  447. },
  448. error: function() {
  449. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  450. module.error.apply(console, arguments);
  451. },
  452. performance: {
  453. log: function(message) {
  454. var
  455. currentTime,
  456. executionTime,
  457. previousTime
  458. ;
  459. if(settings.performance) {
  460. currentTime = new Date().getTime();
  461. previousTime = time || currentTime;
  462. executionTime = currentTime - previousTime;
  463. time = currentTime;
  464. performance.push({
  465. 'Element' : element,
  466. 'Name' : message[0],
  467. 'Arguments' : [].slice.call(message, 1) || '',
  468. 'Execution Time' : executionTime
  469. });
  470. }
  471. clearTimeout(module.performance.timer);
  472. module.performance.timer = setTimeout(module.performance.display, 100);
  473. },
  474. display: function() {
  475. var
  476. title = settings.name + ':',
  477. totalTime = 0
  478. ;
  479. time = false;
  480. clearTimeout(module.performance.timer);
  481. $.each(performance, function(index, data) {
  482. totalTime += data['Execution Time'];
  483. });
  484. title += ' ' + totalTime + 'ms';
  485. if(moduleSelector) {
  486. title += ' \'' + moduleSelector + '\'';
  487. }
  488. if($allModules.size() > 1) {
  489. title += ' ' + '(' + $allModules.size() + ')';
  490. }
  491. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  492. console.groupCollapsed(title);
  493. if(console.table) {
  494. console.table(performance);
  495. }
  496. else {
  497. $.each(performance, function(index, data) {
  498. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  499. });
  500. }
  501. console.groupEnd();
  502. }
  503. performance = [];
  504. }
  505. },
  506. invoke: function(query, passedArguments, context) {
  507. var
  508. object = instance,
  509. maxDepth,
  510. found,
  511. response
  512. ;
  513. passedArguments = passedArguments || queryArguments;
  514. context = element || context;
  515. if(typeof query == 'string' && object !== undefined) {
  516. query = query.split(/[\. ]/);
  517. maxDepth = query.length - 1;
  518. $.each(query, function(depth, value) {
  519. var camelCaseValue = (depth != maxDepth)
  520. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  521. : query
  522. ;
  523. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  524. object = object[camelCaseValue];
  525. }
  526. else if( object[camelCaseValue] !== undefined ) {
  527. found = object[camelCaseValue];
  528. return false;
  529. }
  530. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  531. object = object[value];
  532. }
  533. else if( object[value] !== undefined ) {
  534. found = object[value];
  535. return false;
  536. }
  537. else {
  538. return false;
  539. }
  540. });
  541. }
  542. if ( $.isFunction( found ) ) {
  543. response = found.apply(context, passedArguments);
  544. }
  545. else if(found !== undefined) {
  546. response = found;
  547. }
  548. if($.isArray(returnedValue)) {
  549. returnedValue.push(response);
  550. }
  551. else if(returnedValue !== undefined) {
  552. returnedValue = [returnedValue, response];
  553. }
  554. else if(response !== undefined) {
  555. returnedValue = response;
  556. }
  557. return found;
  558. }
  559. };
  560. if(methodInvoked) {
  561. if(instance === undefined) {
  562. module.initialize();
  563. }
  564. module.invoke(query);
  565. }
  566. else {
  567. if(instance !== undefined) {
  568. module.destroy();
  569. }
  570. module.initialize();
  571. }
  572. })
  573. ;
  574. return (returnedValue !== undefined)
  575. ? returnedValue
  576. : this
  577. ;
  578. };
  579. $.fn.search.settings = {
  580. name : 'Search Module',
  581. namespace : 'search',
  582. debug : true,
  583. verbose : true,
  584. performance : true,
  585. // onSelect default action is defined in module
  586. onSelect : 'default',
  587. onResultsAdd : 'default',
  588. onSearchQuery : function(){},
  589. onResults : function(response){},
  590. onResultsOpen : function(){},
  591. onResultsClose : function(){},
  592. source : false,
  593. automatic : 'true',
  594. type : 'simple',
  595. minCharacters : 3,
  596. searchThrottle : 300,
  597. maxResults : 7,
  598. cache : true,
  599. searchFields : [
  600. 'title',
  601. 'description'
  602. ],
  603. // api config
  604. apiSettings: false,
  605. className: {
  606. active : 'active',
  607. down : 'down',
  608. focus : 'focus',
  609. empty : 'empty',
  610. loading : 'loading'
  611. },
  612. error : {
  613. source : 'No source or api action specified',
  614. noResults : 'Your search returned no results',
  615. logging : 'Error in debug logging, exiting.',
  616. noTemplate : 'A valid template name was not specified.',
  617. serverError : 'There was an issue with querying the server.',
  618. method : 'The method you called is not defined.'
  619. },
  620. selector : {
  621. prompt : '.prompt',
  622. searchButton : '.search.button',
  623. results : '.results',
  624. category : '.category',
  625. result : '.result'
  626. },
  627. templates: {
  628. message: function(message, type) {
  629. var
  630. html = ''
  631. ;
  632. if(message !== undefined && type !== undefined) {
  633. html += ''
  634. + '<div class="message ' + type +'">'
  635. ;
  636. // message type
  637. if(type == 'empty') {
  638. html += ''
  639. + '<div class="header">No Results</div class="header">'
  640. + '<div class="description">' + message + '</div class="description">'
  641. ;
  642. }
  643. else {
  644. html += ' <div class="description">' + message + '</div>';
  645. }
  646. html += '</div>';
  647. }
  648. return html;
  649. },
  650. categories: function(response) {
  651. var
  652. html = ''
  653. ;
  654. if(response.results !== undefined) {
  655. // each category
  656. $.each(response.results, function(index, category) {
  657. if(category.results !== undefined && category.results.length > 0) {
  658. html += ''
  659. + '<div class="category">'
  660. + '<div class="name">' + category.name + '</div>'
  661. ;
  662. // each item inside category
  663. $.each(category.results, function(index, result) {
  664. html += '<div class="result">';
  665. html += '<a href="' + result.url + '"></a>';
  666. if(result.image !== undefined) {
  667. html+= ''
  668. + '<div class="image">'
  669. + ' <img src="' + result.image + '">'
  670. + '</div>'
  671. ;
  672. }
  673. html += '<div class="info">';
  674. if(result.price !== undefined) {
  675. html+= '<div class="price">' + result.price + '</div>';
  676. }
  677. if(result.title !== undefined) {
  678. html+= '<div class="title">' + result.title + '</div>';
  679. }
  680. if(result.description !== undefined) {
  681. html+= '<div class="description">' + result.description + '</div>';
  682. }
  683. html += ''
  684. + '</div>'
  685. + '</div>'
  686. ;
  687. });
  688. html += ''
  689. + '</div>'
  690. ;
  691. }
  692. });
  693. if(response.resultPage) {
  694. html += ''
  695. + '<a href="' + response.resultPage.url + '" class="all">'
  696. + response.resultPage.text
  697. + '</a>';
  698. }
  699. return html;
  700. }
  701. return false;
  702. },
  703. simple: function(response) {
  704. var
  705. html = ''
  706. ;
  707. if(response.results !== undefined) {
  708. // each result
  709. $.each(response.results, function(index, result) {
  710. html += '<a class="result" href="' + result.url + '">';
  711. if(result.image !== undefined) {
  712. html+= ''
  713. + '<div class="image">'
  714. + ' <img src="' + result.image + '">'
  715. + '</div>'
  716. ;
  717. }
  718. html += '<div class="info">';
  719. if(result.price !== undefined) {
  720. html+= '<div class="price">' + result.price + '</div>';
  721. }
  722. if(result.title !== undefined) {
  723. html+= '<div class="title">' + result.title + '</div>';
  724. }
  725. if(result.description !== undefined) {
  726. html+= '<div class="description">' + result.description + '</div>';
  727. }
  728. html += ''
  729. + '</div>'
  730. + '</a>'
  731. ;
  732. });
  733. if(response.resultPage) {
  734. html += ''
  735. + '<a href="' + response.resultPage.url + '" class="all">'
  736. + response.resultPage.text
  737. + '</a>';
  738. }
  739. return html;
  740. }
  741. return false;
  742. }
  743. }
  744. };
  745. })( jQuery, window , document );