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.

searchprovider.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. /*
  2. * @copyright Copyright (c) 2014 Jörn Friedrich Dreyer <jfd@owncloud.com>
  3. *
  4. * @author Jörn Friedrich Dreyer <jfd@owncloud.com>
  5. * @author John Molakvoæ <skjnldsv@protonmail.com>
  6. *
  7. * @license GNU AGPL version 3 or any later version
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as
  11. * published by the Free Software Foundation, either version 3 of the
  12. * License, or (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. */
  23. (function() {
  24. /**
  25. * @class OCA.Search.Core
  26. * @classdesc
  27. *
  28. * The Search class manages a search queries and their results
  29. *
  30. * @param $searchBox container element with existing markup for the #searchbox form
  31. * @param $searchResults container element for results und status message
  32. */
  33. var Search = function($searchBox, $searchResults) {
  34. this.initialize($searchBox, $searchResults);
  35. };
  36. /**
  37. * @memberof OC
  38. */
  39. Search.prototype = {
  40. /**
  41. * Initialize the search box
  42. *
  43. * @param $searchBox container element with existing markup for the #searchbox form
  44. * @param $searchResults container element for results und status message
  45. * @private
  46. */
  47. initialize: function($searchBox, $searchResults) {
  48. var self = this;
  49. /**
  50. * contains closures that are called to filter the current content
  51. */
  52. var filters = {};
  53. this.setFilter = function(type, filter) {
  54. filters[type] = filter;
  55. };
  56. this.hasFilter = function(type) {
  57. return typeof filters[type] !== 'undefined';
  58. };
  59. this.getFilter = function(type) {
  60. return filters[type];
  61. };
  62. /**
  63. * contains closures that are called to render search results
  64. */
  65. var renderers = {};
  66. this.setRenderer = function(type, renderer) {
  67. renderers[type] = renderer;
  68. };
  69. this.hasRenderer = function(type) {
  70. return typeof renderers[type] !== 'undefined';
  71. };
  72. this.getRenderer = function(type) {
  73. return renderers[type];
  74. };
  75. /**
  76. * contains closures that are called when a search result has been clicked
  77. */
  78. var handlers = {};
  79. this.setHandler = function(type, handler) {
  80. handlers[type] = handler;
  81. };
  82. this.hasHandler = function(type) {
  83. return typeof handlers[type] !== 'undefined';
  84. };
  85. this.getHandler = function(type) {
  86. return handlers[type];
  87. };
  88. var currentResult = -1;
  89. var lastQuery = '';
  90. var lastInApps = [];
  91. var lastPage = 0;
  92. var lastSize = 30;
  93. var lastResults = [];
  94. var timeoutID = null;
  95. this.getLastQuery = function() {
  96. return lastQuery;
  97. };
  98. /**
  99. * Do a search query and display the results
  100. * @param {string} query the search query
  101. * @param inApps
  102. * @param page
  103. * @param size
  104. */
  105. this.search = function(query, inApps, page, size) {
  106. if (query) {
  107. if (typeof page !== 'number') {
  108. page = 1;
  109. }
  110. if (typeof size !== 'number') {
  111. size = 30;
  112. }
  113. if (typeof inApps !== 'object') {
  114. var currentApp = getCurrentApp();
  115. if (currentApp) {
  116. inApps = [currentApp];
  117. } else {
  118. inApps = [];
  119. }
  120. }
  121. // prevent double pages
  122. if (
  123. $searchResults &&
  124. query === lastQuery &&
  125. page === lastPage &&
  126. size === lastSize
  127. ) {
  128. return;
  129. }
  130. window.clearTimeout(timeoutID);
  131. timeoutID = window.setTimeout(function() {
  132. lastQuery = query;
  133. lastInApps = inApps;
  134. lastPage = page;
  135. lastSize = size;
  136. //show spinner
  137. $searchResults.removeClass('hidden');
  138. $status.addClass('status emptycontent');
  139. $status.html('<div class="icon-loading"></div><h2>' + t('core', 'Searching other places') + '</h2>');
  140. // do the actual search query
  141. $.getJSON(
  142. OC.generateUrl('core/search'),
  143. {
  144. query: query,
  145. inApps: inApps,
  146. page: page,
  147. size: size
  148. },
  149. function(results) {
  150. lastResults = results;
  151. if (page === 1) {
  152. showResults(results);
  153. } else {
  154. addResults(results);
  155. }
  156. }
  157. );
  158. }, 500);
  159. }
  160. };
  161. //TODO should be a core method, see https://github.com/owncloud/core/issues/12557
  162. function getCurrentApp() {
  163. var content = document.getElementById('content');
  164. if (content) {
  165. var classList = document.getElementById('content').className.split(/\s+/);
  166. for (var i = 0; i < classList.length; i++) {
  167. if (classList[i].indexOf('app-') === 0) {
  168. return classList[i].substr(4);
  169. }
  170. }
  171. }
  172. return false;
  173. }
  174. var $status = $searchResults.find('#status');
  175. // summaryAndStatusHeight is a constant
  176. var summaryAndStatusHeight = 118;
  177. function isStatusOffScreen() {
  178. return (
  179. $searchResults.position() &&
  180. $searchResults.position().top + summaryAndStatusHeight >
  181. window.innerHeight
  182. );
  183. }
  184. function placeStatus() {
  185. if (isStatusOffScreen()) {
  186. $status.addClass('fixed');
  187. } else {
  188. $status.removeClass('fixed');
  189. }
  190. }
  191. function showResults(results) {
  192. lastResults = results;
  193. $searchResults.find('tr.result').remove();
  194. $searchResults.removeClass('hidden');
  195. addResults(results);
  196. }
  197. function addResults(results) {
  198. var $template = $searchResults.find('tr.template');
  199. jQuery.each(results, function(i, result) {
  200. var $row = $template.clone();
  201. $row.removeClass('template');
  202. $row.addClass('result');
  203. $row.data('result', result);
  204. // generic results only have four attributes
  205. $row.find('td.info div.name').text(result.name);
  206. $row.find('td.info a').attr('href', result.link);
  207. /**
  208. * Give plugins the ability to customize the search results. see result.js for examples
  209. */
  210. if (self.hasRenderer(result.type)) {
  211. $row = self.getRenderer(result.type)($row, result);
  212. } else {
  213. // for backward compatibility add text div
  214. $row.find('td.info div.name').addClass('result');
  215. $row.find('td.result div.name').after('<div class="text"></div>');
  216. $row.find('td.result div.text').text(result.name);
  217. if (OC.search.customResults && OC.search.customResults[result.type]) {
  218. OC.search.customResults[result.type]($row, result);
  219. }
  220. }
  221. if ($row) {
  222. $searchResults.find('tbody').append($row);
  223. }
  224. });
  225. var count = $searchResults.find('tr.result').length;
  226. $status.data('count', count);
  227. if (count === 0) {
  228. $status.addClass('emptycontent').removeClass('status');
  229. $status.html('');
  230. $status.append($('<div>').addClass('icon-search'));
  231. var error = t('core', 'No search results in other folders for {tag}{filter}{endtag}', { filter: lastQuery });
  232. $status.append($('<h2>').html(error.replace('{tag}', '<strong>').replace('{endtag}', '</strong>')));
  233. } else {
  234. $status.removeClass('emptycontent').addClass('status summary');
  235. $status.text(n('core','{count} search result in another folder','{count} search results in other folders', count,{ count: count }));
  236. $status.html('<span class="info">' + n( 'core', '{count} search result in another folder', '{count} search results in other folders', count, { count: count } ) + '</span>');
  237. }
  238. }
  239. function renderCurrent() {
  240. var result = $searchResults.find('tr.result')[currentResult];
  241. if (result) {
  242. var $result = $(result);
  243. var currentOffset = $('#app-content').scrollTop();
  244. $('#app-content').animate(
  245. {
  246. // Scrolling to the top of the new result
  247. scrollTop:
  248. currentOffset +
  249. $result.offset().top -
  250. $result.height() * 2
  251. },
  252. {
  253. duration: 100
  254. }
  255. );
  256. $searchResults.find('tr.result.current').removeClass('current');
  257. $result.addClass('current');
  258. }
  259. }
  260. this.hideResults = function() {
  261. $searchResults.addClass('hidden');
  262. $searchResults.find('tr.result').remove();
  263. lastQuery = false;
  264. };
  265. this.clear = function() {
  266. self.hideResults();
  267. if (self.hasFilter(getCurrentApp())) {
  268. self.getFilter(getCurrentApp())('');
  269. }
  270. $searchBox.val('');
  271. $searchBox.blur();
  272. };
  273. /**
  274. * Event handler for when scrolling the list container.
  275. * This appends/renders the next page of entries when reaching the bottom.
  276. */
  277. function onScroll() {
  278. if (
  279. $searchResults &&
  280. lastQuery !== false &&
  281. lastResults.length > 0
  282. ) {
  283. var resultsBottom = $searchResults.offset().top + $searchResults.height();
  284. var containerBottom = $searchResults.offsetParent().offset().top + $searchResults.offsetParent().height();
  285. if (resultsBottom < containerBottom * 1.2) {
  286. self.search(lastQuery, lastInApps, lastPage + 1);
  287. }
  288. placeStatus();
  289. }
  290. }
  291. $('#app-content').on('scroll', _.bind(onScroll, this));
  292. /**
  293. * scrolls the search results to the top
  294. */
  295. function scrollToResults() {
  296. setTimeout(function() {
  297. if (isStatusOffScreen()) {
  298. var newScrollTop = $('#app-content').prop('scrollHeight') - $searchResults.height();
  299. console.log('scrolling to ' + newScrollTop);
  300. $('#app-content').animate(
  301. {
  302. scrollTop: newScrollTop
  303. },
  304. {
  305. duration: 100,
  306. complete: function() {
  307. scrollToResults();
  308. }
  309. }
  310. );
  311. }
  312. }, 150);
  313. }
  314. $searchBox.keyup(function(event) {
  315. if (event.keyCode === 13) {
  316. //enter
  317. if (currentResult > -1) {
  318. var result = $searchResults.find('tr.result a')[currentResult];
  319. window.location = $(result).attr('href');
  320. }
  321. } else if (event.keyCode === 38) {
  322. //up
  323. if (currentResult > 0) {
  324. currentResult--;
  325. renderCurrent();
  326. }
  327. } else if (event.keyCode === 40) {
  328. //down
  329. if (lastResults.length > currentResult + 1) {
  330. currentResult++;
  331. renderCurrent();
  332. }
  333. }
  334. });
  335. $searchResults.on('click', 'tr.result', function(event) {
  336. var $row = $(this);
  337. var item = $row.data('result');
  338. if (self.hasHandler(item.type)) {
  339. var result = self.getHandler(item.type)($row, item, event);
  340. $searchBox.val('');
  341. if (self.hasFilter(getCurrentApp())) {
  342. self.getFilter(getCurrentApp())('');
  343. }
  344. self.hideResults();
  345. return result;
  346. }
  347. });
  348. $searchResults.on('click', '#status', function(event) {
  349. event.preventDefault();
  350. scrollToResults();
  351. return false;
  352. });
  353. placeStatus();
  354. OC.Plugins.attach('OCA.Search.Core', this);
  355. // Finally use default Search registration
  356. return new OCA.Search(
  357. // Search handler
  358. function(query) {
  359. if (lastQuery !== query) {
  360. currentResult = -1;
  361. if (query.length > 2) {
  362. self.search(query);
  363. } else {
  364. self.hideResults();
  365. }
  366. if (self.hasFilter(getCurrentApp())) {
  367. self.getFilter(getCurrentApp())(query);
  368. }
  369. }
  370. },
  371. // Reset handler
  372. function() {
  373. if ($searchBox.val() === '') {
  374. if (self.hasFilter(getCurrentApp())) {
  375. self.getFilter(getCurrentApp())('');
  376. }
  377. self.hideResults();
  378. }
  379. }
  380. );
  381. }
  382. };
  383. OCA.Search.Core = Search;
  384. })();
  385. $(document).ready(function() {
  386. var $searchResults = $('#searchresults');
  387. var $searchBox = $('#searchbox');
  388. if ($searchResults.length > 0 && $searchBox.length > 0) {
  389. $searchResults.addClass('hidden');
  390. $searchResults.load(
  391. OC.webroot + '/core/search/templates/part.results.html',
  392. function() {
  393. OC.Search = new OCA.Search.Core(
  394. $searchBox,
  395. $searchResults
  396. );
  397. }
  398. );
  399. } else {
  400. // check again later
  401. _.defer(function() {
  402. if ($searchResults.length > 0 && $searchBox.length > 0) {
  403. OC.Search = new OCA.Search.Core(
  404. $searchBox,
  405. $searchResults
  406. );
  407. }
  408. });
  409. }
  410. });
  411. /**
  412. * @deprecated use get/setRenderer() instead
  413. */
  414. OC.search.customResults = {};
  415. /**
  416. * @deprecated use get/setRenderer() instead
  417. */
  418. OC.search.resultTypes = {};