return response()->json($suggestions);
}
+ /**
+ * Get tag value suggestions from a given search term.
+ * @param Request $request
+ */
+ public function getValueSuggestions(Request $request)
+ {
+ $searchTerm = $request->get('search');
+ $suggestions = $this->tagRepo->getValueSuggestions($searchTerm);
+ return response()->json($suggestions);
+ }
}
// Tag routes (AJAX)
Route::group(['prefix' => 'ajax/tags'], function() {
Route::get('/get/{entityType}/{entityId}', 'TagController@getForEntity');
- Route::get('/suggest', 'TagController@getNameSuggestions');
+ Route::get('/suggest/names', 'TagController@getNameSuggestions');
+ Route::get('/suggest/values', 'TagController@getValueSuggestions');
Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity');
});
return $query->get(['name'])->pluck('name');
}
+ /**
+ * Get tag value suggestions from scanning existing tag values.
+ * @param $searchTerm
+ * @return array
+ */
+ public function getValueSuggestions($searchTerm)
+ {
+ if ($searchTerm === '') return [];
+ $query = $this->tag->where('value', 'LIKE', $searchTerm . '%')->groupBy('value')->orderBy('value', 'desc');
+ $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
+ return $query->get(['value'])->pluck('value');
+ }
/**
* Save an array of tags to an entity
* @param Entity $entity
}
}]);
-};
\ No newline at end of file
+ ngApp.directive('autosuggestions', ['$http', function($http) {
+ return {
+ restrict: 'A',
+ link: function(scope, elem, attrs) {
+
+ // Local storage for quick caching.
+ const localCache = {};
+
+ // Create suggestion element
+ const suggestionBox = document.createElement('ul');
+ suggestionBox.className = 'suggestion-box';
+ suggestionBox.style.position = 'absolute';
+ suggestionBox.style.display = 'none';
+ const $suggestionBox = $(suggestionBox);
+
+ // General state tracking
+ let isShowing = false;
+ let currentInput = false;
+ let active = 0;
+
+ // Listen to input events on autosuggest fields
+ elem.on('input', '[autosuggest]', function(event) {
+ let $input = $(this);
+ let val = $input.val();
+ let url = $input.attr('autosuggest');
+ // No suggestions until at least 3 chars
+ if (val.length < 3) {
+ if (isShowing) {
+ $suggestionBox.hide();
+ isShowing = false;
+ }
+ return;
+ };
+
+ let suggestionPromise = getSuggestions(val.slice(0, 3), url);
+ suggestionPromise.then((suggestions) => {
+ if (val.length > 2) {
+ suggestions = suggestions.filter((item) => {
+ return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
+ }).slice(0, 4);
+ displaySuggestions($input, suggestions);
+ }
+ });
+ });
+
+ // Hide autosuggestions when input loses focus.
+ // Slight delay to allow clicks.
+ elem.on('blur', '[autosuggest]', function(event) {
+ setTimeout(() => {
+ $suggestionBox.hide();
+ isShowing = false;
+ }, 200)
+ });
+
+ elem.on('keydown', '[autosuggest]', function (event) {
+ if (!isShowing) return;
+
+ let suggestionElems = suggestionBox.childNodes;
+ let suggestCount = suggestionElems.length;
+
+ // Down arrow
+ if (event.keyCode === 40) {
+ let newActive = (active === suggestCount-1) ? 0 : active + 1;
+ changeActiveTo(newActive, suggestionElems);
+ }
+ // Up arrow
+ else if (event.keyCode === 38) {
+ let newActive = (active === 0) ? suggestCount-1 : active - 1;
+ changeActiveTo(newActive, suggestionElems);
+ }
+ // Enter key
+ else if (event.keyCode === 13) {
+ let text = suggestionElems[active].textContent;
+ currentInput[0].value = text;
+ currentInput.focus();
+ $suggestionBox.hide();
+ isShowing = false;
+ event.preventDefault();
+ return false;
+ }
+ });
+
+ // Change the active suggestion to the given index
+ function changeActiveTo(index, suggestionElems) {
+ suggestionElems[active].className = '';
+ active = index;
+ suggestionElems[active].className = 'active';
+ }
+
+ // Display suggestions on a field
+ let prevSuggestions = [];
+ function displaySuggestions($input, suggestions) {
+
+ // Hide if no suggestions
+ if (suggestions.length === 0) {
+ $suggestionBox.hide();
+ isShowing = false;
+ prevSuggestions = suggestions;
+ return;
+ }
+
+ // Otherwise show and attach to input
+ if (!isShowing) {
+ $suggestionBox.show();
+ isShowing = true;
+ }
+ if ($input !== currentInput) {
+ $suggestionBox.detach();
+ $input.after($suggestionBox);
+ currentInput = $input;
+ }
+
+ // Return if no change
+ if (prevSuggestions.join() === suggestions.join()) {
+ prevSuggestions = suggestions;
+ return;
+ }
+
+ // Build suggestions
+ $suggestionBox[0].innerHTML = '';
+ for (let i = 0; i < suggestions.length; i++) {
+ var suggestion = document.createElement('li');
+ suggestion.textContent = suggestions[i];
+ suggestion.onclick = suggestionClick;
+ if (i === 0) {
+ suggestion.className = 'active'
+ active = 0;
+ };
+ $suggestionBox[0].appendChild(suggestion);
+ }
+
+ prevSuggestions = suggestions;
+ }
+
+ // Suggestion click event
+ function suggestionClick(event) {
+ let text = this.textContent;
+ currentInput[0].value = text;
+ currentInput.focus();
+ $suggestionBox.hide();
+ isShowing = false;
+ };
+
+ // Get suggestions & cache
+ function getSuggestions(input, url) {
+ let searchUrl = url + '?search=' + encodeURIComponent(input);
+
+ // Get from local cache if exists
+ if (localCache[searchUrl]) {
+ return new Promise((resolve, reject) => {
+ resolve(localCache[input]);
+ });
+ }
+
+ return $http.get(searchUrl).then((response) => {
+ localCache[input] = response.data;
+ return response.data;
+ });
+ }
+
+ }
+ }
+ }]);
+};
+
+
+
+
+
+
+
+
+
+
+
+
+
+
.tags td {
padding-right: $-s;
padding-top: $-s;
+ position: relative;
}
button.pos {
position: absolute;
}
.tag {
padding: $-s;
+ }
+}
+.suggestion-box {
+ position: absolute;
+ background-color: #FFF;
+ border: 1px solid #BBB;
+ box-shadow: $bs-light;
+ list-style: none;
+ z-index: 100;
+ padding: 0;
+ margin: 0;
+ border-radius: 3px;
+ li {
+ display: block;
+ padding: $-xs $-s;
+ border-bottom: 1px solid #DDD;
+ &:last-child {
+ border-bottom: 0;
+ }
+ &.active {
+ background-color: #EEE;
+ }
}
}
\ No newline at end of file
@section('content')
<div class="flex-fill flex">
- <form action="{{$page->getUrl()}}" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
+ <form action="{{$page->getUrl()}}" autocomplete="off" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
@if(!isset($isDraft))
<input type="hidden" name="_method" value="PUT">
@endif
<div toolbox class="floating-toolbox">
+
<div class="tabs primary-background-light">
<span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>
<span tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
</div>
+
<div tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
<h4>Page Tags</h4>
<div class="padded tags">
<p class="muted small">Add some tags to better categorise your content. <br> You can assign a value to a tag for more in-depth organisation.</p>
- <table class="no-style" style="width: 100%;">
+ <table class="no-style" autosuggestions style="width: 100%;">
<tbody ui-sortable="sortOptions" ng-model="tags" >
<tr ng-repeat="tag in tags track by $index">
<td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
- <td><input class="outline" ng-attr-name="tags[@{{$index}}][name]" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag"></td>
- <td><input class="outline" ng-attr-name="tags[@{{$index}}][value]" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag Value (Optional)"></td>
+ <td><input autosuggest="/ajax/tags/suggest/names" class="outline" ng-attr-name="tags[@{{$index}}][name]" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag"></td>
+ <td><input autosuggest="/ajax/tags/suggest/values" class="outline" ng-attr-name="tags[@{{$index}}][value]" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag Value (Optional)"></td>
<td width="10" ng-show="tags.length != 1" class="text-center text-neg" style="padding: 0;" ng-click="removeTag(tag)"><i class="zmdi zmdi-close"></i></td>
</tr>
</tbody>
</table>
</div>
</div>
+
</div>
\ No newline at end of file
$attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'plans']));
$page = $this->getPageWithTags($attrs);
- $this->asAdmin()->get('/ajax/tags/suggest?search=dog')->seeJsonEquals([]);
- $this->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country', 'county']);
- $this->get('/ajax/tags/suggest?search=cou')->seeJsonEquals(['country', 'county']);
- $this->get('/ajax/tags/suggest?search=pla')->seeJsonEquals(['planet', 'plans']);
+ $this->asAdmin()->get('/ajax/tags/suggest/names?search=dog')->seeJsonEquals([]);
+ $this->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country', 'county']);
+ $this->get('/ajax/tags/suggest/names?search=cou')->seeJsonEquals(['country', 'county']);
+ $this->get('/ajax/tags/suggest/names?search=pla')->seeJsonEquals(['planet', 'plans']);
+ }
+
+ public function test_tag_value_suggestions()
+ {
+ // Create some tags with similar values to test with
+ $attrs = collect();
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'country', 'value' => 'cats']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color', 'value' => 'cattery']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'city', 'value' => 'castle']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'county', 'value' => 'dog']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'planet', 'value' => 'catapult']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'plans', 'value' => 'dodgy']));
+ $page = $this->getPageWithTags($attrs);
+
+ $this->asAdmin()->get('/ajax/tags/suggest/values?search=ora')->seeJsonEquals([]);
+ $this->get('/ajax/tags/suggest/values?search=cat')->seeJsonEquals(['cats', 'cattery', 'catapult']);
+ $this->get('/ajax/tags/suggest/values?search=do')->seeJsonEquals(['dog', 'dodgy']);
+ $this->get('/ajax/tags/suggest/values?search=cas')->seeJsonEquals(['castle']);
}
public function test_entity_permissions_effect_tag_suggestions()