diff --git a/app/javascript/controllers/multi_selection_combobox_controller.js b/app/javascript/controllers/multi_selection_combobox_controller.js new file mode 100644 index 000000000..1dce17356 --- /dev/null +++ b/app/javascript/controllers/multi_selection_combobox_controller.js @@ -0,0 +1,81 @@ +import { Controller } from "@hotwired/stimulus" +import { toSentence } from "helpers/text_helpers" + +export default class extends Controller { + #hiddenField + + static targets = [ "label", "item", "hiddenFieldTemplate" ] + static values = { + selectPropertyName: { type: String, default: "aria-checked" }, + defaultValue: String, + noSelectionLabel: { type: String, default: "No selection" } + } + + connect() { + this.labelTarget.textContent = this.#selectedLabel + } + + change(event) { + const item = event.target.closest("[role='checkbox']") + if (item) { + this.#toggleSelection(item) + } + } + + get #selectedLabel() { + const selectedValues = this.#selectedValues() + if (selectedValues.length === 0) { + return this.noSelectionLabelValue + } + + const labels = this.#selectedItems().map(item => item.dataset.multiSelectionComboboxLabel) + return toSentence(labels, { + two_words_connector: " or ", + last_word_connector: ", or " + }) + } + + #toggleSelection(item) { + const isSelected = item.getAttribute(this.selectPropertyNameValue) === "true" + + if (isSelected) { + item.setAttribute(this.selectPropertyNameValue, "false") + } else { + item.setAttribute(this.selectPropertyNameValue, "true") + } + + this.#updateHiddenFields() + this.labelTarget.textContent = this.#selectedLabel + } + + #updateHiddenFields() { + this.#clearHiddenFields() + this.#addHiddenFields() + } + + #selectedItems() { + return this.itemTargets.filter(item => + item.getAttribute(this.selectPropertyNameValue) === "true" + ) + } + + #selectedValues() { + return this.#selectedItems().map(item => item.dataset.multiSelectionComboboxValue) + } + + #clearHiddenFields() { + this.element.querySelectorAll('input[type="hidden"]').forEach(field => { + if (field !== this.hiddenField) { + field.remove() + } + }) + } + + #addHiddenFields() { + this.#selectedValues().forEach(value => { + const [field] = this.hiddenFieldTemplateTarget.content.cloneNode(true).children + field.value = value + this.element.appendChild(field) + }) + } +} diff --git a/app/javascript/helpers/text_helpers.js b/app/javascript/helpers/text_helpers.js index 22593fe6f..d886e94b9 100644 --- a/app/javascript/helpers/text_helpers.js +++ b/app/javascript/helpers/text_helpers.js @@ -11,3 +11,27 @@ export function normalizeFilteredText(string) { export function filterMatches(text, potentialMatch) { return normalizeFilteredText(text).includes(normalizeFilteredText(potentialMatch)) } + +export function toSentence(array, options = {}) { + const defaultConnectors = { + words_connector: ", ", + two_words_connector: " and ", + last_word_connector: ", and " + } + + const connectors = { ...defaultConnectors, ...options } + + if (array.length === 0) { + return "" + } + + if (array.length === 1) { + return array[0] + } + + if (array.length === 2) { + return array.join(connectors.two_words_connector) + } + + return array.slice(0, -1).join(connectors.words_connector) + connectors.last_word_connector + array[array.length - 1] +} diff --git a/app/views/filters/settings/_controls.html.erb b/app/views/filters/settings/_controls.html.erb index 7c8b48de4..89a53d0f2 100644 --- a/app/views/filters/settings/_controls.html.erb +++ b/app/views/filters/settings/_controls.html.erb @@ -3,7 +3,7 @@ <%= hidden_field_tag :expand_all, true, disabled: !user_filtering.expanded?, data: { toggle_enable_target: "element" } %> <%= render "filters/settings/sorted_by", user_filtering: user_filtering %> <%= render "filters/settings/indexed_by", user_filtering: user_filtering %> - <%#= render "filters/settings/tags", user_filtering: user_filtering %> + <%= render "filters/settings/tags", user_filtering: user_filtering %> <%#= render "filters/settings/assignees", user_filtering: user_filtering %> <%#= render "filters/settings/creators", user_filtering: user_filtering %> <%#= render "filters/settings/closers", user_filtering: user_filtering %> diff --git a/app/views/filters/settings/_tags.html.erb b/app/views/filters/settings/_tags.html.erb index aae738f54..466cda9ab 100644 --- a/app/views/filters/settings/_tags.html.erb +++ b/app/views/filters/settings/_tags.html.erb @@ -1,17 +1,20 @@ <% filter = user_filtering.filter %> <%= tag.div class: "quick-filter position-relative", - data: { controller: "dialog", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", filter_show: user_filtering.show_tags? } do %> - + + <%= filter_dialog "Tagged…" do %> Tagged… @@ -20,34 +23,16 @@ type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %> <% end %> -