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.

1357 lines
43 KiB

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