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