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.

1320 lines
42 KiB

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