Merge pull request #12653 from owncloud/full_content_search_results

Full content width search results
This commit is contained in:
Morris Jobke 2015-01-06 16:44:58 +01:00
commit fe4592937a
26 changed files with 984 additions and 290 deletions

View File

@ -540,7 +540,7 @@ a.action>img {
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)";
filter: alpha(opacity=30);
opacity: .3;
height: 70px;
height: 60px;
}
.summary:hover,
@ -551,8 +551,6 @@ table tr.summary td {
}
.summary td {
padding-top: 20px;
padding-bottom: 150px;
border-bottom: none;
}
.summary .info {
@ -601,3 +599,26 @@ table.dragshadow td.size {
.mask.transparent{
opacity: 0;
}
.nofilterresults {
font-size: 16px;
color: #888;
position: absolute;
text-align: center;
top: 30%;
width: 100%;
}
.nofilterresults h2 {
font-size: 22px;
margin-bottom: 10px;
}
.nofilterresults [class^="icon-"],
.nofilterresults [class*=" icon-"] {
background-size: 64px;
height: 64px;
width: 64px;
margin: 0 auto 15px;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
filter: alpha(opacity=50);
opacity: .5;
}

View File

@ -38,6 +38,7 @@ OCP\Util::addscript('files', 'jquery-visibility');
OCP\Util::addscript('files', 'filesummary');
OCP\Util::addscript('files', 'breadcrumb');
OCP\Util::addscript('files', 'filelist');
OCP\Util::addscript('files', 'search');
\OCP\Util::addScript('files', 'favoritesfilelist');
\OCP\Util::addScript('files', 'tagsplugin');

View File

@ -111,6 +111,12 @@
*/
_selectionSummary: null,
/**
* If not empty, only files containing this string will be shown
* @type String
*/
_filter: '',
/**
* Sort attribute
* @type String
@ -208,6 +214,8 @@
this.$el.on('show', this._onResize);
this.updateSearch();
this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this));
this.$fileList.on('change', 'td.filename>.selectCheckBox', _.bind(this._onClickFileCheckbox, this));
this.$el.on('urlChanged', _.bind(this._onUrlChanged, this));
@ -268,6 +276,8 @@
containerWidth -= $('#app-navigation-toggle').width();
this.breadcrumb.setMaxWidth(containerWidth - actionsWidth - 10);
this.updateSearch();
},
/**
@ -458,6 +468,7 @@
e.preventDefault();
this.changeDirectory($targetDir);
}
this.updateSearch();
},
/**
@ -551,6 +562,7 @@
_nextPage: function(animate) {
var index = this.$fileList.children().length,
count = this.pageSize(),
hidden,
tr,
fileData,
newTrs = [],
@ -562,7 +574,12 @@
while (count > 0 && index < this.files.length) {
fileData = this.files[index];
tr = this._renderRow(fileData, {updateSummary: false, silent: true});
if (this._filter) {
hidden = fileData.name.toLowerCase().indexOf(this._filter.toLowerCase()) === -1;
} else {
hidden = false;
}
tr = this._renderRow(fileData, {updateSummary: false, silent: true, hidden: hidden});
this.$fileList.append(tr);
if (isAllSelected || this._selectedFiles[fileData.id]) {
tr.addClass('selected');
@ -1638,24 +1655,68 @@
});
});
},
/**
* @deprecated use setFilter(filter)
*/
filter:function(query) {
this.setFilter('');
},
/**
* @deprecated use setFilter('')
*/
unfilter:function() {
this.setFilter('');
},
/**
* hide files matching the given filter
* @param filter
*/
setFilter:function(filter) {
this._filter = filter;
this.fileSummary.setFilter(filter, this.files);
this.hideIrrelevantUIWhenNoFilesMatch();
var that = this;
this.$fileList.find('tr').each(function(i,e) {
if ($(e).data('file').toString().toLowerCase().indexOf(query.toLowerCase()) !== -1) {
$(e).addClass("searchresult");
var $e = $(e);
if ($e.data('file').toString().toLowerCase().indexOf(filter.toLowerCase()) === -1) {
$e.addClass('hidden');
that.$container.trigger('scroll');
} else {
$(e).removeClass("searchresult");
$e.removeClass('hidden');
}
});
//do not use scrollto to prevent removing searchresult css class
var first = this.$fileList.find('tr.searchresult').first();
if (first.exists()) {
$(window).scrollTop(first.position().top);
},
hideIrrelevantUIWhenNoFilesMatch:function() {
if (this._filter && this.fileSummary.summary.totalDirs + this.fileSummary.summary.totalFiles === 0) {
this.$el.find('#filestable thead th').addClass('hidden');
this.$el.find('#emptycontent').addClass('hidden');
if ( $('#searchresults').length === 0 || $('#searchresults').hasClass('hidden')) {
this.$el.find('.nofilterresults').removeClass('hidden').
find('p').text(t('files', "No entries in this folder match '{filter}'", {filter:this._filter}));
}
} else {
this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
this.$el.find('.nofilterresults').addClass('hidden');
}
},
unfilter:function() {
this.$fileList.find('tr.searchresult').each(function(i,e) {
$(e).removeClass("searchresult");
});
/**
* get the current filter
* @param filter
*/
getFilter:function(filter) {
return this._filter;
},
/**
* update the search object to use this filelist when filtering
*/
updateSearch:function() {
if (OCA.Search.files) {
OCA.Search.files.setFileList(this);
}
if (OC.Search) {
OC.Search.clear();
}
},
/**
* Update UI based on the current selection

View File

@ -39,7 +39,8 @@
summary: {
totalFiles: 0,
totalDirs: 0,
totalSize: 0
totalSize: 0,
filter:''
},
/**
@ -48,6 +49,9 @@
* @param update whether to update the display
*/
add: function(file, update) {
if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) {
return;
}
if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
this.summary.totalDirs++;
}
@ -65,6 +69,9 @@
* @param update whether to update the display
*/
remove: function(file, update) {
if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) {
return;
}
if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
this.summary.totalDirs--;
}
@ -76,6 +83,10 @@
this.update();
}
},
setFilter: function(filter, files){
this.summary.filter = filter.toLowerCase();
this.calculate(files);
},
/**
* Returns the total of files and directories
*/
@ -91,11 +102,15 @@
var summary = {
totalDirs: 0,
totalFiles: 0,
totalSize: 0
totalSize: 0,
filter: this.summary.filter
};
for (var i = 0; i < files.length; i++) {
file = files[i];
if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) {
continue;
}
if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
summary.totalDirs++;
}
@ -118,6 +133,9 @@
*/
setSummary: function(summary) {
this.summary = summary;
if (typeof this.summary.filter === 'undefined') {
this.summary.filter = '';
}
this.update();
},
@ -137,6 +155,7 @@
var $dirInfo = this.$el.find('.dirinfo');
var $fileInfo = this.$el.find('.fileinfo');
var $connector = this.$el.find('.connector');
var $filterInfo = this.$el.find('.filter');
// Substitute old content with new translations
$dirInfo.html(n('files', '%n folder', '%n folders', this.summary.totalDirs));
@ -159,6 +178,13 @@
if (this.summary.totalDirs > 0 && this.summary.totalFiles > 0) {
$connector.removeClass('hidden');
}
if (this.summary.filter === '') {
$filterInfo.html('');
$filterInfo.addClass('hidden');
} else {
$filterInfo.html(n('files', ' matches \'{filter}\'', ' match \'{filter}\'', this.summary.totalDirs + this.summary.totalFiles, {filter: this.summary.filter}));
$filterInfo.removeClass('hidden');
}
},
render: function() {
if (!this.$el) {
@ -168,6 +194,11 @@
var summary = this.summary;
var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs);
var fileInfo = n('files', '%n file', '%n files', summary.totalFiles);
if (this.summary.filter === '') {
var filterInfo = '';
} else {
var filterInfo = n('files', ' matches \'{filter}\'', ' match \'{filter}\'', summary.totalFiles + summary.totalDirs, {filter: summary.filter});
}
var infoVars = {
dirs: '<span class="dirinfo">'+directoryInfo+'</span><span class="connector">',
@ -182,7 +213,7 @@
var info = t('files', '{dirs} and {files}', infoVars);
var $summary = $('<td><span class="info">'+info+'</span></td>'+fileSize+'<td class="date"></td>');
var $summary = $('<td><span class="info">'+info+'<span class="filter">'+filterInfo+'</span></span></td>'+fileSize+'<td class="date"></td>');
if (!this.summary.totalFiles && !this.summary.totalDirs) {
this.$el.addClass('hidden');

191
apps/files/js/search.js Normal file
View File

@ -0,0 +1,191 @@
/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
/**
* Construct a new FileActions instance
* @constructs Files
*/
var Files = function() {
this.initialize();
};
/**
* @memberof OCA.Search
*/
Files.prototype = {
fileList: null,
/**
* Initialize the file search
*/
initialize: function() {
var self = this;
this.fileAppLoaded = function() {
return !!OCA.Files && !!OCA.Files.App;
};
function inFileList($row, result) {
if (! self.fileAppLoaded()) {
return false;
}
var dir = self.fileList.getCurrentDirectory().replace(/\/+$/,'');
var resultDir = OC.dirname(result.path);
return dir === resultDir && self.fileList.inList(result.name);
}
function updateLegacyMimetype(result) {
// backward compatibility:
if (!result.mime && result.mime_type) {
result.mime = result.mime_type;
}
}
function hideNoFilterResults() {
var $nofilterresults = $('.nofilterresults');
if ( ! $nofilterresults.hasClass('hidden') ) {
$nofilterresults.addClass('hidden');
}
}
this.renderFolderResult = function($row, result) {
if (inFileList($row, result)) {
return null;
}
hideNoFilterResults();
/*render folder icon, show path beneath filename,
show size and last modified date on the right */
this.updateLegacyMimetype(result);
var $pathDiv = $('<div class="path"></div>').text(result.path);
$row.find('td.info div.name').after($pathDiv).text(result.name);
$row.find('td.result a').attr('href', result.link);
$row.find('td.icon').css('background-image', 'url(' + OC.imagePath('core', 'filetypes/folder') + ')');
return $row;
};
this.renderFileResult = function($row, result) {
if (inFileList($row, result)) {
return null;
}
hideNoFilterResults();
/*render preview icon, show path beneath filename,
show size and last modified date on the right */
this.updateLegacyMimetype(result);
var $pathDiv = $('<div class="path"></div>').text(result.path);
$row.find('td.info div.name').after($pathDiv).text(result.name);
$row.find('td.result a').attr('href', result.link);
if (self.fileAppLoaded()) {
self.fileList.lazyLoadPreview({
path: result.path,
mime: result.mime,
callback: function (url) {
$row.find('td.icon').css('background-image', 'url(' + url + ')');
}
});
} else {
// FIXME how to get mime icon if not in files app
var mimeicon = result.mime.replace('/', '-');
$row.find('td.icon').css('background-image', 'url(' + OC.imagePath('core', 'filetypes/' + mimeicon) + ')');
var dir = OC.dirname(result.path);
if (dir === '') {
dir = '/';
}
$row.find('td.info a').attr('href',
OC.generateUrl('/apps/files/?dir={dir}&scrollto={scrollto}', {dir: dir, scrollto: result.name})
);
}
return $row;
};
this.renderAudioResult = function($row, result) {
/*render preview icon, show path beneath filename,
show size and last modified date on the right
show Artist and Album */
$row = this.renderFileResult($row, result);
if ($row) {
$row.find('td.icon').css('background-image', 'url(' + OC.imagePath('core', 'filetypes/audio') + ')');
}
return $row;
};
this.renderImageResult = function($row, result) {
/*render preview icon, show path beneath filename,
show size and last modified date on the right
show width and height */
$row = this.renderFileResult($row, result);
if ($row && !self.fileAppLoaded()) {
$row.find('td.icon').css('background-image', 'url(' + OC.imagePath('core', 'filetypes/image') + ')');
}
return $row;
};
this.handleFolderClick = function($row, result, event) {
// open folder
if (self.fileAppLoaded()) {
self.fileList.changeDirectory(result.path);
return false;
} else {
return true;
}
};
this.handleFileClick = function($row, result, event) {
if (self.fileAppLoaded()) {
self.fileList.changeDirectory(OC.dirname(result.path));
self.fileList.scrollTo(result.name);
return false;
} else {
return true;
}
};
this.updateLegacyMimetype = function (result) {
// backward compatibility:
if (!result.mime && result.mime_type) {
result.mime = result.mime_type;
}
};
this.setFileList = function (fileList) {
this.fileList = fileList;
};
OC.Plugins.register('OCA.Search', this);
},
attach: function(search) {
var self = this;
search.setFilter('files', function (query) {
if (self.fileAppLoaded()) {
self.fileList.setFilter(query);
if (query.length > 2) {
//search is not started until 500msec have passed
window.setTimeout(function() {
$('.nofilterresults').addClass('hidden');
}, 500);
}
}
});
search.setRenderer('folder', this.renderFolderResult.bind(this));
search.setRenderer('file', this.renderFileResult.bind(this));
search.setRenderer('audio', this.renderAudioResult.bind(this));
search.setRenderer('image', this.renderImageResult.bind(this));
search.setHandler('folder', this.handleFolderClick.bind(this));
search.setHandler(['file', 'audio', 'image'], this.handleFileClick.bind(this));
}
};
OCA.Search.Files = Files;
OCA.Search.files = new Files();
})();

View File

@ -60,6 +60,12 @@
<p><?php p($l->t('Upload some content or sync with your devices!')); ?></p>
</div>
<div class="nofilterresults hidden">
<div class="icon-search"></div>
<h2><?php p($l->t('No entries found in this folder')); ?></h2>
<p></p>
</div>
<table id="filestable" data-allow-public-upload="<?php p($_['publicUploadEnabled'])?>" data-preview-x="36" data-preview-y="36">
<thead>
<tr>

View File

@ -11,6 +11,12 @@
<input type="hidden" name="dir" value="" id="dir">
<div class="nofilterresults hidden">
<div class="icon-search"></div>
<h2><?php p($l->t('No entries found in this folder')); ?></h2>
<p></p>
</div>
<table id="filestable">
<thead>
<tr>

View File

@ -85,4 +85,67 @@ describe('OCA.Files.FileSummary tests', function() {
expect(s.summary.totalFiles).toEqual(1);
expect(s.summary.totalSize).toEqual(127900);
});
it('renders filtered summary as text', function() {
var s = new FileSummary($container);
s.setSummary({
totalDirs: 5,
totalFiles: 2,
totalSize: 256000,
filter: 'foo'
});
expect($container.hasClass('hidden')).toEqual(false);
expect($container.find('.info').text()).toEqual('5 folders and 2 files match \'foo\'');
expect($container.find('.filesize').text()).toEqual('250 kB');
});
it('hides filtered summary when no files or folders', function() {
var s = new FileSummary($container);
s.setSummary({
totalDirs: 0,
totalFiles: 0,
totalSize: 0,
filter: 'foo'
});
expect($container.hasClass('hidden')).toEqual(true);
});
it('increases filtered summary when adding files', function() {
var s = new FileSummary($container);
s.setSummary({
totalDirs: 5,
totalFiles: 2,
totalSize: 256000,
filter: 'foo'
});
s.add({name: 'bar.txt', type: 'file', size: 256000});
s.add({name: 'foo.txt', type: 'file', size: 256001});
s.add({name: 'bar', type: 'dir', size: 100});
s.add({name: 'foo', type: 'dir', size: 102});
s.update();
expect($container.hasClass('hidden')).toEqual(false);
expect($container.find('.info').text()).toEqual('6 folders and 3 files match \'foo\'');
expect($container.find('.filesize').text()).toEqual('500 kB');
expect(s.summary.totalDirs).toEqual(6);
expect(s.summary.totalFiles).toEqual(3);
expect(s.summary.totalSize).toEqual(512103);
});
it('decreases filtered summary when removing files', function() {
var s = new FileSummary($container);
s.setSummary({
totalDirs: 5,
totalFiles: 2,
totalSize: 256000,
filter: 'foo'
});
s.remove({name: 'bar.txt', type: 'file', size: 128000});
s.remove({name: 'foo.txt', type: 'file', size: 127999});
s.remove({name: 'bar', type: 'dir', size: 100});
s.remove({name: 'foo', type: 'dir', size: 98});
s.update();
expect($container.hasClass('hidden')).toEqual(false);
expect($container.find('.info').text()).toEqual('4 folders and 1 file match \'foo\'');
expect($container.find('.filesize').text()).toEqual('125 kB');
expect(s.summary.totalDirs).toEqual(4);
expect(s.summary.totalFiles).toEqual(1);
expect(s.summary.totalSize).toEqual(127903);
});
});

View File

@ -8,6 +8,12 @@
<input type="hidden" name="dir" value="" id="dir">
<div class="nofilterresults hidden">
<div class="icon-search"></div>
<h2><?php p($l->t('No entries found in this folder')); ?></h2>
<p></p>
</div>
<table id="filestable">
<thead>
<tr>

View File

@ -12,6 +12,12 @@
<input type="hidden" name="dir" value="" id="dir">
<div class="nofilterresults hidden">
<div class="icon-search"></div>
<h2><?php p($l->t('No entries found in this folder')); ?></h2>
<p></p>
</div>
<table id="filestable">
<thead>
<tr>

View File

@ -40,9 +40,9 @@ try {
$preview->setMaxY($maxY);
$preview->setScalingUp($scalingUp);
$preview->setKeepAspect($keepAspect);
$preview->showPreview();
}
$preview->showPreview();
} catch (\Exception $e) {
\OC_Response::setStatus(500);
\OC_Log::write('core', $e->getmessage(), \OC_Log::DEBUG);

View File

@ -22,6 +22,7 @@
"eventsource.js",
"config.js",
"multiselect.js",
"oc-requesttoken.js"
"oc-requesttoken.js",
"../../search/js/search.js"
]
}

View File

@ -308,22 +308,9 @@ var OC={
* Do a search query and display the results
* @param {string} query the search query
*/
search: _.debounce(function(query){
if(query){
OC.addStyle('search','results');
var classList = document.getElementById('content').className.split(/\s+/);
var inApps = [];
for (var i = 0; i < classList.length; i++) {
if (classList[i].indexOf('app-') === 0) {
var inApps = [classList[i].substr(4)];
}
}
$.getJSON(OC.generateUrl('search/ajax/search.php'), {inApps:inApps, query:query}, function(results){
OC.search.lastResults=results;
OC.search.showResults(results);
});
}
}, 500),
search: function (query) {
OC.Search.search(query, null, 0, 30);
},
/**
* Dialog helper for jquery dialogs.
*
@ -608,10 +595,12 @@ OC.Plugins = {
/**
* @namespace OC.search
*/
OC.search.customResults={};
OC.search.currentResult=-1;
OC.search.lastQuery='';
OC.search.lastResults={};
OC.search.customResults = {};
/**
* @deprecated use get/setFormatter() instead
*/
OC.search.resultTypes = {};
OC.addStyle.loaded=[];
OC.addScript.loaded=[];
@ -1038,48 +1027,6 @@ function initCore() {
}else{
SVGSupport.checkMimeType();
}
$('form.searchbox').submit(function(event){
event.preventDefault();
});
$('#searchbox').keyup(function(event){
if(event.keyCode===13){//enter
if(OC.search.currentResult>-1){
var result=$('#searchresults tr.result a')[OC.search.currentResult];
window.location = $(result).attr('href');
}
}else if(event.keyCode===38){//up
if(OC.search.currentResult>0){
OC.search.currentResult--;
OC.search.renderCurrent();
}
}else if(event.keyCode===40){//down
if(OC.search.lastResults.length>OC.search.currentResult+1){
OC.search.currentResult++;
OC.search.renderCurrent();
}
}else if(event.keyCode===27){//esc
OC.search.hide();
if (FileList && typeof FileList.unfilter === 'function') { //TODO add hook system
FileList.unfilter();
}
}else{
var query=$('#searchbox').val();
if(OC.search.lastQuery!==query){
OC.search.lastQuery=query;
OC.search.currentResult=-1;
if (FileList && typeof FileList.filter === 'function') { //TODO add hook system
FileList.filter(query);
}
if(query.length>2){
OC.search(query);
}else{
if(OC.search.hide){
OC.search.hide();
}
}
}
}
});
// user menu
$('#settings #expand').keydown(function(event) {

View File

@ -362,7 +362,7 @@ class OC {
OC_Util::addScript("eventsource");
OC_Util::addScript("config");
//OC_Util::addScript( "multiselect" );
OC_Util::addScript('search', 'result');
OC_Util::addScript('search', 'search');
OC_Util::addScript("oc-requesttoken");
OC_Util::addScript("apps");
OC_Util::addVendorScript('snapjs/dist/latest/snap');

View File

@ -278,6 +278,7 @@ abstract class Common implements \OC\Files\Storage\Storage {
}
}
}
closedir($dh);
return $files;
}

View File

@ -21,6 +21,7 @@
*/
namespace OC;
use OCP\Search\PagedProvider;
use OCP\Search\Provider;
use OCP\ISearch;
@ -39,12 +40,38 @@ class Search implements ISearch {
* @return array An array of OC\Search\Result's
*/
public function search($query, array $inApps = array()) {
// old apps might assume they get all results, so we set size 0
return $this->searchPaged($query, $inApps, 1, 0);
}
/**
* Search all providers for $query
* @param string $query
* @param string[] $inApps optionally limit results to the given apps
* @param int $page pages start at page 1
* @param int $size, 0 = all
* @return array An array of OC\Search\Result's
*/
public function searchPaged($query, array $inApps = array(), $page = 1, $size = 30) {
$this->initProviders();
$results = array();
foreach($this->providers as $provider) {
/** @var $provider Provider */
if ($provider->providesResultsFor($inApps)) {
$results = array_merge($results, $provider->search($query));
if ( ! $provider->providesResultsFor($inApps) ) {
continue;
}
if ($provider instanceof PagedProvider) {
$results = array_merge($results, $provider->searchPaged($query, $page, $size));
} else if ($provider instanceof Provider) {
$providerResults = $provider->search($query);
if ($size > 0) {
$slicedResults = array_slice($providerResults, ($page - 1) * $size, $size);
$results = array_merge($results, $slicedResults);
} else {
$results = array_merge($results, $providerResults);
}
} else {
\OC::$server->getLogger()->warning('Ignoring Unknown search provider', array('provider' => $provider));
}
}
return $results;

View File

@ -83,7 +83,7 @@ class File extends \OCP\Search\Result {
$this->path = $path;
$this->size = $data->getSize();
$this->modified = $data->getMtime();
$this->mime_type = $data->getMimetype();
$this->mime = $data->getMimetype();
}
/**

View File

@ -34,9 +34,20 @@ interface ISearch {
* @param string $query
* @param string[] $inApps optionally limit results to the given apps
* @return array An array of OCP\Search\Result's
* @deprecated use searchPaged() with page and size
*/
public function search($query, array $inApps = array());
/**
* Search all providers for $query
* @param string $query
* @param string[] $inApps optionally limit results to the given apps
* @param int $page pages start at page 1
* @param int $size
* @return array An array of OCP\Search\Result's
*/
public function searchPaged($query, array $inApps = array(), $page = 1, $size = 30);
/**
* Register a new search provider to search with
* @param string $class class name of a OCP\Search\Provider

View File

@ -0,0 +1,58 @@
<?php
/**
* ownCloud
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCP\Search;
/**
* Provides a template for search functionality throughout ownCloud;
*/
abstract class PagedProvider extends Provider {
/**
* show all results
*/
const SIZE_ALL = 0;
/**
* Constructor
* @param array $options
*/
public function __construct($options) {
$this->options = $options;
}
/**
* Search for $query
* @param string $query
* @return array An array of OCP\Search\Result's
*/
public function search($query) {
// old apps might assume they get all results, so we use SIZE_ALL
$this->searchPaged($query, 1, self::SIZE_ALL);
}
/**
* Search for $query
* @param string $query
* @param int $page pages start at page 1
* @param int $size, 0 = SIZE_ALL
* @return array An array of OCP\Search\Result's
*/
abstract public function searchPaged($query, $page, $size);
}

View File

@ -27,10 +27,10 @@ abstract class Provider {
const OPTION_APPS = 'apps';
/**
* List of options (currently unused)
* List of options
* @var array
*/
private $options;
protected $options;
/**
* Constructor

View File

@ -38,8 +38,18 @@ if (isset($_GET['inApps'])) {
} else {
$inApps = array();
}
if (isset($_GET['page'])) {
$page = (int)$_GET['page'];
} else {
$page = 1;
}
if (isset($_GET['size'])) {
$size = (int)$_GET['size'];
} else {
$size = 30;
}
if($query) {
$result = \OC::$server->getSearch()->search($query, $inApps);
$result = \OC::$server->getSearch()->searchPaged($query, $inApps, $page, $size);
OC_JSON::encodedPrint($result);
}
else {

View File

@ -4,38 +4,42 @@
#searchresults {
background-color:#fff;
border-bottom-left-radius:11px;
box-shadow:0 0 10px #000;
list-style:none;
max-height:80%;
overflow-x:hidden;
overflow-y: auto;
padding-bottom:6px;
position:fixed;
right:0;
text-overflow:ellipsis;
top:45px;
width:380px;
max-width: 95%;
padding-top: 65px;
box-sizing: border-box;
z-index:75;
}
.ie8 #searchresults {
border: 1px solid #666 !important;
#searchresults.hidden {
display: none;
}
#searchresults * {
box-sizing: content-box;
}
#searchresults li.resultHeader {
background-color:#eee;
border-bottom:solid 1px #CCC;
font-size:1.2em;
font-weight:700;
padding:.2em;
#searchresults #status {
background-color: rgba(255, 255, 255, .85);
height: 12px;
padding: 28px 0 28px 56px;
font-size: 18px;
}
.has-favorites:not(.hidden) ~ #searchresults #status {
padding-left: 102px;
}
#searchresults #status.fixed {
position: fixed;
bottom: 0;
width: 100%;
z-index: 10;
}
#searchresults li.result {
margin-left:2em;
#searchresults #status .spinner {
height: 16px;
width: 16px;
vertical-align: middle;
margin-left: 10px;
}
#searchresults table {
border-spacing:0;
table-layout:fixed;
@ -44,48 +48,53 @@
}
#searchresults td {
padding:0 .3em;
height: 44px;
padding: 5px 19px;
font-style: normal;
vertical-align: middle;
border-bottom: none;
}
#searchresults td.icon {
text-align: right;
width: 40px;
height: 40px;
padding: 5px 0;
background-position: right center;
background-repeat: no-repeat;
}
.has-favorites:not(.hidden) ~ #searchresults td.icon {
width: 86px;
}
#searchresults tr.template {
display: none;
}
#searchresults .name,
#searchresults .text {
#searchresults .text,
#searchresults .path {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#searchresults .text {
padding-left: 16px;
color: #999;
white-space: normal;
color: #545454;
}
#searchresults .path {
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
filter: alpha(opacity=50);
opacity: .5;
}
#searchresults .text em {
color: #545454;
font-weight: bold;
opacity: 1;
}
#searchresults td.result * {
#searchresults tr.result * {
cursor:pointer;
}
#searchresults td.container {
width:20px;
}
#searchresults td.container img {
vertical-align: middle;
display:none;
}
#searchresults tr:hover td.container img {
display:inline;
}
#searchresults td.type {
border-bottom:none;
border-right:1px solid #aaa;
font-weight:700;
text-align:right;
width:3.5em;
}
#searchresults tr.current {
background-color:#ddd;
}

View File

@ -1,138 +0,0 @@
/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
//translations for result type ids, can be extended by apps
OC.search.resultTypes={
file: t('core','File'),
folder: t('core','Folder'),
image: t('core','Image'),
audio: t('core','Audio')
};
OC.search.catagorizeResults=function(results){
var types={};
for(var i=0;i<results.length;i++){
var type=results[i].type;
if(!types[type]){
types[type]=[];
}
types[type].push(results[i]);
}
return types;
};
OC.search.hide=function(){
$('#searchresults').hide();
if($('#searchbox').val().length>2){
$('#searchbox').val('');
if (FileList && typeof FileList.unfilter === 'function') { //TODO add hook system
FileList.unfilter();
}
};
if ($('#searchbox').val().length === 0) {
if (FileList && typeof FileList.unfilter === 'function') { //TODO add hook system
FileList.unfilter();
}
}
};
OC.search.showResults=function(results){
if(results.length === 0){
return;
}
if(!OC.search.showResults.loaded){
var parent=$('<div/>');
$('body').append(parent);
parent.load(OC.filePath('search','templates','part.results.php'),function(){
OC.search.showResults.loaded=true;
$('#searchresults').click(function(event){
OC.search.hide();
event.stopPropagation();
});
$(document).click(function(event){
OC.search.hide();
if (FileList && typeof FileList.unfilter === 'function') { //TODO add hook system
FileList.unfilter();
}
});
OC.search.lastResults=results;
OC.search.showResults(results);
});
}else{
var types=OC.search.catagorizeResults(results);
$('#searchresults').show();
$('#searchresults tr.result').remove();
var index=0;
for(var typeid in types){
var type=types[typeid];
if(type.length>0){
for(var i=0;i<type.length;i++){
var row=$('#searchresults tr.template').clone();
row.removeClass('template');
row.addClass('result');
row.data('type', typeid);
row.data('name', type[i].name);
row.data('text', type[i].text);
row.data('index',index);
if (i === 0){
var typeName = OC.search.resultTypes[typeid];
row.children('td.type').text(t('lib', typeName));
}
row.find('td.result div.name').text(type[i].name);
row.find('td.result div.text').text(type[i].text);
if (type[i].path) {
var parent = OC.dirname(type[i].path);
if (parent === '') {
parent = '/';
}
var containerName = OC.basename(parent);
if (containerName === '') {
containerName = '/';
}
var containerLink = OC.linkTo('files', 'index.php')
+'/?dir='+encodeURIComponent(parent)
+'&scrollto='+encodeURIComponent(type[i].name);
row.find('td.result a')
.attr('href', containerLink)
.attr('title', t('core', 'Show in {folder}', {folder: containerName}));
} else {
row.find('td.result a').attr('href', type[i].link);
}
index++;
/**
* Give plugins the ability to customize the search results. For example:
* OC.search.customResults.file = function (row, item){
* if(item.name.search('.json') >= 0) ...
* };
*/
if(OC.search.customResults[typeid]){
OC.search.customResults[typeid](row, type[i]);
}
$('#searchresults tbody').append(row);
}
}
}
$('#searchresults').on('click', 'result', function () {
if ($(this).data('type') === 'Files') {
//FIXME use ajax to navigate to folder & highlight file
}
});
}
};
OC.search.showResults.loaded=false;
OC.search.renderCurrent=function(){
if($('#searchresults tr.result')[OC.search.currentResult]){
var result=$('#searchresults tr.result')[OC.search.currentResult];
$('#searchresults tr.result').removeClass('current');
$(result).addClass('current');
}
};

378
search/js/search.js Normal file
View File

@ -0,0 +1,378 @@
/**
* ownCloud - core
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Jörn Friedrich Dreyer <jfd@owncloud.com>
* @copyright Jörn Friedrich Dreyer 2014
*/
(function () {
/**
* @class OCA.Search
* @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
*/
this.search = function(query, inApps, page, size) {
if (query) {
OC.addStyle('search','results');
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.html(t('core', 'Searching other places')+'<img class="spinner" alt="search in progress" src="'+OC.webroot+'/core/img/loading.gif" />');
// do the actual search query
$.getJSON(OC.generateUrl('search/ajax/search.php'), {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');
const 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('<div class="text"></div>');
$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.text(t('core', 'No search result in other places'));
} else {
$status.text(n('core', '{count} search result in other places', '{count} search results in other places', 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(e) {
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);
}
$('form.searchbox').submit(function(event) {
event.preventDefault();
});
$searchBox.on('search', function (event) {
if($searchBox.val() === '') {
if(self.hasFilter(getCurrentApp())) {
self.getFilter(getCurrentApp())('');
}
self.hideResults();
}
});
$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();
}
} else {
var query = $searchBox.val();
if (lastQuery !== query) {
currentResult = -1;
if (query.length > 2) {
self.search(query);
} else {
self.hideResults();
}
if(self.hasFilter(getCurrentApp())) {
self.getFilter(getCurrentApp())(query);
}
}
}
});
$(document).keyup(function(event) {
if(event.keyCode === 27) { //esc
$searchBox.val('');
if(self.hasFilter(getCurrentApp())) {
self.getFilter(getCurrentApp())('');
}
self.hideResults();
}
});
$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, result, 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', this);
}
};
OCA.Search = Search;
})();
$(document).ready(function() {
var $searchResults = $('<div id="searchresults" class="hidden"/>');
$('#app-content')
.append($searchResults)
.find('.viewcontainer').css('min-height', 'initial');
$searchResults.load(OC.webroot + '/search/templates/part.results.html', function () {
OC.Search = new OCA.Search($('#searchbox'), $('#searchresults'));
});
});
/**
* @deprecated use get/setRenderer() instead
*/
OC.search.customResults = {};
/**
* @deprecated use get/setRenderer() instead
*/
OC.search.resultTypes = {};

View File

@ -0,0 +1,13 @@
<div id="status"></div>
<table>
<tbody>
<tr class="template">
<td class="icon"></td>
<td class="info">
<a class="link">
<div class="name"></div>
</a>
</td>
</tr>
</tbody>
</table>

View File

@ -1,15 +0,0 @@
<div id="searchresults">
<table>
<tbody>
<tr class="template">
<td class="type"></td>
<td class="result">
<a>
<div class="name"></div>
<div class="text"></div>
</a>
</td>
</tr>
</tbody>
</table>
</div>