From 0e9b05b7012844e348d36b5b35e1c50487a3bbc4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= <jfd@butonic.de>
Date: Thu, 11 Dec 2014 16:23:39 +0100
Subject: [PATCH] ajax paging, some js cleanups

---
 lib/private/search.php                        |   4 +-
 search/ajax/search.php                        |   2 +-
 search/css/results.css                        |   6 +
 search/js/result.js                           | 211 ++++--------
 search/js/search.js                           | 320 +++++++++++++-----
 .../{part.results.php => part.results.html}   |   0
 6 files changed, 300 insertions(+), 243 deletions(-)
 rename search/templates/{part.results.php => part.results.html} (100%)

diff --git a/lib/private/search.php b/lib/private/search.php
index 4629d52b40..22f92534cb 100644
--- a/lib/private/search.php
+++ b/lib/private/search.php
@@ -64,8 +64,10 @@ class Search implements ISearch {
 				$providerResults = $provider->search($query);
 				if ($size > 0) {
 					$slicedResults = array_slice($providerResults, $page * $size, $size);
+					$results = array_merge($results, $slicedResults);
+				} else {
+					$results = array_merge($results, $providerResults);
 				}
-				$results = array_merge($results, $slicedResults);
 			} else {
 				\OC::$server->getLogger()->warning('Ignoring Unknown search provider', array('provider' => $provider));
 			}
diff --git a/search/ajax/search.php b/search/ajax/search.php
index 9077108465..e26432d1eb 100644
--- a/search/ajax/search.php
+++ b/search/ajax/search.php
@@ -46,7 +46,7 @@ if (isset($_GET['page'])) {
 if (isset($_GET['size'])) {
 	$size = (int)$_GET['size'];
 } else {
-	$size = 0;
+	$size = 30;
 }
 if($query) {
 	$result = \OC::$server->getSearch()->search($query, $inApps, $page, $size);
diff --git a/search/css/results.css b/search/css/results.css
index 78b60e65e4..5dbfa2bd50 100644
--- a/search/css/results.css
+++ b/search/css/results.css
@@ -14,6 +14,9 @@
 	box-sizing: border-box;
 	z-index:75;
 }
+#searchresults * {
+	box-sizing: content-box;
+}
 
 #searchresults table {
 	border-spacing:0;
@@ -23,6 +26,9 @@
 }
 
 #searchresults td {
+	padding: 0 15px;
+	font-style: normal;
+	vertical-align: middle;
 	border-top: 20px solid white;
 	border-bottom: none;
 }
diff --git a/search/js/result.js b/search/js/result.js
index 95526749c5..217d66dc1e 100644
--- a/search/js/result.js
+++ b/search/js/result.js
@@ -8,171 +8,76 @@
  *
  */
 
-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 class="searchresults-wrapper"/>');
-		$('#app-content').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 {
-		$('#searchresults tr.result').remove();
-		$('#searchresults').show();
-		jQuery.each(results, function(i, result) {
-			var $row = $('#searchresults tr.template').clone();
-			$row.removeClass('template');
-			$row.addClass('result');
+//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) {
+			// backward compatibility:
+			if (typeof result.mime !== 'undefined') {
+				result.mime_type = result.mime;
+			} else if (typeof result.mime_type !== 'undefined') {
+				result.mime = result.mime_type;
+			}
 
-			$row.data('result', result);
+			$pathDiv = $('<div class="path"></div>').text(result.path);
+			$row.find('td.info div.name').after($pathDiv).text(result.name);
 
-			// generic results only have four attributes
-			$row.find('td.info div.name').text(result.name);
-			$row.find('td.info a').attr('href', result.link);
+			$row.find('td.result a').attr('href', result.link);
 
-			$row.find('td.icon').css('background-image', 'url(' + OC.imagePath('core', 'places/link') + ')');
-			/**
-			 * Give plugins the ability to customize the search results. For example:
-			 * OC.search.customResults.file = function (row, item){ FIXME
-			 *  if(item.name.search('.json') >= 0) ...
-			 * };
-			 */
-			if (OC.Search.hasFormatter(result.type)) {
-				OC.Search.getFormatter(result.type)($row, result);
+			if (OCA.Files) {
+				OCA.Files.App.fileList.lazyLoadPreview({
+					path: result.path,
+					mime: result.mime,
+					callback: function (url) {
+						$row.find('td.icon').css('background-image', 'url(' + url + ')');
+					}
+				});
 			} 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);
+				// 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})
+				);
 			}
-			$('#searchresults tbody').append($row);
 		});
-
-		$('#searchresults').on('click', 'tr.result', function (event) {
-			var $row = $(this);
-			var result = $row.data('result');
-			if(OC.Search.hasHandler(result.type)){
-				var result = OC.Search.getHandler(result.type)($row, result, event);
-				OC.Search.hide();
-				event.stopPropagation();
-				return result;
+		OC.Search.setHandler('file', function ($row, result, event) {
+			if (OCA.Files) {
+				OCA.Files.App.fileList.changeDirectory(OC.dirname(result.path));
+				OCA.Files.App.fileList.scrollTo(result.name);
+				return false;
+			} else {
+				return true;
 			}
 		});
-	}
-};
-OC.Search.showResults.loaded = false;
 
-OC.Search.renderCurrent = function(){
-	var $resultsContainer = $('#searchresults');
-	var result = $resultsContainer.find('tr.result')[OC.Search.currentResult]
-	if (result) {
-		var $result = $(result);
-		var currentOffset = $resultsContainer.scrollTop();
-		$resultsContainer.animate({
-			// Scrolling to the top of the new result
-			scrollTop: currentOffset + $result.offset().top - $result.height() * 2
-		}, {
-			duration: 100
-		});
-		$resultsContainer.find('tr.result.current').removeClass('current');
-		$result.addClass('current');
-	}
-};
-
-OC.Search.setFormatter('file', 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;
-	}
-
-	$pathDiv = $('<div class="path"></div>').text(result.path);
-	$row.find('td.info div.name').after($pathDiv).text(result.name);
+		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;
+			}
 
-	$row.find('td.result a').attr('href', result.link);
+			var $pathDiv = $('<div class="path"></div>').text(result.path)
+			$row.find('td.info div.name').after($pathDiv).text(result.name);
 
-	if (OCA.Files) {
-		OCA.Files.App.fileList.lazyLoadPreview({
-			path: result.path,
-			mime: result.mime,
-			callback: function (url) {
-				$row.find('td.icon').css('background-image', 'url(' + url + ')');
+			$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) {
+			if (OCA.Files) {
+				OCA.Files.App.fileList.changeDirectory(result.path);
+				return false;
+			} else {
+				return true;
 			}
 		});
-	} 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})
-		);
-	}
-});
-OC.Search.setHandler('file', function ($row, result, event) {
-	if (OCA.Files) {
-		OCA.Files.App.fileList.changeDirectory(OC.dirname(result.path));
-		OCA.Files.App.fileList.scrollTo(result.name);
-		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) {
-	if (OCA.Files) {
-		OCA.Files.App.fileList.changeDirectory(result.path);
-		return false;
-	} else {
-		return true;
-	}
+	});
 });
diff --git a/search/js/search.js b/search/js/search.js
index 06a96fd582..7a6428bce3 100644
--- a/search/js/search.js
+++ b/search/js/search.js
@@ -4,113 +4,257 @@
  * This file is licensed under the Affero General Public License version 3 or
  * later. See the COPYING file.
  *
- * @author Bernhard Posselt <dev@bernhard-posselt.com>
- * @copyright Bernhard Posselt 2014
+ * @author Jörn Friedrich Dreyer <jfd@owncloud.com>
+ * @copyright Jörn Friedrich Dreyer 2014
  */
 
-(function (exports) {
-
-	'use strict';
+(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
+	 */
+	var Search = function($searchBox) {
+		this.initialize($searchBox);
+	};
+	/**
+	 * @memberof OC
+	 */
+	Search.prototype = {
 
-	exports.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:{},
 		/**
-		 * Do a search query and display the results
-		 * @param {string} query the search query
+		 * Initialize the search box and results
+		 *
+		 * @param $searchBox container element with existing markup for the #searchbox form
+		 * @private
 		 */
-		search: _.debounce(function(query, page, size) {
-			if(query) {
-				exports.addStyle('search','results');
-				if (typeof page !== 'number') {
-					page = 0;
+		initialize: function($searchBox) {
+
+			var that = this;
+
+			/**
+			 * contains closures that are called to format search results
+			 */
+			var formatters = {};
+			this.setFormatter = function(type, formatter) {
+				formatters[type] = formatter;
+			};
+			this.hasFormatter = function(type) {
+				return typeof formatters[type] !== 'undefined';
+			};
+			this.getFormatter = function(type) {
+				return formatters[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 lastPage = 0;
+			var lastSize = 30;
+			var lastResults = {};
+
+			/**
+			 * Do a search query and display the results
+			 * @param {string} query the search query
+			 */
+			this.search = _.debounce(function(query, page, size) {
+				if(query) {
+					OC.addStyle('search','results');
+					if (typeof page !== 'number') {
+						page = 0;
+					}
+					if (typeof size !== 'number') {
+						size = 30;
+					}
+					// prevent double pages
+					if (query === lastPage && page === lastPage && currentResult !== -1) {
+						return;
+					}
+					$.getJSON(OC.generateUrl('search/ajax/search.php'), {query:query, page:page, size:size }, function(results) {
+						lastQuery = query;
+						lastPage = page;
+						lastSize = size;
+						lastResults = results;
+						if (page === 0) {
+							showResults(results);
+						} else {
+							addResults(results);
+						}
+					});
+				}
+			}, 500);
+			var $searchResults = false;
+
+			function showResults(results) {
+				if (results.length === 0) {
+					return;
 				}
-				if (typeof size !== 'number') {
-					size = 30;
+				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();
+						});
+						$(document).click(function (event) {
+							that.hideResults();
+							if (FileList && typeof FileList.unfilter === 'function') { //TODO add hook system
+								FileList.unfilter();
+							}
+						});
+						$searchResults.on('scroll', _.bind(onScroll, this));
+						lastResults = results;
+						showResults(results);
+					});
+				} else {
+					$searchResults.find('tr.result').remove();
+					$searchResults.show();
+					addResults(results);
 				}
-				$.getJSON(OC.generateUrl('search/ajax/search.php'), {query:query, page:page, size:size }, function(results) {
-					exports.Search.lastResults = results;
-					exports.Search.showResults(results);
-				});
 			}
-		}, 500)
-	};
+			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);
 
-	$(document).ready(function () {
-		$('form.searchbox').submit(function(event) {
-			event.preventDefault();
-		});
-		$('#searchbox').keyup(function(event) {
-			if (event.keyCode === 13) { //enter
-				if(exports.Search.currentResult > -1) {
-					var result = $('#searchresults tr.result a')[exports.Search.currentResult];
-					window.location = $(result).attr('href');
-				}
-			} else if(event.keyCode === 38) { //up
-				if(exports.Search.currentResult > 0) {
-					exports.Search.currentResult--;
-					exports.Search.renderCurrent();
+					// generic results only have four attributes
+					$row.find('td.info div.name').text(result.name);
+					$row.find('td.info a').attr('href', result.link);
 
+					$row.find('td.icon').css('background-image', 'url(' + OC.imagePath('core', 'places/link') + ')');
+					/**
+					 * 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);
+					} 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);
+						}
+					}
+					$searchResults.find('tbody').append($row);
+				});
+			}
+			function renderCurrent() {
+				var result = $searchResults.find('tr.result')[currentResult];
+				if (result) {
+					var $result = $(result);
+					var currentOffset = $searchResults.scrollTop();
+					$searchResults.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');
 				}
-			} else if(event.keyCode === 40) { //down
-				if(exports.Search.lastResults.length > exports.Search.currentResult + 1){
-					exports.Search.currentResult++;
-					exports.Search.renderCurrent();
-				}
-			} else if(event.keyCode === 27) { //esc
-				exports.Search.hide();
-				if (FileList && typeof FileList.unfilter === 'function') { //TODO add hook system
-					FileList.unfilter();
+			}
+			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();
+						}
+					}
 				}
-			} else {
-				var query = $('#searchbox').val();
-				if (exports.Search.lastQuery !== query) {
-					exports.Search.lastQuery = query;
-					exports.Search.currentResult = -1;
-					if (FileList && typeof FileList.filter === 'function') { //TODO add hook system
-						FileList.filter(query);
+			};
+
+			$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');
 					}
-					if (query.length > 2) {
-						exports.Search.search(query);
-					} else {
-						if (exports.Search.hide) {
-							exports.Search.hide();
+				} 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 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 (query.length > 2) {
+							that.search(query);
+						} else {
+							if (that.hideResults) {
+								that.hideResults();
+							}
 						}
 					}
 				}
+			});
+
+			/**
+			 * 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.scrollTop() + $searchResults.height() > $searchResults.find('table').height() - 300 ) {
+					that.search(lastQuery, lastPage + 1);
+				}
 			}
-		});
-	});
 
-}(OC));
+			$('form.searchbox').submit(function(event) {
+				event.preventDefault();
+			});
+		}
+	};
+	OCA.Search = Search;
+})();
+
+$(document).ready(function() {
+	OC.Search = new OCA.Search($('#searchbox'));
+});
 
 /**
  * @deprecated use get/setFormatter() instead
diff --git a/search/templates/part.results.php b/search/templates/part.results.html
similarity index 100%
rename from search/templates/part.results.php
rename to search/templates/part.results.html
-- 
GitLab