Browse Source

Add autocomplete for topic tags inputs

merge-requests/68/head
Shane Moore 6 years ago
committed by Deimos
parent
commit
236c9b87e6
  1. 4
      tildes/scss/modules/_form.scss
  2. 10
      tildes/scss/modules/_menu.scss
  3. 1
      tildes/scss/spectre-0.5.1/spectre.scss
  4. 1
      tildes/scss/styles.scss
  5. 31
      tildes/scss/themes/_theme_base.scss
  6. 30
      tildes/static/js/behaviors/autocomplete-chip-clear.js
  7. 126
      tildes/static/js/behaviors/autocomplete-input.js
  8. 68
      tildes/static/js/behaviors/autocomplete-menu-item.js
  9. 11
      tildes/static/js/behaviors/autocomplete-menu.js
  10. 29
      tildes/tildes/templates/macros/forms.jinja2

4
tildes/scss/modules/_form.scss

@ -81,3 +81,7 @@ textarea.form-input {
.form-search .form-input {
margin-right: 0.4rem;
}
#new-topic {
margin-bottom: 200px;
}

10
tildes/scss/modules/_menu.scss

@ -0,0 +1,10 @@
// Copyright (c) 2019 Tildes contributors <code@tildes.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
.menu {
.menu-item {
& > a:hover, & > a:focus {
background-color: transparent;
}
}
}

1
tildes/scss/spectre-0.5.1/spectre.scss

@ -45,4 +45,5 @@
// Utility classes
@import "animations";
@import "autocomplete";
@import "utilities";

1
tildes/scss/styles.scss

@ -19,6 +19,7 @@
@import 'modules/link';
@import 'modules/listing';
@import 'modules/logged-in-user';
@import 'modules/menu';
@import 'modules/message';
@import 'modules/nav';
@import 'modules/pagination';

31
tildes/scss/themes/_theme_base.scss

@ -101,6 +101,12 @@
border-color: map-get($theme, "foreground-highlight");
}
.form-autocomplete {
.menu {
background-color: map-get($theme, "background-secondary");
}
}
.btn {
color: map-get($theme, "button");
background-color: transparent;
@ -195,6 +201,27 @@
@include labelbutton(map-get($theme, "comment-label-malice"));
}
.chip {
background-color: map-get($theme, "background-secondary");
color: map-get($theme, "foreground-highlight");
&.active {
background-color: map-get($theme, "button");
$active-color: #fff;
$is-active-bg-light: lightness(map-get($theme, "button")) > 50;
@if ($is-active-bg-light) {
$active-color: #000;
}
color: $active-color;
.btn {
color: $active-color;
}
}
}
.comment-branch-counter {
color: map-get($theme, "foreground-secondary");
}
@ -281,6 +308,10 @@
color: map-get($theme, "foreground-secondary");
}
.form-autocomplete .form-autocomplete-input .form-input {
border-color: transparent;
}
.form-input {
color: map-get($theme, "foreground-primary");
background-color: map-get($theme, "background-input");

30
tildes/static/js/behaviors/autocomplete-chip-clear.js

@ -0,0 +1,30 @@
// Copyright (c) 2019 Tildes contributors <code@tildes.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
$.onmount('[data-js-autocomplete-chip-clear]', function() {
function clearChip($chip) {
var $tagsHiddenInput = $('[data-js-autocomplete-hidden-input]');
var $autocompleteInput = $('[data-js-autocomplete-input]');
var textToReplace = new RegExp($chip.text() + ',');
$tagsHiddenInput.val($tagsHiddenInput.val().replace(textToReplace, ''));
$chip.remove();
$autocompleteInput.focus();
}
$(this).click(function(event) {
event.preventDefault();
clearChip($(this).parent());
});
$(this).keydown(function(event) {
switch (event.key) {
case 'Backspace':
event.preventDefault();
clearChip($(this).parent());
break;
default:
break;
}
});
});

126
tildes/static/js/behaviors/autocomplete-input.js

@ -0,0 +1,126 @@
// Copyright (c) 2019 Tildes contributors <code@tildes.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
$.onmount('[data-js-autocomplete-input]', function() {
function addChip($input) {
var $autocompleteContainer = $input.parents('[data-js-autocomplete-container]').first();
var $chips = $autocompleteContainer.find('[data-js-autocomplete-chips]').first();
var $tagsHiddenInput = $('[data-js-autocomplete-hidden-input]');
$input.val().split(',').map(function(tag) {
return tag.trim();
}).filter(function(tag) {
return tag !== '';
}).forEach(function(tag) {
if (!$tagsHiddenInput.val().match(new RegExp('(^|,)' + tag + ','))) {
var clearIcon = document.createElement('a');
clearIcon.classList.add('btn');
clearIcon.classList.add('btn-clear');
clearIcon.setAttribute('data-js-autocomplete-chip-clear', '');
clearIcon.setAttribute('aria-label', 'Close');
clearIcon.setAttribute('role', 'button');
clearIcon.setAttribute('tabindex', $chips.children().length);
var $chip = $(document.createElement('div'));
$chip.addClass('chip');
$chip.html(tag);
$chip.append(clearIcon);
$chips.append($chip);
$tagsHiddenInput.val($tagsHiddenInput.val() + tag + ',');
}
});
$autocompleteContainer.find('[data-js-autocomplete-input]').val('');
$autocompleteContainer.find('[data-js-autocomplete-suggestions]').html('');
$.onmount();
}
if ($(this).val() !== '') {
addChip($(this));
}
$(this).focus(function(event) {
var $autocompleteContainer = $(this).parents('[data-js-autocomplete-container]').first();
var $chips = $autocompleteContainer.find('[data-js-autocomplete-chips]').first();
$chips.children().removeClass('active');
});
$(this).keydown(function(event) {
switch (event.key) {
case 'Escape':
$('[data-js-autocomplete-menu]').remove();
$(this).blur();
break;
case ',':
case 'Enter':
event.preventDefault();
addChip($(this));
break;
case 'ArrowDown':
event.preventDefault();
var $autocompleteMenu = $('[data-js-autocomplete-menu]').first();
var $nextActiveItem = $autocompleteMenu.children('.menu-item').first();
$nextActiveItem.children('[data-js-autocomplete-menu-item]').first().focus();
break;
case 'ArrowUp':
event.preventDefault();
var $autocompleteMenu = $('[data-js-autocomplete-menu]').first();
var $nextActiveItem = $autocompleteMenu.children('.menu-item').last();
$nextActiveItem.children('[data-js-autocomplete-menu-item]').first().focus();
break;
case 'Backspace':
if ($(this).val() === '') {
var $autocompleteContainer = $(this).parents('[data-js-autocomplete-container]').first();
var $chips = $autocompleteContainer.find('[data-js-autocomplete-chips]').first();
var $tagsHiddenInput = $("[data-js-autocomplete-hidden-input]");
var $lastChip = $chips.children().last();
$(this).blur();
if (!$lastChip.hasClass('active')) {
$lastChip.addClass("active");
$lastChip.children('[data-js-autocomplete-chip-clear]').first().focus();
}
}
break;
}
});
$(this).keyup(function(event) {
var $this = $(this);
var $autocompleteMenu = $('[data-js-autocomplete-menu]');
if ($autocompleteMenu) {
$autocompleteMenu.remove();
}
if ($this.val() === '') {
return;
}
var $tagsHiddenInput = $('[data-js-autocomplete-hidden-input]');
var suggestions = $this.data('js-autocomplete-input').filter(function(suggestion) {
return suggestion.startsWith($this.val()) &&
!$tagsHiddenInput.val().match(new RegExp('(^|,)' + suggestion + ','));
});
if (suggestions.length === 0) {
return;
}
var $autocompleteSuggestions = $('[data-js-autocomplete-suggestions]');
$autocompleteMenu = $('<ol class="menu" data-js-autocomplete-menu>');
suggestions.forEach(function(suggestion) {
$autocompleteMenu.append(
'<li class="menu-item">' +
'<a href="#" data-js-autocomplete-menu-item>' +
'<div class="tile tile-centered">' +
'<div class="tile-content">' +
suggestion +
'</div>' +
'</div>' +
'</a>' +
'</li>');
});
$autocompleteSuggestions.append($autocompleteMenu);
$.onmount();
});
});

68
tildes/static/js/behaviors/autocomplete-menu-item.js

@ -0,0 +1,68 @@
// Copyright (c) 2019 Tildes contributors <code@tildes.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
$.onmount('[data-js-autocomplete-menu-item]', function() {
function addChip($menuItem) {
var $autocompleteContainer = $menuItem.parents('[data-js-autocomplete-container]').first();
var $clickedSuggestion = $menuItem.find('.tile > .tile-content').first();
var clickedSuggestionText = $clickedSuggestion.html().trim();
var $tagsHiddenInput = $('[data-js-autocomplete-hidden-input]');
var $autocompleteInput = $("[data-js-autocomplete-input]");
if (!$tagsHiddenInput.val().includes(clickedSuggestionText + ',')) {
var $chips = $autocompleteContainer.find('[data-js-autocomplete-chips]').first();
var clearIcon = document.createElement('a');
clearIcon.classList.add('btn');
clearIcon.classList.add('btn-clear');
clearIcon.setAttribute('data-js-autocomplete-chip-clear', '');
clearIcon.setAttribute('aria-label', 'Close');
clearIcon.setAttribute('role', 'button');
clearIcon.setAttribute("tabindex", $chips.children().length);
var $chip = $(document.createElement('div'));
$chip.addClass('chip');
$chip.html(clickedSuggestionText);
$chip.append(clearIcon);
$chips.append($chip);
$tagsHiddenInput.val($tagsHiddenInput.val() + clickedSuggestionText + ',');
}
$autocompleteContainer.find('[data-js-autocomplete-input]').val('');
$autocompleteContainer.find('[data-js-autocomplete-suggestions]').html('');
$autocompleteInput.focus();
$.onmount();
}
$(this).click(function(event) {
event.preventDefault();
addChip($(this), event);
});
$(this).keydown(function(event) {
switch (event.key) {
case 'Escape':
$('[data-js-autocomplete-menu]').parent().remove();
break;
case 'Enter':
event.preventDefault();
addChip($(this));
break;
case 'ArrowDown':
event.preventDefault();
var $nextActiveItem = $(this).parent().next();
$nextActiveItem.children('[data-js-autocomplete-menu-item]').first().focus();
break;
case 'ArrowUp':
event.preventDefault();
var $nextActiveItem = $(this).parent().prev();
$nextActiveItem.children('[data-js-autocomplete-menu-item]').first().focus();
break;
default:
break;
}
});
});

11
tildes/static/js/behaviors/autocomplete-menu.js

@ -0,0 +1,11 @@
// Copyright (c) 2019 Tildes contributors <code@tildes.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
$.onmount('[data-js-autocomplete-menu]', function() {
var $autocompleteContainer = $(this).parents('[data-js-autocomplete-container]').first();
var $chips = $autocompleteContainer.find('[data-js-autocomplete-chips]').first();
$(this).children('[data-js-autocomplete-menu-item]').each(function(index, $menuItem) {
$menuItem.setAttribute('tabindex', $chips.children().length + index);
});
});

29
tildes/tildes/templates/macros/forms.jinja2

@ -17,19 +17,30 @@
{% endmacro %}
{% macro topic_tagging(value=None, auto_focus=False) %}
<div class="form-group">
<div class="form-autocomplete form-group" data-js-autocomplete-container>
<label class="form-label" for="tags">
<span>Tags (optional, comma-separated)</span>
<a href="https://docs.tildes.net/topic-tagging" target="_blank" tabindex="-1">Tagging help</a>
</label>
<input
class="form-input"
id="tags"
name="tags"
placeholder='Tags (like "music, soundtrack, full album")'
{% if value %} value="{{ value }}"{% endif %}
{% if auto_focus %}data-js-auto-focus{% endif %}
>
<div class="form-autocomplete-input form-input">
<div class="chips" data-js-autocomplete-chips></div>
<input
class="form-input"
id="tags"
type="text"
autocorrect="off"
autocomplete="off"
autocapitalize="none"
spellcheck="false"
name="query"
placeholder='Tags (like "music, soundtrack, full album")'
data-js-autocomplete-input=""
{% if value %} value="{{ value }}"{% endif %}
{% if auto_focus %}data-js-auto-focus{% endif %}
>
</div>
<div class="autocomplete" data-js-autocomplete-suggestions></div>
<input name="tags" type="hidden" value="" data-js-autocomplete-hidden-input>
</div>
{% endmacro %}

Loading…
Cancel
Save