From 606f802b7be0cb6f2f6d99c547e432bb8cd27994 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= <jfd@butonic.de>
Date: Wed, 17 Dec 2014 18:49:39 +0100
Subject: [PATCH] move search results below filelist, show hint when results
 are off screen, use js plugin mechanism

---
 apps/files/css/files.css                      |   4 +-
 apps/files/index.php                          |   1 +
 apps/files/js/filelist.js                     |  15 +-
 .../js/result.js => apps/files/js/search.js   |  97 ++++++----
 core/js/js.js                                 |  37 +---
 lib/base.php                                  |   1 -
 lib/private/search.php                        |  10 +-
 lib/public/isearch.php                        |   5 +-
 lib/public/search/pagedprovider.php           |   9 +-
 search/ajax/search.php                        |   4 +-
 search/css/results.css                        |  14 +-
 search/js/search.js                           | 175 +++++++++++++-----
 search/templates/part.results.html            |   2 +-
 13 files changed, 228 insertions(+), 146 deletions(-)
 rename search/js/result.js => apps/files/js/search.js (55%)

diff --git a/apps/files/css/files.css b/apps/files/css/files.css
index 6f31715499..a75ad57c83 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 {
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..08017e3fef 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -1640,21 +1640,14 @@
 		},
 		filter:function(query) {
 			this.$fileList.find('tr').each(function(i,e) {
-				if ($(e).data('file').toString().toLowerCase().indexOf(query.toLowerCase()) !== -1) {
-					$(e).addClass("searchresult");
-				} else {
-					$(e).removeClass("searchresult");
+				if ($(e).data('file').toString().toLowerCase().indexOf(query.toLowerCase()) === -1) {
+					$(e).hide();
 				}
 			});
-			//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);
-			}
 		},
 		unfilter:function() {
-			this.$fileList.find('tr.searchresult').each(function(i,e) {
-				$(e).removeClass("searchresult");
+			this.$fileList.find('tr:hidden').each(function(i,e) {
+				$(e).show();
 			});
 		},
 		/**
diff --git a/search/js/result.js b/apps/files/js/search.js
similarity index 55%
rename from search/js/result.js
rename to apps/files/js/search.js
index 217d66dc1e..a2d3a26125 100644
--- a/search/js/result.js
+++ b/apps/files/js/search.js
@@ -7,13 +7,51 @@
  * See the COPYING-README file.
  *
  */
+(function() {
+	if (!OCA.Files) {
+		OCA.Files = {};
+	}
+	OCA.Files.Search = {
+		attach: function(search) {
+			search.setFilter('files', function (query) {
+				if (query) {
+					if (OCA.Files) {
+						OCA.Files.App.fileList.filter(query);
+					}
+				} else {
+					if (OCA.Files) {
+						OCA.Files.App.fileList.unfilter();
+					}
+				}
+			});
+
+			search.setRenderer('folder', OCA.Files.Search.renderFolderResult);
+			search.setRenderer('file',   OCA.Files.Search.renderFileResult);
+			search.setRenderer('audio',  OCA.Files.Search.renderAudioResult);
+			search.setRenderer('image',  OCA.Files.Search.renderImageResult);
+
+			search.setHandler('folder',  OCA.Files.Search.handleFolderClick);
+			search.setHandler(['file', 'audio', 'image'], OCA.Files.Search.handleFileClick);
+		},
+		renderFolderResult: function($row, result) {
+			/*render folder icon, show path beneath filename,
+			 show size and last modified date on the right */
+			// backward compatibility:
+			if (typeof result.mime !== 'undefined') {
+				result.mime_type = result.mime;
+			} else if (typeof result.mime_type !== 'undefined') {
+				result.mime = result.mime_type;
+			}
+
+			var $pathDiv = $('<div class="path"></div>').text(result.path)
+			$row.find('td.info div.name').after($pathDiv).text(result.name);
 
-//FIXME move to files?
-$(document).ready(function() {
-	// wait for other apps/extensions to register their event handlers and file actions
-	// in the "ready" clause
-	_.defer(function() {
-		OC.Search.setFormatter('file', function ($row, result) {
+			$row.find('td.result a').attr('href', result.link);
+			$row.find('td.icon').css('background-image', 'url(' + OC.imagePath('core', 'filetypes/folder') + ')');
+		},
+		renderFileResult: function($row, result) {
+			/*render preview icon, show path beneath filename,
+			 show size and last modified date on the right */
 			// backward compatibility:
 			if (typeof result.mime !== 'undefined') {
 				result.mime_type = result.mime;
@@ -46,38 +84,35 @@ $(document).ready(function() {
 					OC.generateUrl('/apps/files/?dir={dir}&scrollto={scrollto}', {dir: dir, scrollto: result.name})
 				);
 			}
-		});
-		OC.Search.setHandler('file', function ($row, result, event) {
+		},
+		renderAudioResult: function($row, result) {
+			/*render preview icon, show path beneath filename,
+			 show size and last modified date on the right
+			 show Artist and Album */
+		},
+		renderImageResult: function($row, result) {
+			/*render preview icon, show path beneath filename,
+			 show size and last modified date on the right
+			 show width and height */
+		},
+		handleFolderClick: function($row, result, event) {
+			// open folder
 			if (OCA.Files) {
-				OCA.Files.App.fileList.changeDirectory(OC.dirname(result.path));
-				OCA.Files.App.fileList.scrollTo(result.name);
+				OCA.Files.App.fileList.changeDirectory(result.path);
 				return false;
 			} else {
 				return true;
 			}
-		});
-
-		OC.Search.setFormatter('folder', function ($row, result) {
-			// backward compatibility:
-			if (typeof result.mime !== 'undefined') {
-				result.mime_type = result.mime;
-			} else if (typeof result.mime_type !== 'undefined') {
-				result.mime = result.mime_type;
-			}
-
-			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') + ')');
-		});
-		OC.Search.setHandler('folder', function ($row, result, event) {
+		},
+		handleFileClick: function($row, result, event) {
 			if (OCA.Files) {
-				OCA.Files.App.fileList.changeDirectory(result.path);
+				OCA.Files.App.fileList.changeDirectory(OC.dirname(result.path));
+				OCA.Files.App.fileList.scrollTo(result.name);
 				return false;
 			} else {
 				return true;
 			}
-		});
-	});
-});
+		}
+	};
+})();
+OC.Plugins.register('OCA.Search', OCA.Files.Search);
diff --git a/core/js/js.js b/core/js/js.js
index 9d38a8c77f..8bcd546b42 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -309,7 +309,7 @@ var OC={
 	 * @param {string} query the search query
 	 */
 	search: function (query) {
-		OC.Search.search(query, 0, 30);
+		OC.Search.search(query, null, 0, 30);
 	},
 	/**
 	 * Dialog helper for jquery dialogs.
@@ -601,41 +601,6 @@ OC.search.customResults = {};
  */
 OC.search.resultTypes = {};
 
-/**
- * @namespace OC.Search
- */
-OC.Search = {
-	/**
-	 * contains closures that are called to format search results
-	 */
-	formatter:{},
-	setFormatter: function(type, formatter) {
-		this.formatter[type] = formatter;
-	},
-	hasFormatter: function(type) {
-		return typeof this.formatter[type] !== 'undefined';
-	},
-	getFormatter: function(type) {
-		return this.formatter[type];
-	},
-	/**
-	 * contains closures that are called when a search result has been clicked
-	 */
-	handler:{},
-	setHandler: function(type, handler) {
-		this.handler[type] = handler;
-	},
-	hasHandler: function(type) {
-		return typeof this.handler[type] !== 'undefined';
-	},
-	getHandler: function(type) {
-		return this.handler[type];
-	},
-	currentResult:-1,
-	lastQuery:'',
-	lastResults:{}
-};
-
 OC.addStyle.loaded=[];
 OC.addScript.loaded=[];
 
diff --git a/lib/base.php b/lib/base.php
index 551d0afbd7..34fa178ebf 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -363,7 +363,6 @@ class OC {
 		OC_Util::addScript("config");
 		//OC_Util::addScript( "multiselect" );
 		OC_Util::addScript('search', 'search');
-		OC_Util::addScript('search', 'result');
 		OC_Util::addScript("oc-requesttoken");
 		OC_Util::addScript("apps");
 		OC_Util::addVendorScript('snapjs/dist/latest/snap');
diff --git a/lib/private/search.php b/lib/private/search.php
index 22f92534cb..a29a4762b6 100644
--- a/lib/private/search.php
+++ b/lib/private/search.php
@@ -40,17 +40,19 @@ class Search implements ISearch {
 	 * @return array An array of OC\Search\Result's
 	 */
 	public function search($query, array $inApps = array()) {
-		return $this->searchPaged($query, $inApps, 0, 0);
+		// 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 int $page
+	 * @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, $page = 0, $size = 30) {
+	public function searchPaged($query, array $inApps = array(), $page = 1, $size = 30) {
 		$this->initProviders();
 		$results = array();
 		foreach($this->providers as $provider) {
@@ -63,7 +65,7 @@ class Search implements ISearch {
 			} else if ($provider instanceof Provider) {
 				$providerResults = $provider->search($query);
 				if ($size > 0) {
-					$slicedResults = array_slice($providerResults, $page * $size, $size);
+					$slicedResults = array_slice($providerResults, ($page - 1) * $size, $size);
 					$results = array_merge($results, $slicedResults);
 				} else {
 					$results = array_merge($results, $providerResults);
diff --git a/lib/public/isearch.php b/lib/public/isearch.php
index 84e450afe6..fe58f202d6 100644
--- a/lib/public/isearch.php
+++ b/lib/public/isearch.php
@@ -41,11 +41,12 @@ interface ISearch {
 	/**
 	 * Search all providers for $query
 	 * @param string $query
-	 * @param int $page
+	 * @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, $page = 0, $size = 30);
+	public function searchPaged($query, array $inApps = array(), $page = 1, $size = 30);
 
 	/**
 	 * Register a new search provider to search with
diff --git a/lib/public/search/pagedprovider.php b/lib/public/search/pagedprovider.php
index 97da1dd2c8..10c36cfc48 100644
--- a/lib/public/search/pagedprovider.php
+++ b/lib/public/search/pagedprovider.php
@@ -44,15 +44,16 @@ abstract class PagedProvider extends Provider {
 	 * @return array An array of OCP\Search\Result's
 	 */
 	public function search($query) {
-		$this->searchPaged($query, 0, 0);
+		// old apps might assume they get all results, so we set size 0
+		$this->searchPaged($query, 1, 0);
 	}
 
 	/**
 	 * Search for $query
 	 * @param string $query
-	 * @param int $limit, 0 = unlimited
-	 * @param int $offset
+	 * @param int $page pages start at page 1
+	 * @param int $size, 0 = all
 	 * @return array An array of OCP\Search\Result's
 	 */
-	abstract public function searchPaged($query, $limit, $offset);
+	abstract public function searchPaged($query, $page, $size);
 }
diff --git a/search/ajax/search.php b/search/ajax/search.php
index e26432d1eb..5bd810aacf 100644
--- a/search/ajax/search.php
+++ b/search/ajax/search.php
@@ -41,7 +41,7 @@ if (isset($_GET['inApps'])) {
 if (isset($_GET['page'])) {
 	$page = (int)$_GET['page'];
 } else {
-	$page = 0;
+	$page = 1;
 }
 if (isset($_GET['size'])) {
 	$size = (int)$_GET['size'];
@@ -49,7 +49,7 @@ if (isset($_GET['size'])) {
 	$size = 30;
 }
 if($query) {
-	$result = \OC::$server->getSearch()->search($query, $inApps, $page, $size);
+	$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 5dbfa2bd50..bf9a7871d1 100644
--- a/search/css/results.css
+++ b/search/css/results.css
@@ -6,9 +6,7 @@
 	background-color:#fff;
 	overflow-x:hidden;
 	overflow-y: auto;
-	position:fixed;
 	text-overflow:ellipsis;
-	top:0;
 	padding-top: 45px;
 	height: 100%;
 	box-sizing: border-box;
@@ -18,6 +16,18 @@
 	box-sizing: content-box;
 }
 
+#searchresults #status {
+	background-color:#ccc;
+	height: 24px;
+	padding: 17px 15px;
+}
+#searchresults #status.fixed {
+	position: fixed;
+	bottom: 0;
+	width: 100%;
+	z-index: 10;
+}
+
 #searchresults table {
 	border-spacing:0;
 	table-layout:fixed;
diff --git a/search/js/search.js b/search/js/search.js
index 7a6428bce3..8791b98cfd 100644
--- a/search/js/search.js
+++ b/search/js/search.js
@@ -36,17 +36,31 @@
 			var that = this;
 
 			/**
-			 * contains closures that are called to format search results
+			 * contains closures that are called to filter the current content
 			 */
-			var formatters = {};
-			this.setFormatter = function(type, formatter) {
-				formatters[type] = formatter;
+			var filters = {};
+			this.setFilter = function(type, filter) {
+				filters[type] = filter;
 			};
-			this.hasFormatter = function(type) {
-				return typeof formatters[type] !== 'undefined';
+			this.hasFilter = function(type) {
+				return typeof filters[type] !== 'undefined';
 			};
-			this.getFormatter = function(type) {
-				return formatters[type];
+			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];
 			};
 
 			/**
@@ -73,25 +87,33 @@
 			 * Do a search query and display the results
 			 * @param {string} query the search query
 			 */
-			this.search = _.debounce(function(query, page, size) {
+			this.search = _.debounce(function(query, inApps, page, size) {
 				if(query) {
 					OC.addStyle('search','results');
 					if (typeof page !== 'number') {
-						page = 0;
+						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 (query === lastPage && page === lastPage && currentResult !== -1) {
+					if ($searchResults && query === lastQuery && page === lastPage&& size === lastSize) {
 						return;
 					}
-					$.getJSON(OC.generateUrl('search/ajax/search.php'), {query:query, page:page, size:size }, function(results) {
-						lastQuery = query;
-						lastPage = page;
-						lastSize = size;
+					lastQuery = query;
+					lastPage = page;
+					lastSize = size;
+					$.getJSON(OC.generateUrl('search/ajax/search.php'), {query:query, inApps:inApps, page:page, size:size }, function(results) {
 						lastResults = results;
-						if (page === 0) {
+						if (page === 1) {
 							showResults(results);
 						} else {
 							addResults(results);
@@ -99,29 +121,69 @@
 					});
 				}
 			}, 500);
+
+			function getCurrentApp() {
+				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 $searchResults = false;
+			var $wrapper = false;
+			var $status = false;
+			const summaryAndStatusHeight = 118;
 
+			function isStatusOffScreen() {
+				return $searchResults.position().top + summaryAndStatusHeight > window.innerHeight;
+			}
+
+			function placeStatus() {
+				if (isStatusOffScreen()) {
+					$status.addClass('fixed');
+				} else {
+					$status.removeClass('fixed');
+				}
+			}
 			function showResults(results) {
 				if (results.length === 0) {
 					return;
 				}
 				if (!$searchResults) {
-					var $parent = $('<div class="searchresults-wrapper"/>');
-					$('#app-content').append($parent);
-					$parent.load(OC.webroot + '/search/templates/part.results.html', function () {
-						$searchResults = $parent.find('#searchresults');
-						$searchResults.click(function (event) {
-							that.hideResults();
-							event.stopPropagation();
+					$wrapper = $('<div class="searchresults-wrapper"/>');
+					$('#app-content')
+						.append($wrapper)
+						.find('.viewcontainer').css('min-height', 'initial');
+					$wrapper.load(OC.webroot + '/search/templates/part.results.html', function () {
+						$searchResults = $wrapper.find('#searchresults');
+						$searchResults.on('click', 'tr.result', function (event) {
+							var $row = $(this);
+							var item = $row.data('result');
+							if(that.hasHandler(item.type)){
+								var result = that.getHandler(item.type)($row, result, event);
+								that.hideResults();
+								return result;
+							}
+						});
+						$searchResults.on('click', '#status', function (event) {
+							event.preventDefault();
+							scrollToResults();
+							return false;
 						});
 						$(document).click(function (event) {
 							that.hideResults();
-							if (FileList && typeof FileList.unfilter === 'function') { //TODO add hook system
-								FileList.unfilter();
+							if(that.hasFilter(getCurrentApp())) {
+								that.getFilter(getCurrentApp())('');
 							}
 						});
-						$searchResults.on('scroll', _.bind(onScroll, this));
+						$('#app-content').on('scroll', _.bind(onScroll, this));
 						lastResults = results;
+						$status = $searchResults.find('#status')
+							.text(t('search', '{count} Search results', {count:results.length}, results.length));
+						placeStatus();
 						showResults(results);
 					});
 				} else {
@@ -147,8 +209,8 @@
 					/**
 					 * Give plugins the ability to customize the search results. see result.js for examples
 					 */
-					if (that.hasFormatter(result.type)) {
-						that.getFormatter(result.type)($row, result);
+					if (that.hasRenderer(result.type)) {
+						that.getRenderer(result.type)($row, result);
 					} else {
 						// for backward compatibility add text div
 						$row.find('td.info div.name').addClass('result');
@@ -166,7 +228,7 @@
 				if (result) {
 					var $result = $(result);
 					var currentOffset = $searchResults.scrollTop();
-					$searchResults.animate({
+					$('#app-content').animate({
 						// Scrolling to the top of the new result
 						scrollTop: currentOffset + $result.offset().top - $result.height() * 2
 					}, {
@@ -179,17 +241,10 @@
 			this.hideResults = function() {
 				if ($searchResults) {
 					$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();
-						}
-					}
+					$searchBox.val('');
+					$wrapper.remove();
+					$searchResults = false;
+					$wrapper = false;
 				}
 			};
 
@@ -212,16 +267,13 @@
 					}
 				} else if(event.keyCode === 27) { //esc
 					that.hideResults();
-					if (FileList && typeof FileList.unfilter === 'function') { //TODO add hook system
-						FileList.unfilter();
-					}
 				} else {
 					var query = $searchBox.val();
 					if (lastQuery !== query) {
 						lastQuery = query;
 						currentResult = -1;
-						if (FileList && typeof FileList.filter === 'function') { //TODO add hook system
-							FileList.filter(query);
+						if(that.hasFilter(getCurrentApp())) {
+							that.getFilter(getCurrentApp())(query);
 						}
 						if (query.length > 2) {
 							that.search(query);
@@ -239,14 +291,39 @@
 			 * This appends/renders the next page of entries when reaching the bottom.
 			 */
 			function onScroll(e) {
-				if ( $searchResults.scrollTop() + $searchResults.height() > $searchResults.find('table').height() - 300 ) {
-					that.search(lastQuery, lastPage + 1);
+				if ($searchResults) {
+					//if ( $searchResults && $searchResults.scrollTop() + $searchResults.height() > $searchResults.find('table').height() - 300 ) {
+					//	that.search(lastQuery, lastPage + 1);
+					//}
+					placeStatus();
 				}
 			}
 
+			/**
+			 * 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();
 			});
+
+			OC.Plugins.attach('OCA.Search', this);
 		}
 	};
 	OCA.Search = Search;
@@ -257,10 +334,10 @@ $(document).ready(function() {
 });
 
 /**
- * @deprecated use get/setFormatter() instead
+ * @deprecated use get/setRenderer() instead
  */
 OC.search.customResults = {};
 /**
- * @deprecated use get/setFormatter() instead
+ * @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
index 451df7b143..d82a72c119 100644
--- a/search/templates/part.results.html
+++ b/search/templates/part.results.html
@@ -1,5 +1,5 @@
 <div id="searchresults">
-	<!-- p>{message}</p -->
+	<div id="status">{message}</div>
 	<table>
 		<tbody>
 			<tr class="template">
-- 
GitLab