Completed
Push — stable9 ( 485cb1...e094cf )
by Lukas
26:41 queued 26:23
created

core/search/js/search.js (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
/**
2
 * ownCloud - core
3
 *
4
 * This file is licensed under the Affero General Public License version 3 or
5
 * later. See the COPYING file.
6
 *
7
 * @author Jörn Friedrich Dreyer <[email protected]>
8
 * @copyright Jörn Friedrich Dreyer 2014
9
 */
10
11
(function () {
12
	/**
13
	 * @class OCA.Search
14
	 * @classdesc
15
	 *
16
	 * The Search class manages a search queries and their results
17
	 *
18
	 * @param $searchBox container element with existing markup for the #searchbox form
19
	 * @param $searchResults container element for results und status message
20
	 */
21
	var Search = function($searchBox, $searchResults) {
22
		this.initialize($searchBox, $searchResults);
23
	};
24
	/**
25
	 * @memberof OC
26
	 */
27
	Search.prototype = {
28
29
		/**
30
		 * Initialize the search box
31
		 *
32
		 * @param $searchBox container element with existing markup for the #searchbox form
33
		 * @param $searchResults container element for results und status message
34
		 * @private
35
		 */
36
		initialize: function($searchBox, $searchResults) {
37
38
			var self = this;
39
40
			/**
41
			 * contains closures that are called to filter the current content
42
			 */
43
			var filters = {};
44
			this.setFilter = function(type, filter) {
45
				filters[type] = filter;
46
			};
47
			this.hasFilter = function(type) {
48
				return typeof filters[type] !== 'undefined';
49
			};
50
			this.getFilter = function(type) {
51
				return filters[type];
52
			};
53
54
			/**
55
			 * contains closures that are called to render search results
56
			 */
57
			var renderers = {};
58
			this.setRenderer = function(type, renderer) {
59
				renderers[type] = renderer;
60
			};
61
			this.hasRenderer = function(type) {
62
				return typeof renderers[type] !== 'undefined';
63
			};
64
			this.getRenderer = function(type) {
65
				return renderers[type];
66
			};
67
68
			/**
69
			 * contains closures that are called when a search result has been clicked
70
			 */
71
			var handlers = {};
72
			this.setHandler = function(type, handler) {
73
				handlers[type] = handler;
74
			};
75
			this.hasHandler = function(type) {
76
				return typeof handlers[type] !== 'undefined';
77
			};
78
			this.getHandler = function(type) {
79
				return handlers[type];
80
			};
81
82
			var currentResult = -1;
83
			var lastQuery = '';
84
			var lastInApps = [];
85
			var lastPage = 0;
86
			var lastSize = 30;
87
			var lastResults = [];
88
			var timeoutID = null;
89
90
			this.getLastQuery = function() {
91
				return lastQuery;
92
			};
93
94
			/**
95
			 * Do a search query and display the results
96
			 * @param {string} query the search query
97
			 * @param inApps
98
			 * @param page
99
			 * @param size
100
			 */
101
			this.search = function(query, inApps, page, size) {
102
				if (query) {
103
					OC.addStyle('core/search','results');
104
					if (typeof page !== 'number') {
105
						page = 1;
106
					}
107
					if (typeof size !== 'number') {
108
						size = 30;
109
					}
110
					if (typeof inApps !== 'object') {
111
						var currentApp = getCurrentApp();
112
						if(currentApp) {
113
							inApps = [currentApp];
114
						} else {
115
							inApps = [];
116
						}
117
					}
118
					// prevent double pages
119
					if ($searchResults && query === lastQuery && page === lastPage && size === lastSize) {
120
						return;
121
					}
122
					window.clearTimeout(timeoutID);
123
					timeoutID = window.setTimeout(function() {
124
						lastQuery = query;
125
						lastInApps = inApps;
126
						lastPage = page;
127
						lastSize = size;
128
129
						//show spinner
130
						$searchResults.removeClass('hidden');
131
						$status.addClass('status');
132
						$status.html(t('core', 'Searching other places')+'<img class="spinner" alt="search in progress" src="'+OC.webroot+'/core/img/loading.gif" />');
0 ignored issues
show
Line is too long.
Loading history...
133
134
						// do the actual search query
135
						$.getJSON(OC.generateUrl('core/search'), {query:query, inApps:inApps, page:page, size:size }, function(results) {
0 ignored issues
show
Line is too long.
Loading history...
136
							lastResults = results;
137
							if (page === 1) {
138
								showResults(results);
139
							} else {
140
								addResults(results);
141
							}
142
						});
143
					}, 500);
144
				}
145
			};
146
147
			//TODO should be a core method, see https://github.com/owncloud/core/issues/12557
148
			function getCurrentApp() {
149
				var content = document.getElementById('content');
150
				if (content) {
151
					var classList = document.getElementById('content').className.split(/\s+/);
152
					for (var i = 0; i < classList.length; i++) {
153
						if (classList[i].indexOf('app-') === 0) {
154
							return classList[i].substr(4);
155
						}
156
					}
157
				}
158
				return false;
159
			}
160
161
			var $status = $searchResults.find('#status');
162
			// summaryAndStatusHeight is a constant
163
			var summaryAndStatusHeight = 118;
164
165
			function isStatusOffScreen() {
166
				return $searchResults.position() &&
167
					($searchResults.position().top + summaryAndStatusHeight > window.innerHeight);
168
			}
169
170
			function placeStatus() {
171
				if (isStatusOffScreen()) {
172
					$status.addClass('fixed');
173
				} else {
174
					$status.removeClass('fixed');
175
				}
176
			}
177
			function showResults(results) {
178
				lastResults = results;
179
				$searchResults.find('tr.result').remove();
180
				$searchResults.removeClass('hidden');
181
				addResults(results);
182
			}
183
			function addResults(results) {
184
				var $template = $searchResults.find('tr.template');
185
				jQuery.each(results, function (i, result) {
186
					var $row = $template.clone();
187
					$row.removeClass('template');
188
					$row.addClass('result');
189
190
					$row.data('result', result);
191
192
					// generic results only have four attributes
193
					$row.find('td.info div.name').text(result.name);
194
					$row.find('td.info a').attr('href', result.link);
195
196
					/**
197
					 * Give plugins the ability to customize the search results. see result.js for examples
198
					 */
199
					if (self.hasRenderer(result.type)) {
200
						$row = self.getRenderer(result.type)($row, result);
201
					} else {
202
						// for backward compatibility add text div
203
						$row.find('td.info div.name').addClass('result');
204
						$row.find('td.result div.name').after('<div class="text"></div>');
205
						$row.find('td.result div.text').text(result.name);
206
						if (OC.search.customResults && OC.search.customResults[result.type]) {
207
							OC.search.customResults[result.type]($row, result);
208
						}
209
					}
210
					if ($row) {
211
						$searchResults.find('tbody').append($row);
212
					}
213
				});
214
				var count = $searchResults.find('tr.result').length;
215
				$status.data('count', count);
216
				if (count === 0) {
217
					$status.addClass('emptycontent').removeClass('status');
218
					$status.html('');
219
					$status.append('<div class="icon-search"></div>');
220
					$status.append('<h2>' + t('core', 'No search results in other folders') + '</h2>');
221
				} else {
222
					$status.removeClass('emptycontent').addClass('status');
223
					$status.text(n('core', '{count} search result in another folder', '{count} search results in other folders', count, {count:count}));
0 ignored issues
show
Line is too long.
Loading history...
224
				}
225
			}
226
			function renderCurrent() {
227
				var result = $searchResults.find('tr.result')[currentResult];
228
				if (result) {
229
					var $result = $(result);
230
					var currentOffset = $('#app-content').scrollTop();
231
					$('#app-content').animate({
232
						// Scrolling to the top of the new result
233
						scrollTop: currentOffset + $result.offset().top - $result.height() * 2
234
					}, {
235
						duration: 100
236
					});
237
					$searchResults.find('tr.result.current').removeClass('current');
238
					$result.addClass('current');
239
				}
240
			}
241
			this.hideResults = function() {
242
				$searchResults.addClass('hidden');
243
				$searchResults.find('tr.result').remove();
244
				lastQuery = false;
245
			};
246
			this.clear = function() {
247
				self.hideResults();
248
				if(self.hasFilter(getCurrentApp())) {
249
					self.getFilter(getCurrentApp())('');
250
				}
251
				$searchBox.val('');
252
				$searchBox.blur();
253
			};
254
255
			/**
256
			 * Event handler for when scrolling the list container.
257
			 * This appends/renders the next page of entries when reaching the bottom.
258
			 */
259
			function onScroll() {
260
				if ($searchResults && lastQuery !== false && lastResults.length > 0) {
261
					var resultsBottom = $searchResults.offset().top + $searchResults.height();
262
					var containerBottom = $searchResults.offsetParent().offset().top +
263
						$searchResults.offsetParent().height();
264
					if ( resultsBottom < containerBottom * 1.2 ) {
265
						self.search(lastQuery, lastInApps, lastPage + 1);
266
					}
267
					placeStatus();
268
				}
269
			}
270
271
			$('#app-content').on('scroll', _.bind(onScroll, this));
272
273
			/**
274
			 * scrolls the search results to the top
275
			 */
276
			function scrollToResults() {
277
				setTimeout(function() {
278
					if (isStatusOffScreen()) {
279
						var newScrollTop = $('#app-content').prop('scrollHeight') - $searchResults.height();
280
						console.log('scrolling to ' + newScrollTop);
281
						$('#app-content').animate({
282
							scrollTop: newScrollTop
283
						}, {
284
							duration: 100,
285
							complete: function () {
286
								scrollToResults();
287
							}
288
						});
289
					}
290
				}, 150);
291
			}
292
293
			$('form.searchbox').submit(function(event) {
294
				event.preventDefault();
295
			});
296
297
			$searchBox.on('search', function () {
298
				if($searchBox.val() === '') {
299
					if(self.hasFilter(getCurrentApp())) {
300
						self.getFilter(getCurrentApp())('');
301
					}
302
					self.hideResults();
303
				}
304
			});
305
			$searchBox.keyup(function(event) {
306
				if (event.keyCode === 13) { //enter
307
					if(currentResult > -1) {
308
						var result = $searchResults.find('tr.result a')[currentResult];
309
						window.location = $(result).attr('href');
310
					}
311
				} else if(event.keyCode === 38) { //up
312
					if(currentResult > 0) {
313
						currentResult--;
314
						renderCurrent();
315
					}
316
				} else if(event.keyCode === 40) { //down
317
					if(lastResults.length > currentResult + 1){
318
						currentResult++;
319
						renderCurrent();
320
					}
321
				} else {
322
					var query = $searchBox.val();
323
					if (lastQuery !== query) {
324
						currentResult = -1;
325
						if (query.length > 2) {
326
							self.search(query);
327
						} else {
328
							self.hideResults();
329
						}
330
						if(self.hasFilter(getCurrentApp())) {
331
							self.getFilter(getCurrentApp())(query);
332
						}
333
					}
334
				}
335
			});
336
			$(document).keyup(function(event) {
337
				if(event.keyCode === 27) { //esc
338
					$searchBox.val('');
339
					if(self.hasFilter(getCurrentApp())) {
340
						self.getFilter(getCurrentApp())('');
341
					}
342
					self.hideResults();
343
				}
344
			});
345
346
			$(document).keydown(function(event) {
347
				if ((event.ctrlKey || event.metaKey) && // Ctrl or Command (OSX)
348
					!event.shiftKey &&
349
					event.keyCode === 70 && // F
350
					self.hasFilter(getCurrentApp()) && // Search is enabled
351
					!$searchBox.is(':focus') // if searchbox is already focused do nothing (fallback to browser default)
352
				) {
353
					$searchBox.focus();
354
					event.preventDefault();
355
				}
356
			});
357
358
			$searchResults.on('click', 'tr.result', function (event) {
359
				var $row = $(this);
360
				var item = $row.data('result');
361
				if(self.hasHandler(item.type)){
362
					var result = self.getHandler(item.type)($row, item, event);
363
					$searchBox.val('');
364
					if(self.hasFilter(getCurrentApp())) {
365
						self.getFilter(getCurrentApp())('');
366
					}
367
					self.hideResults();
368
					return result;
369
				}
370
			});
371
			$searchResults.on('click', '#status', function (event) {
372
				event.preventDefault();
373
				scrollToResults();
374
				return false;
375
			});
376
			placeStatus();
377
378
			OC.Plugins.attach('OCA.Search', this);
379
380
			// hide search file if search is not enabled
381
			if(self.hasFilter(getCurrentApp())) {
382
				return;
383
			}
384
			if ($searchResults.length === 0) {
385
				$searchBox.hide();
386
			}
387
		}
388
	};
389
	OCA.Search = Search;
390
})();
391
392
$(document).ready(function() {
393
	var $searchResults = $('#searchresults');
394
	if ($searchResults.length > 0) {
395
		$searchResults.addClass('hidden');
396
		$('#app-content')
397
			.find('.viewcontainer').css('min-height', 'initial');
398
		$searchResults.load(OC.webroot + '/core/search/templates/part.results.html', function () {
399
			OC.Search = new OCA.Search($('#searchbox'), $('#searchresults'));
400
		});
401
	} else {
402
		_.defer(function() {
403
			OC.Search = new OCA.Search($('#searchbox'), $('#searchresults'));
404
		});
405
	}
406
});
407
408
/**
409
 * @deprecated use get/setRenderer() instead
410
 */
411
OC.search.customResults = {};
412
/**
413
 * @deprecated use get/setRenderer() instead
414
 */
415
OC.search.resultTypes = {};
416