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.

1451 lines
46 KiB

9 years ago
10 years ago
10 years ago
9 years ago
10 years ago
7 years ago
8 years ago
10 years ago
9 years ago
10 years ago
9 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
7 years ago
10 years ago
8 years ago
7 years ago
10 years ago
9 years ago
10 years ago
9 years ago
7 years ago
9 years ago
8 years ago
9 years ago
10 years ago
8 years ago
9 years ago
10 years ago
8 years ago
10 years ago
10 years ago
10 years ago
8 years ago
8 years ago
10 years ago
7 years ago
10 years ago
10 years ago
10 years ago
8 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
7 years ago
7 years ago
7 years ago
9 years ago
9 years ago
8 years ago
8 years ago
8 years ago
9 years ago
8 years ago
9 years ago
8 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
7 years ago
10 years ago
10 years ago
7 years ago
7 years ago
10 years ago
7 years ago
7 years ago
7 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
7 years ago
7 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
7 years ago
10 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
7 years ago
10 years ago
7 years ago
10 years ago
9 years ago
10 years ago
7 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
7 years ago
10 years ago
9 years ago
10 years ago
7 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
10 years ago
9 years ago
9 years ago
8 years ago
9 years ago
9 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
8 years ago
10 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
  1. /*!
  2. * # Semantic UI 2.2.14 - Search
  3. * http://github.com/semantic-org/semantic-ui/
  4. *
  5. *
  6. * Released under the MIT license
  7. * http://opensource.org/licenses/MIT
  8. *
  9. */
  10. ;(function ($, window, document, undefined) {
  11. "use strict";
  12. window = (typeof window != 'undefined' && window.Math == Math)
  13. ? window
  14. : (typeof self != 'undefined' && self.Math == Math)
  15. ? self
  16. : Function('return this')()
  17. ;
  18. $.fn.search = function(parameters) {
  19. var
  20. $allModules = $(this),
  21. moduleSelector = $allModules.selector || '',
  22. time = new Date().getTime(),
  23. performance = [],
  24. query = arguments[0],
  25. methodInvoked = (typeof query == 'string'),
  26. queryArguments = [].slice.call(arguments, 1),
  27. returnedValue
  28. ;
  29. $(this)
  30. .each(function() {
  31. var
  32. settings = ( $.isPlainObject(parameters) )
  33. ? $.extend(true, {}, $.fn.search.settings, parameters)
  34. : $.extend({}, $.fn.search.settings),
  35. className = settings.className,
  36. metadata = settings.metadata,
  37. regExp = settings.regExp,
  38. fields = settings.fields,
  39. selector = settings.selector,
  40. error = settings.error,
  41. namespace = settings.namespace,
  42. eventNamespace = '.' + namespace,
  43. moduleNamespace = namespace + '-module',
  44. $module = $(this),
  45. $prompt = $module.find(selector.prompt),
  46. $searchButton = $module.find(selector.searchButton),
  47. $results = $module.find(selector.results),
  48. $result = $module.find(selector.result),
  49. $category = $module.find(selector.category),
  50. element = this,
  51. instance = $module.data(moduleNamespace),
  52. disabledBubbled = false,
  53. resultsDismissed = false,
  54. module
  55. ;
  56. module = {
  57. initialize: function() {
  58. module.verbose('Initializing module');
  59. module.determine.searchFields();
  60. module.bind.events();
  61. module.set.type();
  62. module.create.results();
  63. module.instantiate();
  64. },
  65. instantiate: function() {
  66. module.verbose('Storing instance of module', module);
  67. instance = module;
  68. $module
  69. .data(moduleNamespace, module)
  70. ;
  71. },
  72. destroy: function() {
  73. module.verbose('Destroying instance');
  74. $module
  75. .off(eventNamespace)
  76. .removeData(moduleNamespace)
  77. ;
  78. },
  79. refresh: function() {
  80. module.debug('Refreshing selector cache');
  81. $prompt = $module.find(selector.prompt);
  82. $searchButton = $module.find(selector.searchButton);
  83. $category = $module.find(selector.category);
  84. $results = $module.find(selector.results);
  85. $result = $module.find(selector.result);
  86. },
  87. refreshResults: function() {
  88. $results = $module.find(selector.results);
  89. $result = $module.find(selector.result);
  90. },
  91. bind: {
  92. events: function() {
  93. module.verbose('Binding events to search');
  94. if(settings.automatic) {
  95. $module
  96. .on(module.get.inputEvent() + eventNamespace, selector.prompt, module.event.input)
  97. ;
  98. $prompt
  99. .attr('autocomplete', 'off')
  100. ;
  101. }
  102. $module
  103. // prompt
  104. .on('focus' + eventNamespace, selector.prompt, module.event.focus)
  105. .on('blur' + eventNamespace, selector.prompt, module.event.blur)
  106. .on('keydown' + eventNamespace, selector.prompt, module.handleKeyboard)
  107. // search button
  108. .on('click' + eventNamespace, selector.searchButton, module.query)
  109. // results
  110. .on('mousedown' + eventNamespace, selector.results, module.event.result.mousedown)
  111. .on('mouseup' + eventNamespace, selector.results, module.event.result.mouseup)
  112. .on('click' + eventNamespace, selector.result, module.event.result.click)
  113. ;
  114. }
  115. },
  116. determine: {
  117. searchFields: function() {
  118. // this makes sure $.extend does not add specified search fields to default fields
  119. // this is the only setting which should not extend defaults
  120. if(parameters && parameters.searchFields !== undefined) {
  121. settings.searchFields = parameters.searchFields;
  122. }
  123. }
  124. },
  125. event: {
  126. input: function() {
  127. if(settings.searchDelay) {
  128. clearTimeout(module.timer);
  129. module.timer = setTimeout(function() {
  130. if(module.is.focused()) {
  131. module.query();
  132. }
  133. }, settings.searchDelay);
  134. }
  135. else {
  136. module.query();
  137. }
  138. },
  139. focus: function() {
  140. module.set.focus();
  141. if(settings.searchOnFocus && module.has.minimumCharacters() ) {
  142. module.query(function() {
  143. if(module.can.show() ) {
  144. module.showResults();
  145. }
  146. });
  147. }
  148. },
  149. blur: function(event) {
  150. var
  151. pageLostFocus = (document.activeElement === this),
  152. callback = function() {
  153. module.cancel.query();
  154. module.remove.focus();
  155. module.timer = setTimeout(module.hideResults, settings.hideDelay);
  156. }
  157. ;
  158. if(pageLostFocus) {
  159. return;
  160. }
  161. resultsDismissed = false;
  162. if(module.resultsClicked) {
  163. module.debug('Determining if user action caused search to close');
  164. $module
  165. .one('click.close' + eventNamespace, selector.results, function(event) {
  166. if(module.is.inMessage(event) || disabledBubbled) {
  167. $prompt.focus();
  168. return;
  169. }
  170. disabledBubbled = false;
  171. if( !module.is.animating() && !module.is.hidden()) {
  172. callback();
  173. }
  174. })
  175. ;
  176. }
  177. else {
  178. module.debug('Input blurred without user action, closing results');
  179. callback();
  180. }
  181. },
  182. result: {
  183. mousedown: function() {
  184. module.resultsClicked = true;
  185. },
  186. mouseup: function() {
  187. module.resultsClicked = false;
  188. },
  189. click: function(event) {
  190. module.debug('Search result selected');
  191. var
  192. $result = $(this),
  193. $title = $result.find(selector.title).eq(0),
  194. $link = $result.is('a[href]')
  195. ? $result
  196. : $result.find('a[href]').eq(0),
  197. href = $link.attr('href') || false,
  198. target = $link.attr('target') || false,
  199. title = $title.html(),
  200. // title is used for result lookup
  201. value = ($title.length > 0)
  202. ? $title.text()
  203. : false,
  204. results = module.get.results(),
  205. result = $result.data(metadata.result) || module.get.result(value, results),
  206. returnedValue
  207. ;
  208. if( $.isFunction(settings.onSelect) ) {
  209. if(settings.onSelect.call(element, result, results) === false) {
  210. module.debug('Custom onSelect callback cancelled default select action');
  211. disabledBubbled = true;
  212. return;
  213. }
  214. }
  215. module.hideResults();
  216. if(value) {
  217. module.set.value(value);
  218. }
  219. if(href) {
  220. module.verbose('Opening search link found in result', $link);
  221. if(target == '_blank' || event.ctrlKey) {
  222. window.open(href);
  223. }
  224. else {
  225. window.location.href = (href);
  226. }
  227. }
  228. }
  229. }
  230. },
  231. handleKeyboard: function(event) {
  232. var
  233. // force selector refresh
  234. $result = $module.find(selector.result),
  235. $category = $module.find(selector.category),
  236. $activeResult = $result.filter('.' + className.active),
  237. currentIndex = $result.index( $activeResult ),
  238. resultSize = $result.length,
  239. hasActiveResult = $activeResult.length > 0,
  240. keyCode = event.which,
  241. keys = {
  242. backspace : 8,
  243. enter : 13,
  244. escape : 27,
  245. upArrow : 38,
  246. downArrow : 40
  247. },
  248. newIndex
  249. ;
  250. // search shortcuts
  251. if(keyCode == keys.escape) {
  252. module.verbose('Escape key pressed, blurring search field');
  253. module.hideResults();
  254. resultsDismissed = true;
  255. }
  256. if( module.is.visible() ) {
  257. if(keyCode == keys.enter) {
  258. module.verbose('Enter key pressed, selecting active result');
  259. if( $result.filter('.' + className.active).length > 0 ) {
  260. module.event.result.click.call($result.filter('.' + className.active), event);
  261. event.preventDefault();
  262. return false;
  263. }
  264. }
  265. else if(keyCode == keys.upArrow && hasActiveResult) {
  266. module.verbose('Up key pressed, changing active result');
  267. newIndex = (currentIndex - 1 < 0)
  268. ? currentIndex
  269. : currentIndex - 1
  270. ;
  271. $category
  272. .removeClass(className.active)
  273. ;
  274. $result
  275. .removeClass(className.active)
  276. .eq(newIndex)
  277. .addClass(className.active)
  278. .closest($category)
  279. .addClass(className.active)
  280. ;
  281. event.preventDefault();
  282. }
  283. else if(keyCode == keys.downArrow) {
  284. module.verbose('Down key pressed, changing active result');
  285. newIndex = (currentIndex + 1 >= resultSize)
  286. ? currentIndex
  287. : currentIndex + 1
  288. ;
  289. $category
  290. .removeClass(className.active)
  291. ;
  292. $result
  293. .removeClass(className.active)
  294. .eq(newIndex)
  295. .addClass(className.active)
  296. .closest($category)
  297. .addClass(className.active)
  298. ;
  299. event.preventDefault();
  300. }
  301. }
  302. else {
  303. // query shortcuts
  304. if(keyCode == keys.enter) {
  305. module.verbose('Enter key pressed, executing query');
  306. module.query();
  307. module.set.buttonPressed();
  308. $prompt.one('keyup', module.remove.buttonFocus);
  309. }
  310. }
  311. },
  312. setup: {
  313. api: function(searchTerm, callback) {
  314. var
  315. apiSettings = {
  316. debug : settings.debug,
  317. on : false,
  318. cache : true,
  319. action : 'search',
  320. urlData : {
  321. query : searchTerm
  322. },
  323. onSuccess : function(response) {
  324. module.parse.response.call(element, response, searchTerm);
  325. callback();
  326. },
  327. onFailure : function() {
  328. module.displayMessage(error.serverError);
  329. callback();
  330. },
  331. onAbort : function(response) {
  332. },
  333. onError : module.error
  334. },
  335. searchHTML
  336. ;
  337. $.extend(true, apiSettings, settings.apiSettings);
  338. module.verbose('Setting up API request', apiSettings);
  339. $module.api(apiSettings);
  340. }
  341. },
  342. can: {
  343. useAPI: function() {
  344. return $.fn.api !== undefined;
  345. },
  346. show: function() {
  347. return module.is.focused() && !module.is.visible() && !module.is.empty();
  348. },
  349. transition: function() {
  350. return settings.transition && $.fn.transition !== undefined && $module.transition('is supported');
  351. }
  352. },
  353. is: {
  354. animating: function() {
  355. return $results.hasClass(className.animating);
  356. },
  357. hidden: function() {
  358. return $results.hasClass(className.hidden);
  359. },
  360. inMessage: function(event) {
  361. if(!event.target) {
  362. return;
  363. }
  364. var
  365. $target = $(event.target),
  366. isInDOM = $.contains(document.documentElement, event.target)
  367. ;
  368. return (isInDOM && $target.closest(selector.message).length > 0);
  369. },
  370. empty: function() {
  371. return ($results.html() === '');
  372. },
  373. visible: function() {
  374. return ($results.filter(':visible').length > 0);
  375. },
  376. focused: function() {
  377. return ($prompt.filter(':focus').length > 0);
  378. }
  379. },
  380. get: {
  381. inputEvent: function() {
  382. var
  383. prompt = $prompt[0],
  384. inputEvent = (prompt !== undefined && prompt.oninput !== undefined)
  385. ? 'input'
  386. : (prompt !== undefined && prompt.onpropertychange !== undefined)
  387. ? 'propertychange'
  388. : 'keyup'
  389. ;
  390. return inputEvent;
  391. },
  392. value: function() {
  393. return $prompt.val();
  394. },
  395. results: function() {
  396. var
  397. results = $module.data(metadata.results)
  398. ;
  399. return results;
  400. },
  401. result: function(value, results) {
  402. var
  403. lookupFields = ['title', 'id'],
  404. result = false
  405. ;
  406. value = (value !== undefined)
  407. ? value
  408. : module.get.value()
  409. ;
  410. results = (results !== undefined)
  411. ? results
  412. : module.get.results()
  413. ;
  414. if(settings.type === 'category') {
  415. module.debug('Finding result that matches', value);
  416. $.each(results, function(index, category) {
  417. if($.isArray(category.results)) {
  418. result = module.search.object(value, category.results, lookupFields)[0];
  419. // don't continue searching if a result is found
  420. if(result) {
  421. return false;
  422. }
  423. }
  424. });
  425. }
  426. else {
  427. module.debug('Finding result in results object', value);
  428. result = module.search.object(value, results, lookupFields)[0];
  429. }
  430. return result || false;
  431. },
  432. },
  433. select: {
  434. firstResult: function() {
  435. module.verbose('Selecting first result');
  436. $result.first().addClass(className.active);
  437. }
  438. },
  439. set: {
  440. focus: function() {
  441. $module.addClass(className.focus);
  442. },
  443. loading: function() {
  444. $module.addClass(className.loading);
  445. },
  446. value: function(value) {
  447. module.verbose('Setting search input value', value);
  448. $prompt
  449. .val(value)
  450. ;
  451. },
  452. type: function(type) {
  453. type = type || settings.type;
  454. if(settings.type == 'category') {
  455. $module.addClass(settings.type);
  456. }
  457. },
  458. buttonPressed: function() {
  459. $searchButton.addClass(className.pressed);
  460. }
  461. },
  462. remove: {
  463. loading: function() {
  464. $module.removeClass(className.loading);
  465. },
  466. focus: function() {
  467. $module.removeClass(className.focus);
  468. },
  469. buttonPressed: function() {
  470. $searchButton.removeClass(className.pressed);
  471. }
  472. },
  473. query: function(callback) {
  474. callback = $.isFunction(callback)
  475. ? callback
  476. : function(){}
  477. ;
  478. var
  479. searchTerm = module.get.value(),
  480. cache = module.read.cache(searchTerm)
  481. ;
  482. callback = callback || function() {};
  483. if( module.has.minimumCharacters() ) {
  484. if(cache) {
  485. module.debug('Reading result from cache', searchTerm);
  486. module.save.results(cache.results);
  487. module.addResults(cache.html);
  488. module.inject.id(cache.results);
  489. callback();
  490. }
  491. else {
  492. module.debug('Querying for', searchTerm);
  493. if($.isPlainObject(settings.source) || $.isArray(settings.source)) {
  494. module.search.local(searchTerm);
  495. callback();
  496. }
  497. else if( module.can.useAPI() ) {
  498. module.search.remote(searchTerm, callback);
  499. }
  500. else {
  501. module.error(error.source);
  502. callback();
  503. }
  504. }
  505. settings.onSearchQuery.call(element, searchTerm);
  506. }
  507. else {
  508. module.hideResults();
  509. }
  510. },
  511. search: {
  512. local: function(searchTerm) {
  513. var
  514. results = module.search.object(searchTerm, settings.content),
  515. searchHTML
  516. ;
  517. module.set.loading();
  518. module.save.results(results);
  519. module.debug('Returned local search results', results);
  520. searchHTML = module.generateResults({
  521. results: results
  522. });
  523. module.remove.loading();
  524. module.addResults(searchHTML);
  525. module.inject.id(results);
  526. module.write.cache(searchTerm, {
  527. html : searchHTML,
  528. results : results
  529. });
  530. },
  531. remote: function(searchTerm, callback) {
  532. callback = $.isFunction(callback)
  533. ? callback
  534. : function(){}
  535. ;
  536. if($module.api('is loading')) {
  537. $module.api('abort');
  538. }
  539. module.setup.api(searchTerm, callback);
  540. $module
  541. .api('query')
  542. ;
  543. },
  544. object: function(searchTerm, source, searchFields) {
  545. var
  546. results = [],
  547. fuzzyResults = [],
  548. searchExp = searchTerm.toString().replace(regExp.escape, '\\$&'),
  549. matchRegExp = new RegExp(regExp.beginsWith + searchExp, 'i'),
  550. // avoid duplicates when pushing results
  551. addResult = function(array, result) {
  552. var
  553. notResult = ($.inArray(result, results) == -1),
  554. notFuzzyResult = ($.inArray(result, fuzzyResults) == -1)
  555. ;
  556. if(notResult && notFuzzyResult) {
  557. array.push(result);
  558. }
  559. }
  560. ;
  561. source = source || settings.source;
  562. searchFields = (searchFields !== undefined)
  563. ? searchFields
  564. : settings.searchFields
  565. ;
  566. // search fields should be array to loop correctly
  567. if(!$.isArray(searchFields)) {
  568. searchFields = [searchFields];
  569. }
  570. // exit conditions if no source
  571. if(source === undefined || source === false) {
  572. module.error(error.source);
  573. return [];
  574. }
  575. // iterate through search fields looking for matches
  576. $.each(searchFields, function(index, field) {
  577. $.each(source, function(label, content) {
  578. var
  579. fieldExists = (typeof content[field] == 'string')
  580. ;
  581. if(fieldExists) {
  582. if( content[field].search(matchRegExp) !== -1) {
  583. // content starts with value (first in results)
  584. addResult(results, content);
  585. }
  586. else if(settings.searchFullText && module.fuzzySearch(searchTerm, content[field]) ) {
  587. // content fuzzy matches (last in results)
  588. addResult(fuzzyResults, content);
  589. }
  590. }
  591. });
  592. });
  593. return $.merge(results, fuzzyResults);
  594. }
  595. },
  596. fuzzySearch: function(query, term) {
  597. var
  598. termLength = term.length,
  599. queryLength = query.length
  600. ;
  601. if(typeof query !== 'string') {
  602. return false;
  603. }
  604. query = query.toLowerCase();
  605. term = term.toLowerCase();
  606. if(queryLength > termLength) {
  607. return false;
  608. }
  609. if(queryLength === termLength) {
  610. return (query === term);
  611. }
  612. search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
  613. var
  614. queryCharacter = query.charCodeAt(characterIndex)
  615. ;
  616. while(nextCharacterIndex < termLength) {
  617. if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
  618. continue search;
  619. }
  620. }
  621. return false;
  622. }
  623. return true;
  624. },
  625. parse: {
  626. response: function(response, searchTerm) {
  627. var
  628. searchHTML = module.generateResults(response)
  629. ;
  630. module.verbose('Parsing server response', response);
  631. if(response !== undefined) {
  632. if(searchTerm !== undefined && response[fields.results] !== undefined) {
  633. module.addResults(searchHTML);
  634. module.inject.id(response[fields.results]);
  635. module.write.cache(searchTerm, {
  636. html : searchHTML,
  637. results : response[fields.results]
  638. });
  639. module.save.results(response[fields.results]);
  640. }
  641. }
  642. }
  643. },
  644. cancel: {
  645. query: function() {
  646. if( module.can.useAPI() ) {
  647. $module.api('abort');
  648. }
  649. }
  650. },
  651. has: {
  652. minimumCharacters: function() {
  653. var
  654. searchTerm = module.get.value(),
  655. numCharacters = searchTerm.length
  656. ;
  657. return (numCharacters >= settings.minCharacters);
  658. },
  659. results: function() {
  660. if($results.length === 0) {
  661. return false;
  662. }
  663. var
  664. html = $results.html()
  665. ;
  666. return html != '';
  667. }
  668. },
  669. clear: {
  670. cache: function(value) {
  671. var
  672. cache = $module.data(metadata.cache)
  673. ;
  674. if(!value) {
  675. module.debug('Clearing cache', value);
  676. $module.removeData(metadata.cache);
  677. }
  678. else if(value && cache && cache[value]) {
  679. module.debug('Removing value from cache', value);
  680. delete cache[value];
  681. $module.data(metadata.cache, cache);
  682. }
  683. }
  684. },
  685. read: {
  686. cache: function(name) {
  687. var
  688. cache = $module.data(metadata.cache)
  689. ;
  690. if(settings.cache) {
  691. module.verbose('Checking cache for generated html for query', name);
  692. return (typeof cache == 'object') && (cache[name] !== undefined)
  693. ? cache[name]
  694. : false
  695. ;
  696. }
  697. return false;
  698. }
  699. },
  700. create: {
  701. id: function(resultIndex, categoryIndex) {
  702. var
  703. resultID = (resultIndex + 1), // not zero indexed
  704. categoryID = (categoryIndex + 1),
  705. firstCharCode,
  706. letterID,
  707. id
  708. ;
  709. if(categoryIndex !== undefined) {
  710. // start char code for "A"
  711. letterID = String.fromCharCode(97 + categoryIndex);
  712. id = letterID + resultID;
  713. module.verbose('Creating category result id', id);
  714. }
  715. else {
  716. id = resultID;
  717. module.verbose('Creating result id', id);
  718. }
  719. return id;
  720. },
  721. results: function() {
  722. if($results.length === 0) {
  723. $results = $('<div />')
  724. .addClass(className.results)
  725. .appendTo($module)
  726. ;
  727. }
  728. }
  729. },
  730. inject: {
  731. result: function(result, resultIndex, categoryIndex) {
  732. module.verbose('Injecting result into results');
  733. var
  734. $selectedResult = (categoryIndex !== undefined)
  735. ? $results
  736. .children().eq(categoryIndex)
  737. .children(selector.result).eq(resultIndex)
  738. : $results
  739. .children(selector.result).eq(resultIndex)
  740. ;
  741. module.verbose('Injecting results metadata', $selectedResult);
  742. $selectedResult
  743. .data(metadata.result, result)
  744. ;
  745. },
  746. id: function(results) {
  747. module.debug('Injecting unique ids into results');
  748. var
  749. // since results may be object, we must use counters
  750. categoryIndex = 0,
  751. resultIndex = 0
  752. ;
  753. if(settings.type === 'category') {
  754. // iterate through each category result
  755. $.each(results, function(index, category) {
  756. resultIndex = 0;
  757. $.each(category.results, function(index, value) {
  758. var
  759. result = category.results[index]
  760. ;
  761. if(result.id === undefined) {
  762. result.id = module.create.id(resultIndex, categoryIndex);
  763. }
  764. module.inject.result(result, resultIndex, categoryIndex);
  765. resultIndex++;
  766. });
  767. categoryIndex++;
  768. });
  769. }
  770. else {
  771. // top level
  772. $.each(results, function(index, value) {
  773. var
  774. result = results[index]
  775. ;
  776. if(result.id === undefined) {
  777. result.id = module.create.id(resultIndex);
  778. }
  779. module.inject.result(result, resultIndex);
  780. resultIndex++;
  781. });
  782. }
  783. return results;
  784. }
  785. },
  786. save: {
  787. results: function(results) {
  788. module.verbose('Saving current search results to metadata', results);
  789. $module.data(metadata.results, results);
  790. }
  791. },
  792. write: {
  793. cache: function(name, value) {
  794. var
  795. cache = ($module.data(metadata.cache) !== undefined)
  796. ? $module.data(metadata.cache)
  797. : {}
  798. ;
  799. if(settings.cache) {
  800. module.verbose('Writing generated html to cache', name, value);
  801. cache[name] = value;
  802. $module
  803. .data(metadata.cache, cache)
  804. ;
  805. }
  806. }
  807. },
  808. addResults: function(html) {
  809. if( $.isFunction(settings.onResultsAdd) ) {
  810. if( settings.onResultsAdd.call($results, html) === false ) {
  811. module.debug('onResultsAdd callback cancelled default action');
  812. return false;
  813. }
  814. }
  815. if(html) {
  816. $results
  817. .html(html)
  818. ;
  819. module.refreshResults();
  820. if(settings.selectFirstResult) {
  821. module.select.firstResult();
  822. }
  823. module.showResults();
  824. }
  825. else {
  826. module.hideResults(function() {
  827. $results.empty();
  828. });
  829. }
  830. },
  831. showResults: function(callback) {
  832. callback = $.isFunction(callback)
  833. ? callback
  834. : function(){}
  835. ;
  836. if(resultsDismissed) {
  837. return;
  838. }
  839. if(!module.is.visible() && module.has.results()) {
  840. if( module.can.transition() ) {
  841. module.debug('Showing results with css animations');
  842. $results
  843. .transition({
  844. animation : settings.transition + ' in',
  845. debug : settings.debug,
  846. verbose : settings.verbose,
  847. duration : settings.duration,
  848. onComplete : function() {
  849. callback();
  850. },
  851. queue : true
  852. })
  853. ;
  854. }
  855. else {
  856. module.debug('Showing results with javascript');
  857. $results
  858. .stop()
  859. .fadeIn(settings.duration, settings.easing)
  860. ;
  861. }
  862. settings.onResultsOpen.call($results);
  863. }
  864. },
  865. hideResults: function(callback) {
  866. callback = $.isFunction(callback)
  867. ? callback
  868. : function(){}
  869. ;
  870. if( module.is.visible() ) {
  871. if( module.can.transition() ) {
  872. module.debug('Hiding results with css animations');
  873. $results
  874. .transition({
  875. animation : settings.transition + ' out',
  876. debug : settings.debug,
  877. verbose : settings.verbose,
  878. duration : settings.duration,
  879. onComplete : function() {
  880. callback();
  881. },
  882. queue : true
  883. })
  884. ;
  885. }
  886. else {
  887. module.debug('Hiding results with javascript');
  888. $results
  889. .stop()
  890. .fadeOut(settings.duration, settings.easing)
  891. ;
  892. }
  893. settings.onResultsClose.call($results);
  894. }
  895. },
  896. generateResults: function(response) {
  897. module.debug('Generating html from response', response);
  898. var
  899. template = settings.templates[settings.type],
  900. isProperObject = ($.isPlainObject(response[fields.results]) && !$.isEmptyObject(response[fields.results])),
  901. isProperArray = ($.isArray(response[fields.results]) && response[fields.results].length > 0),
  902. html = ''
  903. ;
  904. if(isProperObject || isProperArray ) {
  905. if(settings.maxResults > 0) {
  906. if(isProperObject) {
  907. if(settings.type == 'standard') {
  908. module.error(error.maxResults);
  909. }
  910. }
  911. else {
  912. response[fields.results] = response[fields.results].slice(0, settings.maxResults);
  913. }
  914. }
  915. if($.isFunction(template)) {
  916. html = template(response, fields);
  917. }
  918. else {
  919. module.error(error.noTemplate, false);
  920. }
  921. }
  922. else if(settings.showNoResults) {
  923. html = module.displayMessage(error.noResults, 'empty');
  924. }
  925. settings.onResults.call(element, response);
  926. return html;
  927. },
  928. displayMessage: function(text, type) {
  929. type = type || 'standard';
  930. module.debug('Displaying message', text, type);
  931. module.addResults( settings.templates.message(text, type) );
  932. return settings.templates.message(text, type);
  933. },
  934. setting: function(name, value) {
  935. if( $.isPlainObject(name) ) {
  936. $.extend(true, settings, name);
  937. }
  938. else if(value !== undefined) {
  939. settings[name] = value;
  940. }
  941. else {
  942. return settings[name];
  943. }
  944. },
  945. internal: function(name, value) {
  946. if( $.isPlainObject(name) ) {
  947. $.extend(true, module, name);
  948. }
  949. else if(value !== undefined) {
  950. module[name] = value;
  951. }
  952. else {
  953. return module[name];
  954. }
  955. },
  956. debug: function() {
  957. if(!settings.silent && settings.debug) {
  958. if(settings.performance) {
  959. module.performance.log(arguments);
  960. }
  961. else {
  962. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  963. module.debug.apply(console, arguments);
  964. }
  965. }
  966. },
  967. verbose: function() {
  968. if(!settings.silent && settings.verbose && settings.debug) {
  969. if(settings.performance) {
  970. module.performance.log(arguments);
  971. }
  972. else {
  973. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  974. module.verbose.apply(console, arguments);
  975. }
  976. }
  977. },
  978. error: function() {
  979. if(!settings.silent) {
  980. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  981. module.error.apply(console, arguments);
  982. }
  983. },
  984. performance: {
  985. log: function(message) {
  986. var
  987. currentTime,
  988. executionTime,
  989. previousTime
  990. ;
  991. if(settings.performance) {
  992. currentTime = new Date().getTime();
  993. previousTime = time || currentTime;
  994. executionTime = currentTime - previousTime;
  995. time = currentTime;
  996. performance.push({
  997. 'Name' : message[0],
  998. 'Arguments' : [].slice.call(message, 1) || '',
  999. 'Element' : element,
  1000. 'Execution Time' : executionTime
  1001. });
  1002. }
  1003. clearTimeout(module.performance.timer);
  1004. module.performance.timer = setTimeout(module.performance.display, 500);
  1005. },
  1006. display: function() {
  1007. var
  1008. title = settings.name + ':',
  1009. totalTime = 0
  1010. ;
  1011. time = false;
  1012. clearTimeout(module.performance.timer);
  1013. $.each(performance, function(index, data) {
  1014. totalTime += data['Execution Time'];
  1015. });
  1016. title += ' ' + totalTime + 'ms';
  1017. if(moduleSelector) {
  1018. title += ' \'' + moduleSelector + '\'';
  1019. }
  1020. if($allModules.length > 1) {
  1021. title += ' ' + '(' + $allModules.length + ')';
  1022. }
  1023. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  1024. console.groupCollapsed(title);
  1025. if(console.table) {
  1026. console.table(performance);
  1027. }
  1028. else {
  1029. $.each(performance, function(index, data) {
  1030. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  1031. });
  1032. }
  1033. console.groupEnd();
  1034. }
  1035. performance = [];
  1036. }
  1037. },
  1038. invoke: function(query, passedArguments, context) {
  1039. var
  1040. object = instance,
  1041. maxDepth,
  1042. found,
  1043. response
  1044. ;
  1045. passedArguments = passedArguments || queryArguments;
  1046. context = element || context;
  1047. if(typeof query == 'string' && object !== undefined) {
  1048. query = query.split(/[\. ]/);
  1049. maxDepth = query.length - 1;
  1050. $.each(query, function(depth, value) {
  1051. var camelCaseValue = (depth != maxDepth)
  1052. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  1053. : query
  1054. ;
  1055. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  1056. object = object[camelCaseValue];
  1057. }
  1058. else if( object[camelCaseValue] !== undefined ) {
  1059. found = object[camelCaseValue];
  1060. return false;
  1061. }
  1062. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  1063. object = object[value];
  1064. }
  1065. else if( object[value] !== undefined ) {
  1066. found = object[value];
  1067. return false;
  1068. }
  1069. else {
  1070. return false;
  1071. }
  1072. });
  1073. }
  1074. if( $.isFunction( found ) ) {
  1075. response = found.apply(context, passedArguments);
  1076. }
  1077. else if(found !== undefined) {
  1078. response = found;
  1079. }
  1080. if($.isArray(returnedValue)) {
  1081. returnedValue.push(response);
  1082. }
  1083. else if(returnedValue !== undefined) {
  1084. returnedValue = [returnedValue, response];
  1085. }
  1086. else if(response !== undefined) {
  1087. returnedValue = response;
  1088. }
  1089. return found;
  1090. }
  1091. };
  1092. if(methodInvoked) {
  1093. if(instance === undefined) {
  1094. module.initialize();
  1095. }
  1096. module.invoke(query);
  1097. }
  1098. else {
  1099. if(instance !== undefined) {
  1100. instance.invoke('destroy');
  1101. }
  1102. module.initialize();
  1103. }
  1104. })
  1105. ;
  1106. return (returnedValue !== undefined)
  1107. ? returnedValue
  1108. : this
  1109. ;
  1110. };
  1111. $.fn.search.settings = {
  1112. name : 'Search',
  1113. namespace : 'search',
  1114. silent : false,
  1115. debug : false,
  1116. verbose : false,
  1117. performance : true,
  1118. // template to use (specified in settings.templates)
  1119. type : 'standard',
  1120. // minimum characters required to search
  1121. minCharacters : 1,
  1122. // whether to select first result after searching automatically
  1123. selectFirstResult : false,
  1124. // API config
  1125. apiSettings : false,
  1126. // object to search
  1127. source : false,
  1128. // Whether search should query current term on focus
  1129. searchOnFocus : true,
  1130. // fields to search
  1131. searchFields : [
  1132. 'title',
  1133. 'description'
  1134. ],
  1135. // field to display in standard results template
  1136. displayField : '',
  1137. // whether to include fuzzy results in local search
  1138. searchFullText : true,
  1139. // whether to add events to prompt automatically
  1140. automatic : true,
  1141. // delay before hiding menu after blur
  1142. hideDelay : 0,
  1143. // delay before searching
  1144. searchDelay : 200,
  1145. // maximum results returned from local
  1146. maxResults : 7,
  1147. // whether to store lookups in local cache
  1148. cache : true,
  1149. // whether no results errors should be shown
  1150. showNoResults : true,
  1151. // transition settings
  1152. transition : 'scale',
  1153. duration : 200,
  1154. easing : 'easeOutExpo',
  1155. // callbacks
  1156. onSelect : false,
  1157. onResultsAdd : false,
  1158. onSearchQuery : function(query){},
  1159. onResults : function(response){},
  1160. onResultsOpen : function(){},
  1161. onResultsClose : function(){},
  1162. className: {
  1163. animating : 'animating',
  1164. active : 'active',
  1165. empty : 'empty',
  1166. focus : 'focus',
  1167. hidden : 'hidden',
  1168. loading : 'loading',
  1169. results : 'results',
  1170. pressed : 'down'
  1171. },
  1172. error : {
  1173. source : 'Cannot search. No source used, and Semantic API module was not included',
  1174. noResults : 'Your search returned no results',
  1175. logging : 'Error in debug logging, exiting.',
  1176. noEndpoint : 'No search endpoint was specified',
  1177. noTemplate : 'A valid template name was not specified.',
  1178. serverError : 'There was an issue querying the server.',
  1179. maxResults : 'Results must be an array to use maxResults setting',
  1180. method : 'The method you called is not defined.'
  1181. },
  1182. metadata: {
  1183. cache : 'cache',
  1184. results : 'results',
  1185. result : 'result'
  1186. },
  1187. regExp: {
  1188. escape : /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,
  1189. beginsWith : '(?:\s|^)'
  1190. },
  1191. // maps api response attributes to internal representation
  1192. fields: {
  1193. categories : 'results', // array of categories (category view)
  1194. categoryName : 'name', // name of category (category view)
  1195. categoryResults : 'results', // array of results (category view)
  1196. description : 'description', // result description
  1197. image : 'image', // result image
  1198. price : 'price', // result price
  1199. results : 'results', // array of results (standard)
  1200. title : 'title', // result title
  1201. url : 'url', // result url
  1202. action : 'action', // "view more" object name
  1203. actionText : 'text', // "view more" text
  1204. actionURL : 'url' // "view more" url
  1205. },
  1206. selector : {
  1207. prompt : '.prompt',
  1208. searchButton : '.search.button',
  1209. results : '.results',
  1210. message : '.results > .message',
  1211. category : '.category',
  1212. result : '.result',
  1213. title : '.title, .name'
  1214. },
  1215. templates: {
  1216. escape: function(string) {
  1217. var
  1218. badChars = /[&<>"'`]/g,
  1219. shouldEscape = /[&<>"'`]/,
  1220. escape = {
  1221. "&": "&amp;",
  1222. "<": "&lt;",
  1223. ">": "&gt;",
  1224. '"': "&quot;",
  1225. "'": "&#x27;",
  1226. "`": "&#x60;"
  1227. },
  1228. escapedChar = function(chr) {
  1229. return escape[chr];
  1230. }
  1231. ;
  1232. if(shouldEscape.test(string)) {
  1233. return string.replace(badChars, escapedChar);
  1234. }
  1235. return string;
  1236. },
  1237. message: function(message, type) {
  1238. var
  1239. html = ''
  1240. ;
  1241. if(message !== undefined && type !== undefined) {
  1242. html += ''
  1243. + '<div class="message ' + type + '">'
  1244. ;
  1245. // message type
  1246. if(type == 'empty') {
  1247. html += ''
  1248. + '<div class="header">No Results</div class="header">'
  1249. + '<div class="description">' + message + '</div class="description">'
  1250. ;
  1251. }
  1252. else {
  1253. html += ' <div class="description">' + message + '</div>';
  1254. }
  1255. html += '</div>';
  1256. }
  1257. return html;
  1258. },
  1259. category: function(response, fields) {
  1260. var
  1261. html = '',
  1262. escape = $.fn.search.settings.templates.escape
  1263. ;
  1264. if(response[fields.categoryResults] !== undefined) {
  1265. // each category
  1266. $.each(response[fields.categoryResults], function(index, category) {
  1267. if(category[fields.results] !== undefined && category.results.length > 0) {
  1268. html += '<div class="category">';
  1269. if(category[fields.categoryName] !== undefined) {
  1270. html += '<div class="name">' + category[fields.categoryName] + '</div>';
  1271. }
  1272. // each item inside category
  1273. $.each(category.results, function(index, result) {
  1274. if(result[fields.url]) {
  1275. html += '<a class="result" href="' + result[fields.url] + '">';
  1276. }
  1277. else {
  1278. html += '<a class="result">';
  1279. }
  1280. if(result[fields.image] !== undefined) {
  1281. html += ''
  1282. + '<div class="image">'
  1283. + ' <img src="' + result[fields.image] + '">'
  1284. + '</div>'
  1285. ;
  1286. }
  1287. html += '<div class="content">';
  1288. if(result[fields.price] !== undefined) {
  1289. html += '<div class="price">' + result[fields.price] + '</div>';
  1290. }
  1291. if(result[fields.title] !== undefined) {
  1292. html += '<div class="title">' + result[fields.title] + '</div>';
  1293. }
  1294. if(result[fields.description] !== undefined) {
  1295. html += '<div class="description">' + result[fields.description] + '</div>';
  1296. }
  1297. html += ''
  1298. + '</div>'
  1299. ;
  1300. html += '</a>';
  1301. });
  1302. html += ''
  1303. + '</div>'
  1304. ;
  1305. }
  1306. });
  1307. if(response[fields.action]) {
  1308. html += ''
  1309. + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">'
  1310. + response[fields.action][fields.actionText]
  1311. + '</a>';
  1312. }
  1313. return html;
  1314. }
  1315. return false;
  1316. },
  1317. standard: function(response, fields) {
  1318. var
  1319. html = ''
  1320. ;
  1321. if(response[fields.results] !== undefined) {
  1322. // each result
  1323. $.each(response[fields.results], function(index, result) {
  1324. if(result[fields.url]) {
  1325. html += '<a class="result" href="' + result[fields.url] + '">';
  1326. }
  1327. else {
  1328. html += '<a class="result">';
  1329. }
  1330. if(result[fields.image] !== undefined) {
  1331. html += ''
  1332. + '<div class="image">'
  1333. + ' <img src="' + result[fields.image] + '">'
  1334. + '</div>'
  1335. ;
  1336. }
  1337. html += '<div class="content">';
  1338. if(result[fields.price] !== undefined) {
  1339. html += '<div class="price">' + result[fields.price] + '</div>';
  1340. }
  1341. if(result[fields.title] !== undefined) {
  1342. html += '<div class="title">' + result[fields.title] + '</div>';
  1343. }
  1344. if(result[fields.description] !== undefined) {
  1345. html += '<div class="description">' + result[fields.description] + '</div>';
  1346. }
  1347. html += ''
  1348. + '</div>'
  1349. ;
  1350. html += '</a>';
  1351. });
  1352. if(response[fields.action]) {
  1353. html += ''
  1354. + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">'
  1355. + response[fields.action][fields.actionText]
  1356. + '</a>';
  1357. }
  1358. return html;
  1359. }
  1360. return false;
  1361. }
  1362. }
  1363. };
  1364. })( jQuery, window, document );