mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-21 05:09:15 +00:00
Rework TagListInputComponent to integrate autocomplete
The component now will try to load a list of existing tag if you give an `autocomplete_url`. I tried to keep the tag input and the autocomplete functionality decoupled but is wasn't really possible. Instead I opted to sub class the Autocomplete stimulus controller, but it only gets initialised if we pass an `autocomplete_url`.
This commit is contained in:
@@ -6,18 +6,18 @@ class TagListInputComponent < ViewComponent::Base
|
||||
only_one: false,
|
||||
aria_label: nil,
|
||||
hidden_field_data_options: {},
|
||||
input_field_data_options: {})
|
||||
autocomplete_url: "")
|
||||
@name = name
|
||||
@tags = tags
|
||||
@placeholder = placeholder
|
||||
@only_one = only_one
|
||||
@aria_label_option = aria_label ? { 'aria-label': aria_label } : {}
|
||||
@hidden_field_data_options = hidden_field_data_options
|
||||
@input_field_data_options = input_field_data_options
|
||||
@autocomplete_url = autocomplete_url
|
||||
end
|
||||
|
||||
attr_reader :name, :tags, :placeholder, :only_one, :aria_label_option,
|
||||
:hidden_field_data_options, :input_field_data_options
|
||||
:hidden_field_data_options, :autocomplete_url
|
||||
|
||||
private
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
%div{ "data-controller": "tag-list-input", "data-tag-list-input-only-one-value": "#{only_one}" }
|
||||
%div{ "data-controller": "tag-list-input", "data-tag-list-input-only-one-value": "#{only_one}", "data-tag-list-input-url-value": autocomplete_url, "data-action": "autocomplete.change->tag-list-input#addTag" }
|
||||
.tags-input
|
||||
.tags
|
||||
- # We use display:none instead of hidden field, so changes to the value can be picked up by the bulkFormController
|
||||
@@ -20,9 +20,8 @@
|
||||
nil,
|
||||
{ class: "input",
|
||||
placeholder: placeholder,
|
||||
"data-action": "keydown.enter->tag-list-input#addTag keyup->tag-list-input#filterInput blur->tag-list-input#addTag",
|
||||
"data-tag-list-input-target": "newTag",
|
||||
"data-action": "keydown.enter->tag-list-input#keyboardAddTag keyup->tag-list-input#filterInput blur->tag-list-input#onBlur focus->tag-list-input#onInputChange",
|
||||
"data-tag-list-input-target": "input",
|
||||
**aria_label_option,
|
||||
style: "display: #{display};"}.merge(input_field_data_options))
|
||||
- if content?
|
||||
= content
|
||||
style: "display: #{display};"})
|
||||
%ul.suggestion-list{ "data-tag-list-input-target": "results" }
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { Controller } from "stimulus";
|
||||
import { Autocomplete } from "stimulus-autocomplete";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["tagList", "newTag", "template", "list"];
|
||||
// Extend the stimulus-autocomplete controller, so we can load tag with existing rules
|
||||
// The autocomplete functionality is only loaded if the url value is set
|
||||
// For more informatioon on "stimulus-autocomplete", see:
|
||||
// https://github.com/afcapel/stimulus-autocomplete/tree/main
|
||||
//
|
||||
export default class extends Autocomplete {
|
||||
static targets = ["tagList", "input", "template", "list"];
|
||||
static values = { onlyOne: Boolean };
|
||||
|
||||
addTag(event) {
|
||||
// prevent hotkey form submitting the form (default action for "enter" key)
|
||||
event.preventDefault();
|
||||
connect() {
|
||||
// Don't start autocomplete controller if we don't have an url
|
||||
if (this.urlValue.length == 0) return;
|
||||
|
||||
const newTagName = this.newTagTarget.value.trim().replaceAll(" ", "-");
|
||||
super.connect();
|
||||
}
|
||||
|
||||
addTag(event) {
|
||||
const newTagName = this.inputTarget.value.trim().replaceAll(" ", "-");
|
||||
if (newTagName.length == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -18,7 +27,7 @@ export default class extends Controller {
|
||||
const index = tags.indexOf(newTagName);
|
||||
if (index != -1) {
|
||||
// highlight the value in red
|
||||
this.newTagTarget.classList.add("tag-error");
|
||||
this.inputTarget.classList.add("tag-error");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,14 +47,23 @@ export default class extends Controller {
|
||||
this.listTarget.appendChild(newTagElement);
|
||||
|
||||
// Clear new tag value
|
||||
this.newTagTarget.value = "";
|
||||
this.inputTarget.value = "";
|
||||
|
||||
// hide tag input if limited to one tag
|
||||
if (this.tagListTarget.value.split(",").length == 1 && this.onlyOneValue == true) {
|
||||
this.newTagTarget.style.display = "none";
|
||||
this.inputTarget.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
keyboardAddTag(event) {
|
||||
// prevent hotkey form submitting the form (default action for "enter" key)
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this.addTag();
|
||||
}
|
||||
|
||||
removeTag(event) {
|
||||
// Text to remove
|
||||
const tagName = event.srcElement.previousElementSibling.textContent;
|
||||
@@ -62,14 +80,14 @@ export default class extends Controller {
|
||||
|
||||
// Make sure the tag input is displayed
|
||||
if (this.tagListTarget.value.length == 0) {
|
||||
this.newTagTarget.style.display = "block";
|
||||
this.inputTarget.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
filterInput(event) {
|
||||
// clear error class if key is not enter
|
||||
if (event.key !== "Enter") {
|
||||
this.newTagTarget.classList.remove("tag-error");
|
||||
this.inputTarget.classList.remove("tag-error");
|
||||
}
|
||||
|
||||
// Strip comma from tag name
|
||||
@@ -77,4 +95,53 @@ export default class extends Controller {
|
||||
event.srcElement.value = event.srcElement.value.replace(",", "");
|
||||
}
|
||||
}
|
||||
|
||||
// Add tag if we don't have an autocomplete list open
|
||||
onBlur() {
|
||||
// check if we have any autocomplete results
|
||||
if (this.resultsTarget.childElementCount == 0) this.addTag();
|
||||
}
|
||||
|
||||
// Override original to add tag filtering
|
||||
replaceResults(html) {
|
||||
const filteredHtml = this.#filterResults(html);
|
||||
|
||||
// Don't show result if we don't have anything to show
|
||||
if (filteredHtml.length == 0) return;
|
||||
|
||||
super.replaceResults(filteredHtml);
|
||||
}
|
||||
|
||||
// Override original to all empty query, which will return all existing tags
|
||||
onInputChange = () => {
|
||||
if (this.urlValue.length == 0) return;
|
||||
|
||||
if (this.hasHiddenTarget) this.hiddenTarget.value = "";
|
||||
|
||||
const query = this.inputTarget.value.trim();
|
||||
if (query.length >= this.minLengthValue) {
|
||||
this.fetchResults(query);
|
||||
} else {
|
||||
this.hideAndRemoveOptions();
|
||||
}
|
||||
};
|
||||
|
||||
//private
|
||||
|
||||
#filterResults(html) {
|
||||
const existingTags = this.tagListTarget.value.split(",");
|
||||
// Parse the HTML
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
const lis = doc.getElementsByTagName("li");
|
||||
// Filter
|
||||
let filteredHtml = "";
|
||||
for (let li of lis) {
|
||||
if (!existingTags.includes(li.dataset.autocompleteValue)) {
|
||||
filteredHtml += li.outerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
return filteredHtml;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class VariantTagListInputComponent < ViewComponent::Base
|
||||
def initialize(name:, variant:, tag_list_input_component: TagListInputComponent,
|
||||
placeholder: I18n.t("components.tag_list_input.default_placeholder"),
|
||||
aria_label: nil)
|
||||
@tag_list_input_component = tag_list_input_component
|
||||
@variant = variant
|
||||
@name = name
|
||||
@tags = variant.tag_list
|
||||
@placeholder = placeholder
|
||||
@only_one = false
|
||||
@aria_label = aria_label
|
||||
end
|
||||
|
||||
attr_reader :tag_list_input_component, :variant, :name, :tags, :placeholder, :only_one,
|
||||
:aria_label
|
||||
|
||||
private
|
||||
|
||||
def autocomplete_url
|
||||
"/admin/tag_rules/variant_tag_rules?enterprise_id=#{variant.supplier_id}"
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
%div{ "data-controller": "variant-tag-list-input", "data-variant-tag-list-input-url-value": autocomplete_url }
|
||||
= render tag_list_input_component.new(name:,
|
||||
tags:,
|
||||
placeholder:,
|
||||
aria_label:,
|
||||
only_one:,
|
||||
input_field_data_options: { "data-variant-tag-list-input-target": "input" },
|
||||
hidden_field_data_options: { "data-variant-tag-list-input-target": "tags" }) do
|
||||
%ul.suggestion-list{ "data-variant-tag-list-input-target": "results" }
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Autocomplete } from "stimulus-autocomplete";
|
||||
|
||||
// Extend the stimulus-autocomplete controller, so we can add the ability to filter out existing
|
||||
// tag
|
||||
export default class extends Autocomplete {
|
||||
static targets = ["tags"];
|
||||
|
||||
replaceResults(html) {
|
||||
const filteredHtml = this.#filterResults(html);
|
||||
super.replaceResults(filteredHtml);
|
||||
}
|
||||
|
||||
//private
|
||||
|
||||
#filterResults(html) {
|
||||
const existingTags = this.tagsTarget.value.split(",");
|
||||
// Parse the HTML
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
const lis = doc.getElementsByTagName("li");
|
||||
// Filter
|
||||
let filteredHtml = "";
|
||||
for (let li of lis) {
|
||||
if (!existingTags.includes(li.dataset.autocompleteValue)) {
|
||||
filteredHtml += li.outerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
return filteredHtml;
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@
|
||||
= error_message_on variant, :tax_category
|
||||
- if feature?(:variant_tag, spree_current_user)
|
||||
%td.col-tags.field.naked_inputs
|
||||
= render VariantTagListInputComponent.new(name: f.field_name(:tag_list), variant:, placeholder: t('.add_a_tag'), aria_label: t('admin.products_page.columns.tags'))
|
||||
= render TagListInputComponent.new(name: f.field_name(:tag_list), tags: variant.tag_list, autocomplete_url: "/admin/tag_rules/variant_tag_rules?enterprise_id=#{variant.supplier_id}", placeholder: t('.add_a_tag'), aria_label: t('admin.products_page.columns.tags'))
|
||||
%td.col-inherits_properties.align-left
|
||||
-# empty
|
||||
%td.align-right
|
||||
|
||||
Reference in New Issue
Block a user