diff --git a/apps/files/css/files.css b/apps/files/css/files.css
index 6f31715499..1e7b1d45f7 100644
--- a/apps/files/css/files.css
+++ b/apps/files/css/files.css
@@ -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;
+}
\ No newline at end of file
diff --git a/apps/files/index.php b/apps/files/index.php
index 64b49c3bf1..767cb156ca 100644
--- a/apps/files/index.php
+++ b/apps/files/index.php
@@ -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');
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index 09cb3d3287..e680ef4b3e 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -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
diff --git a/apps/files/js/filesummary.js b/apps/files/js/filesummary.js
index f83eb54678..d69c5f1b53 100644
--- a/apps/files/js/filesummary.js
+++ b/apps/files/js/filesummary.js
@@ -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: ''+directoryInfo+'',
@@ -182,7 +213,7 @@
var info = t('files', '{dirs} and {files}', infoVars);
- var $summary = $('
'+info+' | '+fileSize+' | ');
+ var $summary = $(''+info+''+filterInfo+' | '+fileSize+' | ');
if (!this.summary.totalFiles && !this.summary.totalDirs) {
this.$el.addClass('hidden');
diff --git a/apps/files/js/search.js b/apps/files/js/search.js
new file mode 100644
index 0000000000..394bcb4860
--- /dev/null
+++ b/apps/files/js/search.js
@@ -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 = $('').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 = $('').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();
+})();
diff --git a/apps/files/templates/list.php b/apps/files/templates/list.php
index 4224d9bc10..aa879002ba 100644
--- a/apps/files/templates/list.php
+++ b/apps/files/templates/list.php
@@ -60,6 +60,12 @@
t('Upload some content or sync with your devices!')); ?>
+
+
+
t('No entries found in this folder')); ?>
+
+
+
diff --git a/apps/files/templates/simplelist.php b/apps/files/templates/simplelist.php
index d806a220ac..6b6c018024 100644
--- a/apps/files/templates/simplelist.php
+++ b/apps/files/templates/simplelist.php
@@ -11,6 +11,12 @@
+
+
+
t('No entries found in this folder')); ?>
+
+
+
diff --git a/apps/files/tests/js/filesummarySpec.js b/apps/files/tests/js/filesummarySpec.js
index 5e39dd1d23..4c53b7d8b3 100644
--- a/apps/files/tests/js/filesummarySpec.js
+++ b/apps/files/tests/js/filesummarySpec.js
@@ -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);
+ });
});
diff --git a/apps/files_sharing/templates/list.php b/apps/files_sharing/templates/list.php
index a1d95ebc1f..55ad55a0a4 100644
--- a/apps/files_sharing/templates/list.php
+++ b/apps/files_sharing/templates/list.php
@@ -8,6 +8,12 @@
+
+
+
t('No entries found in this folder')); ?>
+
+
+
diff --git a/apps/files_trashbin/templates/index.php b/apps/files_trashbin/templates/index.php
index fe1311340c..0c0f955cf4 100644
--- a/apps/files_trashbin/templates/index.php
+++ b/apps/files_trashbin/templates/index.php
@@ -12,6 +12,12 @@
+
+
+
t('No entries found in this folder')); ?>
+
+
+
diff --git a/core/ajax/preview.php b/core/ajax/preview.php
index 56ef5ea847..03dfb48306 100644
--- a/core/ajax/preview.php
+++ b/core/ajax/preview.php
@@ -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);
diff --git a/core/js/core.json b/core/js/core.json
index d3a9e2404e..101a88cd4f 100644
--- a/core/js/core.json
+++ b/core/js/core.json
@@ -22,6 +22,7 @@
"eventsource.js",
"config.js",
"multiselect.js",
- "oc-requesttoken.js"
+ "oc-requesttoken.js",
+ "../../search/js/search.js"
]
}
diff --git a/core/js/js.js b/core/js/js.js
index 57ce1ab695..8bcd546b42 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -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) {
diff --git a/lib/base.php b/lib/base.php
index 009732ead7..34fa178ebf 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -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');
diff --git a/lib/private/files/storage/common.php b/lib/private/files/storage/common.php
index fe6aefbb42..b2bf41f751 100644
--- a/lib/private/files/storage/common.php
+++ b/lib/private/files/storage/common.php
@@ -278,6 +278,7 @@ abstract class Common implements \OC\Files\Storage\Storage {
}
}
}
+ closedir($dh);
return $files;
}
diff --git a/lib/private/search.php b/lib/private/search.php
index 8f04aa8360..a29a4762b6 100644
--- a/lib/private/search.php
+++ b/lib/private/search.php
@@ -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;
diff --git a/lib/private/search/result/file.php b/lib/private/search/result/file.php
index 331fdaa383..13f1a62fbc 100644
--- a/lib/private/search/result/file.php
+++ b/lib/private/search/result/file.php
@@ -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();
}
/**
diff --git a/lib/public/isearch.php b/lib/public/isearch.php
index 229376ed3a..fe58f202d6 100644
--- a/lib/public/isearch.php
+++ b/lib/public/isearch.php
@@ -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
diff --git a/lib/public/search/pagedprovider.php b/lib/public/search/pagedprovider.php
new file mode 100644
index 0000000000..55771762e6
--- /dev/null
+++ b/lib/public/search/pagedprovider.php
@@ -0,0 +1,58 @@
+.
+ *
+ */
+
+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);
+}
diff --git a/lib/public/search/provider.php b/lib/public/search/provider.php
index d6cd1fb025..c9ef173d36 100644
--- a/lib/public/search/provider.php
+++ b/lib/public/search/provider.php
@@ -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
diff --git a/search/ajax/search.php b/search/ajax/search.php
index 21e127e72b..5bd810aacf 100644
--- a/search/ajax/search.php
+++ b/search/ajax/search.php
@@ -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 {
diff --git a/search/css/results.css b/search/css/results.css
index 6aa73f55c3..04f7b6dcb9 100644
--- a/search/css/results.css
+++ b/search/css/results.css
@@ -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;
}
diff --git a/search/js/result.js b/search/js/result.js
deleted file mode 100644
index fe84aecde3..0000000000
--- a/search/js/result.js
+++ /dev/null
@@ -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;i2){
- $('#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=$('');
- $('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= 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');
- }
-};
diff --git a/search/js/search.js b/search/js/search.js
new file mode 100644
index 0000000000..318858ebd7
--- /dev/null
+++ b/search/js/search.js
@@ -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
+ * @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')+'');
+
+ // 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('');
+ $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 = $('');
+ $('#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 = {};
\ No newline at end of file
diff --git a/search/templates/part.results.html b/search/templates/part.results.html
new file mode 100644
index 0000000000..612d02c18f
--- /dev/null
+++ b/search/templates/part.results.html
@@ -0,0 +1,13 @@
+
+
diff --git a/search/templates/part.results.php b/search/templates/part.results.php
deleted file mode 100644
index b6e7bad4a2..0000000000
--- a/search/templates/part.results.php
+++ /dev/null
@@ -1,15 +0,0 @@
-