Added tag autosuggestion when no input provided
Shows the most popular tag names/values. As requested on #121
Showing
3 changed files
with
60 additions
and
44 deletions
| ... | @@ -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 | } | ... | ... |
-
Please register or sign in to post a comment