Skip to content

Commit

Permalink
Persist multiselect values and hide selected options
Browse files Browse the repository at this point in the history
  • Loading branch information
josefarias committed Mar 16, 2024
1 parent c8e584b commit 6d68b24
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 71 deletions.
4 changes: 3 additions & 1 deletion app/assets/javascripts/controllers/hw_combobox_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ export default class HwComboboxController extends Concerns(...concerns) {
const inputType = element.dataset.inputType
const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY

this._resetMultiselectionMarks()

if (inputType && inputType !== "hw:lockInSelection") {
if (delay) await sleep(delay)
this._selectOnQuery({ inputType })
this._selectOnQuery(inputType)
} else {
this._preselect()
}
Expand Down
4 changes: 2 additions & 2 deletions app/assets/javascripts/hw_combobox/models/combobox/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { dispatch } from "hw_combobox/helpers"

Combobox.Events = Base => class extends Base {
_dispatchSelectionEvent({ isNewAndAllowed, previousValue }) {
if (previousValue === this._fieldValue) return
if (previousValue === this._fieldValueString) return

dispatch("hw-combobox:selection", {
target: this.element,
Expand All @@ -20,7 +20,7 @@ Combobox.Events = Base => class extends Base {

get _eventableDetails() {
return {
value: this._fieldValue,
value: this._fieldValueString,
display: this._fullQuery,
query: this._typedQuery,
fieldName: this._fieldName,
Expand Down
25 changes: 15 additions & 10 deletions app/assets/javascripts/hw_combobox/models/combobox/filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { applyFilter, debounce, unselectedPortion } from "hw_combobox/helpers"
import { get } from "hw_combobox/vendor/requestjs"

Combobox.Filtering = Base => class extends Base {
filterAndSelect(event) {
this._filter(event)
filterAndSelect({ inputType }) {
this._filter(inputType)

if (this._isSync) {
this._selectOnQuery(event)
this._selectOnQuery(inputType)
} else {
// noop, async selection is handled by stimulus callbacks
}
Expand All @@ -18,32 +18,37 @@ Combobox.Filtering = Base => class extends Base {
this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this))
}

_filter(event) {
_filter(inputType) {
if (this._isAsync) {
this._debouncedFilterAsync(event)
this._debouncedFilterAsync(inputType)
} else {
this._filterSync()
}

this._markQueried()
}

_debouncedFilterAsync(event) {
this._filterAsync(event)
_debouncedFilterAsync(inputType) {
this._filterAsync(inputType)
}

async _filterAsync(event) {
async _filterAsync(inputType) {
const query = {
q: this._fullQuery,
input_type: event.inputType,
input_type: inputType,
for_id: this.element.dataset.asyncId
}

await get(this.asyncSrcValue, { responseKind: "turbo-stream", query })
}

_filterSync() {
this._allOptionElements.forEach(applyFilter(this._fullQuery, { matching: this.filterableAttributeValue }))
this._allFilterableOptionElements.forEach(
applyFilter(
this._fullQuery,
{ matching: this.filterableAttributeValue }
)
)
}

_clearQuery() {
Expand Down
52 changes: 46 additions & 6 deletions app/assets/javascripts/hw_combobox/models/combobox/form_field.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,59 @@
import Combobox from "hw_combobox/models/combobox/base"

Combobox.FormField = Base => class extends Base {
_setFieldValue(value) {
this.hiddenFieldTarget.value = value
get _fieldValue() {
if (this._isMultiselect) {
const currentValue = this.hiddenFieldTarget.value
const arrayFromValue = currentValue ? currentValue.split(",") : []

return new Set(arrayFromValue)
} else {
return this.hiddenFieldTarget.value
}
}

_setFieldName(value) {
this.hiddenFieldTarget.name = value
get _fieldValueString() {
if (this._isMultiselect) {
return this._fieldValueArray.join(",")
} else {
return this.hiddenFieldTarget.value
}
}

get _fieldValue() {
return this.hiddenFieldTarget.value
get _fieldValueArray() {
if (this._isMultiselect) {
return Array.from(this._fieldValue)
} else {
return [ this.hiddenFieldTarget.value ]
}
}

set _fieldValue(value) {
if (this._isMultiselect) {
this.hiddenFieldTarget.dataset.valueForMultiselect = value
this.hiddenFieldTarget.dataset.displayForMultiselect = this._fullQuery
} else {
this.hiddenFieldTarget.value = value
}
}

get _hasEmptyFieldValue() {
if (this._isMultiselect) {
return this.hiddenFieldTarget.dataset.valueForMultiselect === ""
} else {
return this.hiddenFieldTarget.value === ""
}
}

get _hasFieldValue() {
return !this._hasEmptyFieldValue
}

get _fieldName() {
return this.hiddenFieldTarget.name
}

set _fieldName(value) {
this.hiddenFieldTarget.name = value
}
}
101 changes: 80 additions & 21 deletions app/assets/javascripts/hw_combobox/models/combobox/multiselect.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,19 @@ import { cancel } from "hw_combobox/helpers"
import { get } from "hw_combobox/vendor/requestjs"

Combobox.Multiselect = Base => class extends Base {
async createChip() {
if (!this._isMultiselect || !this._fieldValue) return

await get(this.selectionChipSrcValue, {
responseKind: "turbo-stream",
query: {
for_id: this.element.dataset.asyncId,
combobox_value: this._fieldValue
}
})

this._clearQuery()

if (!this._isSmallViewport) {
this.openByFocusing()
}
}

navigateChip(event) {
this._chipKeyHandlers[event.key]?.call(this, event)
}

removeChip(event) {
event.currentTarget.closest("[data-hw-combobox-chip]").remove()
console.log("removing ", event.params.value) // TODO: implement removal
removeChip({ currentTarget, params }) {
const option = this._optionElementWithValue(params.value)

this._markNotSelected(option)
this._markNotMultiselected(option)
this._removeFromFieldValue(params.value)
this._filter("hw:multiselectSync")

currentTarget.closest("[data-hw-combobox-chip]").remove()

if (!this._isSmallViewport) {
this.openByFocusing()
Expand All @@ -53,11 +41,82 @@ Combobox.Multiselect = Base => class extends Base {
}
}

_createChip() {
if (!this._isMultiselect) return

this._beforeClearingMultiselectQuery(async (display, value) => {
this._fullQuery = ""
this._filter("hw:multiselectSync")

await get(this.selectionChipSrcValue, {
responseKind: "turbo-stream",
query: {
for_id: this.element.dataset.asyncId,
combobox_value: value,
display_value: display
}
})

this._addToFieldValue(value)
})
}

_beforeClearingMultiselectQuery(callback) {
const display = this.hiddenFieldTarget.dataset.displayForMultiselect
const value = this.hiddenFieldTarget.dataset.valueForMultiselect

if (value && !this._fieldValue.has(value)) {
callback(display, value)
}

this.hiddenFieldTarget.dataset.displayForMultiselect = ""
this.hiddenFieldTarget.dataset.valueForMultiselect = ""
}

_resetMultiselectionMarks() {
if (!this._isMultiselect) return

this._fieldValueArray.forEach(value => {
const option = this._optionElementWithValue(value)
option.setAttribute("data-multiselected", "")
option.hidden = true
})
}

_markNotMultiselected(option) {
if (!this._isMultiselect) return

option.removeAttribute("data-multiselected")
option.hidden = false
}

_addToFieldValue(value) {
let newValue = this._fieldValue

newValue.add(String(value))
this.hiddenFieldTarget.value = Array.from(newValue).join(",")

if (this._isSync) this._resetMultiselectionMarks()
}

_removeFromFieldValue(value) {
let newValue = this._fieldValue

newValue.delete(String(value))
this.hiddenFieldTarget.value = Array.from(newValue).join(",")

if (this._isSync) this._resetMultiselectionMarks()
}

_focusLastChipDismisser() {
this.chipDismisserTargets[this.chipDismisserTargets.length - 1].focus()
}

get _isMultiselect() {
return this.hasSelectionChipSrcValue
}

get _isSingleSelect() {
return !this._isMultiselect
}
}
16 changes: 10 additions & 6 deletions app/assets/javascripts/hw_combobox/models/combobox/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,28 @@ Combobox.Options = Base => class extends Base {
}

_resetOptions(deselectionStrategy) {
this._setFieldName(this.originalNameValue)
this._fieldName = this.originalNameValue
deselectionStrategy()
}

_optionElementWithValue(value) {
return this._actingListbox.querySelector(`[${this.filterableAttributeValue}][data-value='${value}']`)
}

get _allowNew() {
return !!this.nameWhenNewValue
}

get _allOptions() {
return Array.from(this._allOptionElements)
return Array.from(this._allFilterableOptionElements)
}

get _allOptionElements() {
return this._actingListbox.querySelectorAll(`[${this.filterableAttributeValue}]`)
get _allFilterableOptionElements() {
return this._actingListbox.querySelectorAll(`[${this.filterableAttributeValue}]:not([data-multiselected])`)
}

get _visibleOptionElements() {
return [ ...this._allOptionElements ].filter(visible)
return [ ...this._allFilterableOptionElements ].filter(visible)
}

get _selectedOptionElement() {
Expand All @@ -40,7 +44,7 @@ Combobox.Options = Base => class extends Base {
}

get _isUnjustifiablyBlank() {
const valueIsMissing = !this._fieldValue
const valueIsMissing = this._hasEmptyFieldValue
const noBlankOptionSelected = !this._selectedOptionElement

return valueIsMissing && noBlankOptionSelected
Expand Down
Loading

0 comments on commit 6d68b24

Please sign in to comment.