/* * @copyright Copyright (c) 2014 Jörn Friedrich Dreyer * * @author Jörn Friedrich Dreyer * @author John Molakvoæ * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ (function() { /** * @class OCA.Search.Core * @classdesc * * The Search class manages a search queries and their results * * @param $searchBox container element with existing markup for the #searchbox form * @param $searchResults container element for results und status message */ var Search = function($searchBox, $searchResults) { this.initialize($searchBox, $searchResults); }; /** * @memberof OC */ Search.prototype = { /** * Initialize the search box * * @param $searchBox container element with existing markup for the #searchbox form * @param $searchResults container element for results und status message * @private */ initialize: function($searchBox, $searchResults) { var self = this; /** * contains closures that are called to filter the current content */ var filters = {}; this.setFilter = function(type, filter) { filters[type] = filter; }; this.hasFilter = function(type) { return typeof filters[type] !== 'undefined'; }; this.getFilter = function(type) { return filters[type]; }; /** * contains closures that are called to render search results */ var renderers = {}; this.setRenderer = function(type, renderer) { renderers[type] = renderer; }; this.hasRenderer = function(type) { return typeof renderers[type] !== 'undefined'; }; this.getRenderer = function(type) { return renderers[type]; }; /** * contains closures that are called when a search result has been clicked */ var handlers = {}; this.setHandler = function(type, handler) { handlers[type] = handler; }; this.hasHandler = function(type) { return typeof handlers[type] !== 'undefined'; }; this.getHandler = function(type) { return handlers[type]; }; var currentResult = -1; var lastQuery = ''; var lastInApps = []; var lastPage = 0; var lastSize = 30; var lastResults = []; var timeoutID = null; this.getLastQuery = function() { return lastQuery; }; /** * Do a search query and display the results * @param {string} query the search query * @param inApps * @param page * @param size */ this.search = function(query, inApps, page, size) { if (query) { if (typeof page !== 'number') { page = 1; } if (typeof size !== 'number') { size = 30; } if (typeof inApps !== 'object') { var currentApp = getCurrentApp(); if (currentApp) { inApps = [currentApp]; } else { inApps = []; } } // prevent double pages if ( $searchResults && query === lastQuery && page === lastPage && size === lastSize ) { return; } window.clearTimeout(timeoutID); timeoutID = window.setTimeout(function() { lastQuery = query; lastInApps = inApps; lastPage = page; lastSize = size; //show spinner $searchResults.removeClass('hidden'); $status.addClass('status emptycontent'); $status.html('

' + t('core', 'Searching other places') + '

'); // do the actual search query $.getJSON( OC.generateUrl('core/search'), { query: query, inApps: inApps, page: page, size: size }, function(results) { lastResults = results; if (page === 1) { showResults(results); } else { addResults(results); } } ); }, 500); } }; //TODO should be a core method, see https://github.com/owncloud/core/issues/12557 function getCurrentApp() { var content = document.getElementById('content'); if (content) { var classList = document.getElementById('content').className.split(/\s+/); for (var i = 0; i < classList.length; i++) { if (classList[i].indexOf('app-') === 0) { return classList[i].substr(4); } } } return false; } var $status = $searchResults.find('#status'); // summaryAndStatusHeight is a constant var summaryAndStatusHeight = 118; function isStatusOffScreen() { return ( $searchResults.position() && $searchResults.position().top + summaryAndStatusHeight > window.innerHeight ); } function placeStatus() { if (isStatusOffScreen()) { $status.addClass('fixed'); } else { $status.removeClass('fixed'); } } function showResults(results) { lastResults = results; $searchResults.find('tr.result').remove(); $searchResults.removeClass('hidden'); addResults(results); } function addResults(results) { var $template = $searchResults.find('tr.template'); jQuery.each(results, function(i, result) { var $row = $template.clone(); $row.removeClass('template'); $row.addClass('result'); $row.data('result', result); // generic results only have four attributes $row.find('td.info div.name').text(result.name); $row.find('td.info a').attr('href', result.link); /** * Give plugins the ability to customize the search results. see result.js for examples */ if (self.hasRenderer(result.type)) { $row = self.getRenderer(result.type)($row, result); } else { // for backward compatibility add text div $row.find('td.info div.name').addClass('result'); $row.find('td.result div.name').after('
'); $row.find('td.result div.text').text(result.name); if (OC.search.customResults && OC.search.customResults[result.type]) { OC.search.customResults[result.type]($row, result); } } if ($row) { $searchResults.find('tbody').append($row); } }); var count = $searchResults.find('tr.result').length; $status.data('count', count); if (count === 0) { $status.addClass('emptycontent').removeClass('status'); $status.html(''); $status.append($('
').addClass('icon-search')); var error = t('core', 'No search results in other folders for {tag}{filter}{endtag}', { filter: lastQuery }); $status.append($('

').html(error.replace('{tag}', '').replace('{endtag}', ''))); } else { $status.removeClass('emptycontent').addClass('status summary'); $status.text(n('core','{count} search result in another folder','{count} search results in other folders', count,{ count: count })); $status.html('' + n( 'core', '{count} search result in another folder', '{count} search results in other folders', count, { count: count } ) + ''); } } function renderCurrent() { var result = $searchResults.find('tr.result')[currentResult]; if (result) { var $result = $(result); var currentOffset = $('#app-content').scrollTop(); $('#app-content').animate( { // Scrolling to the top of the new result scrollTop: currentOffset + $result.offset().top - $result.height() * 2 }, { duration: 100 } ); $searchResults.find('tr.result.current').removeClass('current'); $result.addClass('current'); } } this.hideResults = function() { $searchResults.addClass('hidden'); $searchResults.find('tr.result').remove(); lastQuery = false; }; this.clear = function() { self.hideResults(); if (self.hasFilter(getCurrentApp())) { self.getFilter(getCurrentApp())(''); } $searchBox.val(''); $searchBox.blur(); }; /** * Event handler for when scrolling the list container. * This appends/renders the next page of entries when reaching the bottom. */ function onScroll() { if ( $searchResults && lastQuery !== false && lastResults.length > 0 ) { var resultsBottom = $searchResults.offset().top + $searchResults.height(); var containerBottom = $searchResults.offsetParent().offset().top + $searchResults.offsetParent().height(); if (resultsBottom < containerBottom * 1.2) { self.search(lastQuery, lastInApps, lastPage + 1); } placeStatus(); } } $('#app-content').on('scroll', _.bind(onScroll, this)); /** * scrolls the search results to the top */ function scrollToResults() { setTimeout(function() { if (isStatusOffScreen()) { var newScrollTop = $('#app-content').prop('scrollHeight') - $searchResults.height(); console.log('scrolling to ' + newScrollTop); $('#app-content').animate( { scrollTop: newScrollTop }, { duration: 100, complete: function() { scrollToResults(); } } ); } }, 150); } $searchBox.keyup(function(event) { if (event.keyCode === 13) { //enter if (currentResult > -1) { var result = $searchResults.find('tr.result a')[currentResult]; window.location = $(result).attr('href'); } } else if (event.keyCode === 38) { //up if (currentResult > 0) { currentResult--; renderCurrent(); } } else if (event.keyCode === 40) { //down if (lastResults.length > currentResult + 1) { currentResult++; renderCurrent(); } } }); $searchResults.on('click', 'tr.result', function(event) { var $row = $(this); var item = $row.data('result'); if (self.hasHandler(item.type)) { var result = self.getHandler(item.type)($row, item, event); $searchBox.val(''); if (self.hasFilter(getCurrentApp())) { self.getFilter(getCurrentApp())(''); } self.hideResults(); return result; } }); $searchResults.on('click', '#status', function(event) { event.preventDefault(); scrollToResults(); return false; }); placeStatus(); OC.Plugins.attach('OCA.Search.Core', this); // Finally use default Search registration return new OCA.Search( // Search handler function(query) { if (lastQuery !== query) { currentResult = -1; if (query.length > 2) { self.search(query); } else { self.hideResults(); } if (self.hasFilter(getCurrentApp())) { self.getFilter(getCurrentApp())(query); } } }, // Reset handler function() { if ($searchBox.val() === '') { if (self.hasFilter(getCurrentApp())) { self.getFilter(getCurrentApp())(''); } self.hideResults(); } } ); } }; OCA.Search.Core = Search; })(); $(document).ready(function() { var $searchResults = $('#searchresults'); var $searchBox = $('#searchbox'); if ($searchResults.length > 0 && $searchBox.length > 0) { $searchResults.addClass('hidden'); $searchResults.load( OC.webroot + '/core/search/templates/part.results.html', function() { OC.Search = new OCA.Search.Core( $searchBox, $searchResults ); } ); } else { // check again later _.defer(function() { if ($searchResults.length > 0 && $searchBox.length > 0) { OC.Search = new OCA.Search.Core( $searchBox, $searchResults ); } }); } }); /** * @deprecated use get/setRenderer() instead */ OC.search.customResults = {}; /** * @deprecated use get/setRenderer() instead */ OC.search.resultTypes = {};