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 90 this.getLastQuery = function() { 91 return lastQuery; 92 }; 93 94 /** 95 * Do a search query and display the results 96 * @param {string} query the search query 97 * @param inApps 98 * @param page 99 * @param size 100 */ 101 this.search = function(query, inApps, page, size) { 102 if (query) { 103 OC.addStyle('core/search','results'); 104 if (typeof page !== 'number') { 105 page = 1; 106 } 107 if (typeof size !== 'number') { 108 size = 30; 109 } 110 if (typeof inApps !== 'object') { 111 var currentApp = getCurrentApp(); 112 if(currentApp) { 113 inApps = [currentApp]; 114 } else { 115 inApps = []; 116 } 117 } 118 // prevent double pages 119 if ($searchResults && query === lastQuery && page === lastPage && size === lastSize) { 120 return; 121 } 122 window.clearTimeout(timeoutID); 123 timeoutID = window.setTimeout(function() { 124 lastQuery = query; 125 lastInApps = inApps; 126 lastPage = page; 127 lastSize = size; 128 129 //show spinner 130 $searchResults.removeClass('hidden'); 131 $status.addClass('status'); 132 $status.html(t('core', 'Searching other places')+'<img class="spinner" alt="search in progress" src="'+OC.webroot+'/core/img/loading.gif" />'); 133 134 // do the actual search query 135 $.getJSON(OC.generateUrl('core/search'), {query:query, inApps:inApps, page:page, size:size }, function(results) { 136 lastResults = results; 137 if (page === 1) { 138 showResults(results); 139 } else { 140 addResults(results); 141 } 142 }); 143 }, 500); 144 } 145 }; 146 147 //TODO should be a core method, see https://github.com/owncloud/core/issues/12557 148 function getCurrentApp() { 149 var content = document.getElementById('content'); 150 if (content) { 151 var classList = document.getElementById('content').className.split(/\s+/); 152 for (var i = 0; i < classList.length; i++) { 153 if (classList[i].indexOf('app-') === 0) { 154 return classList[i].substr(4); 155 } 156 } 157 } 158 return false;
159 } 160 161 var $status = $searchResults.find('#status'); 162 // summaryAndStatusHeight is a constant 163 var summaryAndStatusHeight = 118; 164 165 function isStatusOffScreen() { 166 return $searchResults.position() && 167 ($searchResults.position().top + summaryAndStatusHeight > window.innerHeight); 168 } 169 170 function placeStatus() { 171 if (isStatusOffScreen()) { 172 $status.addClass('fixed'); 173 } else { 174 $status.removeClass('fixed'); 175 } 176 } 177 function showResults(results) { 178 lastResults = results; 179 $searchResults.find('tr.result').remove(); 180 $searchResults.removeClass('hidden'); 181 addResults(results); 182 } 183 function addResults(results) { 184 var $template = $searchResults.find('tr.template'); 185 jQuery.each(results, function (i, result) { 186 var $row = $template.clone(); 187 $row.removeClass('template'); 188 $row.addClass('result'); 189 190 $row.data('result', result); 191 192 // generic results only have four attributes 193 $row.find('td.info div.name').text(result.name); 194 $row.find('td.info a').attr('href', result.link); 195 196 /** 197 * Give plugins the ability to customize the search results. see result.js for examples 198 */ 199 if (self.hasRenderer(result.type)) { 200 $row = self.getRenderer(result.type)($row, result); 201 } else { 202 // for backward compatibility add text div 203 $row.find('td.info div.name').addClass('result'); 204 $row.find('td.result div.name').after('<div class="text"></div>'); 205 $row.find('td.result div.text').text(result.name); 206 if (OC.search.customResults && OC.search.customResults[result.type]) { 207 OC.search.customResults[result.type]($row, result); 208 } 209 } 210 if ($row) { 211 $searchResults.find('tbody').append($row); 212 } 213 }); 214 var count = $searchResults.find('tr.result').length; 215 $status.data('count', count); 216 if (count === 0) { 217 $status.addClass('emptycontent').removeClass('status'); 218 $status.html(''); 219 $status.append('<div class="icon-search"></div>'); 220 $status.append('<h2>' + t('core', 'No search results in other folders') + '</h2>'); 221 } else { 222 $status.removeClass('emptycontent').addClass('status'); 223 $status.text(n('core', '{count} search result in another folder', '{count} search results in other folders', count, {count:count})); 224 } 225 } 226 function renderCurrent() { 227 var result = $searchResults.find('tr.result')[currentResult]; 228 if (result) { 229 var $result = $(result); 230 var currentOffset = $('#app-content').scrollTop(); 231 $('#app-content').animate({ 232 // Scrolling to the top of the new result 233 scrollTop: currentOffset + $result.offset().top - $result.height() * 2 234 }, { 235 duration: 100 236 }); 237 $searchResults.find('tr.result.current').removeClass('current'); 238 $result.addClass('current'); 239 } 240 } 241 this.hideResults = function() { 242 $searchResults.addClass('hidden'); 243 $searchResults.find('tr.result').remove(); 244 lastQuery = false; 245 }; 246 this.clear = function() { 247 self.hideResults(); 248 if(self.hasFilter(getCurrentApp())) { 249 self.getFilter(getCurrentApp())(''); 250 } 251 $searchBox.val(''); 252 $searchBox.blur(); 253 }; 254 255 /** 256 * Event handler for when scrolling the list container. 257 * This appends/renders the next page of entries when reaching the bottom. 258 */ 259 function onScroll() { 260 if ($searchResults && lastQuery !== false && lastResults.length > 0) { 261 var resultsBottom = $searchResults.offset().top + $searchResults.height(); 262 var containerBottom = $searchResults.offsetParent().offset().top + 263 $searchResults.offsetParent().height(); 264 if ( resultsBottom < containerBottom * 1.2 ) { 265 self.search(lastQuery, lastInApps, lastPage + 1); 266 } 267 placeStatus(); 268 } 269 } 270 271 $('#app-content').on('scroll', _.bind(onScroll, this)); 272 273 /** 274 * scrolls the search results to the top 275 */ 276 function scrollToResults() { 277 setTimeout(function() { 278 if (isStatusOffScreen()) { 279 var newScrollTop = $('#app-content').prop('scrollHeight') - $searchResults.height(); 280 console.log('scrolling to ' + newScrollTop); 281 $('#app-content').animate({ 282 scrollTop: newScrollTop 283 }, {
284 duration: 100, 285 complete: function () { 286 scrollToResults(); 287 } 288 }); 289 } 290 }, 150); 291 } 292 293 $('form.searchbox').submit(function(event) { 294 event.preventDefault(); 295 }); 296 297 $searchBox.on('search', function () { 298 if($searchBox.val() === '') { 299 if(self.hasFilter(getCurrentApp())) { 300 self.getFilter(getCurrentApp())(''); 301 } 302 self.hideResults(); 303 } 304 }); 305 $searchBox.keyup(function(event) { 306 if (event.keyCode === 13) { //enter 307 if(currentResult > -1) { 308 var result = $searchResults.find('tr.result a')[currentResult]; 309 window.location = $(result).attr('href'); 310 } 311 } else if(event.keyCode === 38) { //up 312 if(currentResult > 0) { 313 currentResult--; 314 renderCurrent(); 315 } 316 } else if(event.keyCode === 40) { //down 317 if(lastResults.length > currentResult + 1){ 318 currentResult++; 319 renderCurrent(); 320 } 321 } else { 322 var query = $searchBox.val(); 323 if (lastQuery !== query) { 324 currentResult = -1; 325 if (query.length > 2) { 326 self.search(query); 327 } else { 328 self.hideResults(); 329 } 330 if(self.hasFilter(getCurrentApp())) { 331 self.getFilter(getCurrentApp())(query); 332 } 333 } 334 } 335 }); 336 $(document).keyup(function(event) { 337 if(event.keyCode === 27) { //esc 338 $searchBox.val(''); 339 if(self.hasFilter(getCurrentApp())) { 340 self.getFilter(getCurrentApp())(''); 341 } 342 self.hideResults(); 343 } 344 }); 345 346 $(document).keydown(function(event) { 347 if ((event.ctrlKey || event.metaKey) && // Ctrl or Command (OSX) 348 !event.shiftKey && 349 event.keyCode === 70 && // F 350 self.hasFilter(getCurrentApp()) && // Search is enabled 351 !$searchBox.is(':focus') // if searchbox is already focused do nothing (fallback to browser default) 352 ) { 353 $searchBox.focus(); 354 event.preventDefault(); 355 } 356 }); 357 358 $searchResults.on('click', 'tr.result', function (event) { 359 var $row = $(this); 360 var item = $row.data('result'); 361 if(self.hasHandler(item.type)){ 362 var result = self.getHandler(item.type)($row, item, event); 363 $searchBox.val(''); 364 if(self.hasFilter(getCurrentApp())) { 365 self.getFilter(getCurrentApp())(''); 366 } 367 self.hideResults(); 368 return result; 369 } 370 }); 371 $searchResults.on('click', '#status', function (event) { 372 event.preventDefault(); 373 scrollToResults(); 374 return false; 375 }); 376 placeStatus(); 377 378 OC.Plugins.attach('OCA.Search', this); 379 380 // hide search file if search is not enabled 381 if(self.hasFilter(getCurrentApp())) { 382 return; 383 } 384 if ($searchResults.length === 0) { 385 $searchBox.hide(); 386 } 387 } 388 }; 389 OCA.Search = Search; 390})(); 391 392$(document).ready(function() { 393 var $searchResults = $('#searchresults'); 394 if ($searchResults.length > 0) { 395 $searchResults.addClass('hidden'); 396 $('#app-content') 397 .find('.viewcontainer').css('min-height', 'initial'); 398 $searchResults.load(OC.webroot + '/core/search/templates/part.results.html', function () { 399 OC.Search = new OCA.Search($('#searchbox'), $('#searchresults')); 400 }); 401 } else { 402 _.defer(function() { 403 OC.Search = new OCA.Search($('#searchbox'), $('#searchresults')); 404 }); 405 } 406}); 407 408/** 409 * @deprecated use get/setRenderer() instead 410 */ 411OC.search.customResults = {}; 412/** 413 * @deprecated use get/setRenderer() instead 414 */ 415OC.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.