Dan Brown

Added tag autosuggestion when no input provided

Shows the most popular tag names/values.
As requested on #121
...@@ -55,7 +55,7 @@ class TagController extends Controller ...@@ -55,7 +55,7 @@ class TagController extends Controller
55 */ 55 */
56 public function getNameSuggestions(Request $request) 56 public function getNameSuggestions(Request $request)
57 { 57 {
58 - $searchTerm = $request->get('search'); 58 + $searchTerm = $request->has('search') ? $request->get('search') : false;
59 $suggestions = $this->tagRepo->getNameSuggestions($searchTerm); 59 $suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
60 return response()->json($suggestions); 60 return response()->json($suggestions);
61 } 61 }
...@@ -66,7 +66,7 @@ class TagController extends Controller ...@@ -66,7 +66,7 @@ class TagController extends Controller
66 */ 66 */
67 public function getValueSuggestions(Request $request) 67 public function getValueSuggestions(Request $request)
68 { 68 {
69 - $searchTerm = $request->get('search'); 69 + $searchTerm = $request->has('search') ? $request->get('search') : false;
70 $tagName = $request->has('name') ? $request->get('name') : false; 70 $tagName = $request->has('name') ? $request->get('name') : false;
71 $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName); 71 $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
72 return response()->json($suggestions); 72 return response()->json($suggestions);
......
...@@ -58,30 +58,44 @@ class TagRepo ...@@ -58,30 +58,44 @@ class TagRepo
58 58
59 /** 59 /**
60 * Get tag name suggestions from scanning existing tag names. 60 * Get tag name suggestions from scanning existing tag names.
61 + * If no search term is given the 50 most popular tag names are provided.
61 * @param $searchTerm 62 * @param $searchTerm
62 * @return array 63 * @return array
63 */ 64 */
64 - public function getNameSuggestions($searchTerm) 65 + public function getNameSuggestions($searchTerm = false)
65 { 66 {
66 - if ($searchTerm === '') return []; 67 + $query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name');
67 - $query = $this->tag->where('name', 'LIKE', $searchTerm . '%')->groupBy('name')->orderBy('name', 'desc'); 68 +
69 + if ($searchTerm) {
70 + $query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
71 + } else {
72 + $query = $query->orderBy('count', 'desc')->take(50);
73 + }
74 +
68 $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); 75 $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
69 return $query->get(['name'])->pluck('name'); 76 return $query->get(['name'])->pluck('name');
70 } 77 }
71 78
72 /** 79 /**
73 * Get tag value suggestions from scanning existing tag values. 80 * Get tag value suggestions from scanning existing tag values.
81 + * If no search is given the 50 most popular values are provided.
82 + * Passing a tagName will only find values for a tags with a particular name.
74 * @param $searchTerm 83 * @param $searchTerm
75 * @param $tagName 84 * @param $tagName
76 * @return array 85 * @return array
77 */ 86 */
78 - public function getValueSuggestions($searchTerm, $tagName = false) 87 + public function getValueSuggestions($searchTerm = false, $tagName = false)
79 { 88 {
80 - if ($searchTerm === '') return []; 89 + $query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value');
81 - $query = $this->tag->where('value', 'LIKE', $searchTerm . '%')->groupBy('value')->orderBy('value', 'desc'); 90 +
82 - if ($tagName !== false) { 91 + if ($searchTerm) {
83 - $query = $query->where('name', '=', $tagName); 92 + $query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
93 + } else {
94 + $query = $query->orderBy('count', 'desc')->take(50);
84 } 95 }
96 +
97 + if ($tagName !== false) $query = $query->where('name', '=', $tagName);
98 +
85 $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); 99 $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
86 return $query->get(['value'])->pluck('value'); 100 return $query->get(['value'])->pluck('value');
87 } 101 }
......
...@@ -166,7 +166,7 @@ module.exports = function (ngApp, events) { ...@@ -166,7 +166,7 @@ module.exports = function (ngApp, events) {
166 }; 166 };
167 }]); 167 }]);
168 168
169 - ngApp.directive('tinymce', ['$timeout', function($timeout) { 169 + ngApp.directive('tinymce', ['$timeout', function ($timeout) {
170 return { 170 return {
171 restrict: 'A', 171 restrict: 'A',
172 scope: { 172 scope: {
...@@ -204,8 +204,8 @@ module.exports = function (ngApp, events) { ...@@ -204,8 +204,8 @@ module.exports = function (ngApp, events) {
204 scope.tinymce.extraSetups.push(tinyMceSetup); 204 scope.tinymce.extraSetups.push(tinyMceSetup);
205 205
206 // Custom tinyMCE plugins 206 // Custom tinyMCE plugins
207 - tinymce.PluginManager.add('customhr', function(editor) { 207 + tinymce.PluginManager.add('customhr', function (editor) {
208 - editor.addCommand('InsertHorizontalRule', function() { 208 + editor.addCommand('InsertHorizontalRule', function () {
209 var hrElem = document.createElement('hr'); 209 var hrElem = document.createElement('hr');
210 var cNode = editor.selection.getNode(); 210 var cNode = editor.selection.getNode();
211 var parentNode = cNode.parentNode; 211 var parentNode = cNode.parentNode;
...@@ -231,7 +231,7 @@ module.exports = function (ngApp, events) { ...@@ -231,7 +231,7 @@ module.exports = function (ngApp, events) {
231 } 231 }
232 }]); 232 }]);
233 233
234 - ngApp.directive('markdownInput', ['$timeout', function($timeout) { 234 + ngApp.directive('markdownInput', ['$timeout', function ($timeout) {
235 return { 235 return {
236 restrict: 'A', 236 restrict: 'A',
237 scope: { 237 scope: {
...@@ -255,7 +255,7 @@ module.exports = function (ngApp, events) { ...@@ -255,7 +255,7 @@ module.exports = function (ngApp, events) {
255 255
256 scope.$on('markdown-update', (event, value) => { 256 scope.$on('markdown-update', (event, value) => {
257 element.val(value); 257 element.val(value);
258 - scope.mdModel= value; 258 + scope.mdModel = value;
259 scope.mdChange(markdown(value)); 259 scope.mdChange(markdown(value));
260 }); 260 });
261 261
...@@ -263,7 +263,7 @@ module.exports = function (ngApp, events) { ...@@ -263,7 +263,7 @@ module.exports = function (ngApp, events) {
263 } 263 }
264 }]); 264 }]);
265 265
266 - ngApp.directive('markdownEditor', ['$timeout', function($timeout) { 266 + ngApp.directive('markdownEditor', ['$timeout', function ($timeout) {
267 return { 267 return {
268 restrict: 'A', 268 restrict: 'A',
269 link: function (scope, element, attrs) { 269 link: function (scope, element, attrs) {
...@@ -303,7 +303,7 @@ module.exports = function (ngApp, events) { ...@@ -303,7 +303,7 @@ module.exports = function (ngApp, events) {
303 if (now - lastScroll > scrollDebounceTime) { 303 if (now - lastScroll > scrollDebounceTime) {
304 setScrollHeights() 304 setScrollHeights()
305 } 305 }
306 - let scrollPercent = (input.scrollTop() / (inputScrollHeight-inputHeight)); 306 + let scrollPercent = (input.scrollTop() / (inputScrollHeight - inputHeight));
307 let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent; 307 let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent;
308 display.scrollTop(displayScrollY); 308 display.scrollTop(displayScrollY);
309 lastScroll = now; 309 lastScroll = now;
...@@ -342,10 +342,10 @@ module.exports = function (ngApp, events) { ...@@ -342,10 +342,10 @@ module.exports = function (ngApp, events) {
342 } 342 }
343 }]); 343 }]);
344 344
345 - ngApp.directive('toolbox', [function() { 345 + ngApp.directive('toolbox', [function () {
346 return { 346 return {
347 restrict: 'A', 347 restrict: 'A',
348 - link: function(scope, elem, attrs) { 348 + link: function (scope, elem, attrs) {
349 349
350 // Get common elements 350 // Get common elements
351 const $buttons = elem.find('[tab-button]'); 351 const $buttons = elem.find('[tab-button]');
...@@ -370,7 +370,7 @@ module.exports = function (ngApp, events) { ...@@ -370,7 +370,7 @@ module.exports = function (ngApp, events) {
370 setActive($content.first().attr('tab-content'), false); 370 setActive($content.first().attr('tab-content'), false);
371 371
372 // Handle tab button click 372 // Handle tab button click
373 - $buttons.click(function(e) { 373 + $buttons.click(function (e) {
374 let name = $(this).attr('tab-button'); 374 let name = $(this).attr('tab-button');
375 setActive(name, true); 375 setActive(name, true);
376 }); 376 });
...@@ -378,10 +378,10 @@ module.exports = function (ngApp, events) { ...@@ -378,10 +378,10 @@ module.exports = function (ngApp, events) {
378 } 378 }
379 }]); 379 }]);
380 380
381 - ngApp.directive('tagAutosuggestions', ['$http', function($http) { 381 + ngApp.directive('tagAutosuggestions', ['$http', function ($http) {
382 return { 382 return {
383 restrict: 'A', 383 restrict: 'A',
384 - link: function(scope, elem, attrs) { 384 + link: function (scope, elem, attrs) {
385 385
386 // Local storage for quick caching. 386 // Local storage for quick caching.
387 const localCache = {}; 387 const localCache = {};
...@@ -399,33 +399,26 @@ module.exports = function (ngApp, events) { ...@@ -399,33 +399,26 @@ module.exports = function (ngApp, events) {
399 let active = 0; 399 let active = 0;
400 400
401 // Listen to input events on autosuggest fields 401 // Listen to input events on autosuggest fields
402 - elem.on('input', '[autosuggest]', function(event) { 402 + elem.on('input focus', '[autosuggest]', function (event) {
403 let $input = $(this); 403 let $input = $(this);
404 let val = $input.val(); 404 let val = $input.val();
405 let url = $input.attr('autosuggest'); 405 let url = $input.attr('autosuggest');
406 let type = $input.attr('autosuggest-type'); 406 let type = $input.attr('autosuggest-type');
407 407
408 - // No suggestions until at least 3 chars
409 - if (val.length < 3) {
410 - if (isShowing) {
411 - $suggestionBox.hide();
412 - isShowing = false;
413 - }
414 - return;
415 - }
416 -
417 // Add name param to request if for a value 408 // Add name param to request if for a value
418 if (type.toLowerCase() === 'value') { 409 if (type.toLowerCase() === 'value') {
419 let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first(); 410 let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
420 let nameVal = $nameInput.val(); 411 let nameVal = $nameInput.val();
421 - if (nameVal === '') return; 412 + if (nameVal !== '') {
422 url += '?name=' + encodeURIComponent(nameVal); 413 url += '?name=' + encodeURIComponent(nameVal);
423 - console.log(url); 414 + }
424 } 415 }
425 416
426 let suggestionPromise = getSuggestions(val.slice(0, 3), url); 417 let suggestionPromise = getSuggestions(val.slice(0, 3), url);
427 suggestionPromise.then(suggestions => { 418 suggestionPromise.then(suggestions => {
428 - if (val.length > 2) { 419 + if (val.length === 0) {
420 + displaySuggestions($input, suggestions.slice(0, 6));
421 + } else {
429 suggestions = suggestions.filter(item => { 422 suggestions = suggestions.filter(item => {
430 return item.toLowerCase().indexOf(val.toLowerCase()) !== -1; 423 return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
431 }).slice(0, 4); 424 }).slice(0, 4);
...@@ -436,12 +429,19 @@ module.exports = function (ngApp, events) { ...@@ -436,12 +429,19 @@ module.exports = function (ngApp, events) {
436 429
437 // Hide autosuggestions when input loses focus. 430 // Hide autosuggestions when input loses focus.
438 // Slight delay to allow clicks. 431 // Slight delay to allow clicks.
439 - elem.on('blur', '[autosuggest]', function(event) { 432 + let lastFocusTime = 0;
433 + elem.on('blur', '[autosuggest]', function (event) {
434 + let startTime = Date.now();
440 setTimeout(() => { 435 setTimeout(() => {
436 + if (lastFocusTime < startTime) {
441 $suggestionBox.hide(); 437 $suggestionBox.hide();
442 isShowing = false; 438 isShowing = false;
439 + }
443 }, 200) 440 }, 200)
444 }); 441 });
442 + elem.on('focus', '[autosuggest]', function (event) {
443 + lastFocusTime = Date.now();
444 + });
445 445
446 elem.on('keydown', '[autosuggest]', function (event) { 446 elem.on('keydown', '[autosuggest]', function (event) {
447 if (!isShowing) return; 447 if (!isShowing) return;
...@@ -451,12 +451,12 @@ module.exports = function (ngApp, events) { ...@@ -451,12 +451,12 @@ module.exports = function (ngApp, events) {
451 451
452 // Down arrow 452 // Down arrow
453 if (event.keyCode === 40) { 453 if (event.keyCode === 40) {
454 - let newActive = (active === suggestCount-1) ? 0 : active + 1; 454 + let newActive = (active === suggestCount - 1) ? 0 : active + 1;
455 changeActiveTo(newActive, suggestionElems); 455 changeActiveTo(newActive, suggestionElems);
456 } 456 }
457 // Up arrow 457 // Up arrow
458 else if (event.keyCode === 38) { 458 else if (event.keyCode === 38) {
459 - let newActive = (active === 0) ? suggestCount-1 : active - 1; 459 + let newActive = (active === 0) ? suggestCount - 1 : active - 1;
460 changeActiveTo(newActive, suggestionElems); 460 changeActiveTo(newActive, suggestionElems);
461 } 461 }
462 // Enter or tab key 462 // Enter or tab key
...@@ -482,6 +482,7 @@ module.exports = function (ngApp, events) { ...@@ -482,6 +482,7 @@ module.exports = function (ngApp, events) {
482 482
483 // Display suggestions on a field 483 // Display suggestions on a field
484 let prevSuggestions = []; 484 let prevSuggestions = [];
485 +
485 function displaySuggestions($input, suggestions) { 486 function displaySuggestions($input, suggestions) {
486 487
487 // Hide if no suggestions 488 // Hide if no suggestions
...@@ -518,7 +519,8 @@ module.exports = function (ngApp, events) { ...@@ -518,7 +519,8 @@ module.exports = function (ngApp, events) {
518 if (i === 0) { 519 if (i === 0) {
519 suggestion.className = 'active' 520 suggestion.className = 'active'
520 active = 0; 521 active = 0;
521 - }; 522 + }
523 + ;
522 $suggestionBox[0].appendChild(suggestion); 524 $suggestionBox[0].appendChild(suggestion);
523 } 525 }
524 526
...@@ -537,17 +539,17 @@ module.exports = function (ngApp, events) { ...@@ -537,17 +539,17 @@ module.exports = function (ngApp, events) {
537 // Get suggestions & cache 539 // Get suggestions & cache
538 function getSuggestions(input, url) { 540 function getSuggestions(input, url) {
539 let hasQuery = url.indexOf('?') !== -1; 541 let hasQuery = url.indexOf('?') !== -1;
540 - let searchUrl = url + (hasQuery?'&':'?') + 'search=' + encodeURIComponent(input); 542 + let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input);
541 543
542 // Get from local cache if exists 544 // Get from local cache if exists
543 - if (localCache[searchUrl]) { 545 + if (typeof localCache[searchUrl] !== 'undefined') {
544 return new Promise((resolve, reject) => { 546 return new Promise((resolve, reject) => {
545 - resolve(localCache[input]); 547 + resolve(localCache[searchUrl]);
546 }); 548 });
547 } 549 }
548 550
549 - return $http.get(searchUrl).then((response) => { 551 + return $http.get(searchUrl).then(response => {
550 - localCache[input] = response.data; 552 + localCache[searchUrl] = response.data;
551 return response.data; 553 return response.data;
552 }); 554 });
553 } 555 }
......