Files
Modrinth-plus/packages/ui/src/components/base/ManySelect.vue
Calum H. 37eac92329 refactor: migrate all input fields to StyledInput (#5306)
* feat: StyledInput component

* migrate: auth pages to styledInput

* migrate: search/filter inputs

* migrate: dashboard inputs

* migrate: app frontend

* migrate: search related inputs

* migrate: all of app-frontend

* fix: missing inputs on app-frontend

* migrate: frontend

* feat: multiline

* migrate: textareas

* fix: storybook use text-primary

* fix: lint

* fix: merge conflict

* feat: cleanup
2026-02-09 14:57:31 +00:00

150 lines
3.6 KiB
Vue

<template>
<ButtonStyled>
<PopoutMenu
v-if="options.length > 1 || showAlways"
v-bind="$attrs"
:disabled="disabled"
:position="position"
:direction="direction"
:dropdown-id="dropdownId"
:dropdown-class="dropdownClass"
:tooltip="tooltip"
@open="
() => {
searchQuery = ''
}
"
>
<slot />
<DropdownIcon class="h-5 w-5 text-secondary" />
<template #menu>
<StyledInput
v-if="search"
id="search-input"
ref="searchInput"
v-model="searchQuery"
:icon="SearchIcon"
placeholder="Search..."
type="text"
wrapper-class="mb-2 w-full"
@keydown.enter="
() => {
toggleOption(filteredOptions[0])
}
"
/>
<ScrollablePanel v-if="search">
<Button
v-for="(option, index) in filteredOptions"
:key="`option-${index}`"
:transparent="!manyValues.includes(option)"
:action="() => toggleOption(option)"
class="!w-full"
:color="manyValues.includes(option) ? 'secondary' : 'default'"
>
<slot name="option" :option="option">{{ getOptionLabel(option) }}</slot>
<CheckIcon
class="h-5 w-5 text-contrast ml-auto transition-opacity"
:class="{ 'opacity-0': !manyValues.includes(option) }"
/>
</Button>
</ScrollablePanel>
<div v-else class="flex flex-col gap-1">
<Button
v-for="(option, index) in filteredOptions"
:key="`option-${index}`"
:transparent="!manyValues.includes(option)"
:action="() => toggleOption(option)"
class="!w-full"
:color="manyValues.includes(option) ? 'secondary' : 'default'"
>
<slot name="option" :option="option">{{ getOptionLabel(option) }}</slot>
<CheckIcon
class="h-5 w-5 text-contrast ml-auto transition-opacity"
:class="{ 'opacity-0': !manyValues.includes(option) }"
/>
</Button>
</div>
<slot name="footer" />
</template>
</PopoutMenu>
</ButtonStyled>
</template>
<script setup lang="ts">
import { CheckIcon, DropdownIcon, SearchIcon } from '@modrinth/assets'
import { computed, ref } from 'vue'
import { Button, ButtonStyled, PopoutMenu, StyledInput } from '../index'
import ScrollablePanel from './ScrollablePanel.vue'
type Option = string | number | object
const props = withDefaults(
defineProps<{
modelValue: Option[]
options: Option[]
disabled?: boolean
position?: string
direction?: string
displayName?: (option: Option) => string
search?: boolean
dropdownId?: string
dropdownClass?: string
showAlways?: boolean
tooltip?: string
}>(),
{
disabled: false,
position: 'auto',
direction: 'auto',
displayName: undefined,
search: false,
dropdownId: '',
dropdownClass: '',
showAlways: false,
tooltip: '',
},
)
function getOptionLabel(option: Option): string {
return props.displayName?.(option) ?? (option as string)
}
const emit = defineEmits(['update:modelValue', 'change'])
const selectedValues = ref(props.modelValue || [])
const searchInput = ref()
const searchQuery = ref('')
const manyValues = computed({
get() {
return props.modelValue || selectedValues.value
},
set(newValue) {
emit('update:modelValue', newValue)
emit('change', newValue)
selectedValues.value = newValue
},
})
const filteredOptions = computed(() => {
return props.options.filter(
(x) =>
!searchQuery.value ||
getOptionLabel(x).toLowerCase().includes(searchQuery.value.toLowerCase()),
)
})
defineOptions({
inheritAttrs: false,
})
function toggleOption(id: Option) {
if (manyValues.value.includes(id)) {
manyValues.value = manyValues.value.filter((x) => x !== id)
} else {
manyValues.value = [...manyValues.value, id]
}
}
</script>