選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

1506 行
49KB

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