import { makeComponentProps } from '@/composables/component' import { computed, ref, Ref } from 'vue' import { useEventListener } from '@vueuse/core' import '@/components/UDropdown/scrollbar.scss' import DropdownVariant from '@/types/dropdownVariants' import { Sizes } from '@/composables/size' import { UIcon } from '@/components/UIcon/UIcon' import { UAvatar } from '@/components/UAvatar/UAvatar' import { UInput } from '@/components/UInput/UInput' import { UTag } from '@/components/UTag/UTag' import { genericComponent, propsFactory, useRender } from '@/utils' export const makeUDropdownProps = propsFactory( { modelValue: Object, label: String, placeholder: String, hint: String, appendIcon: { type: String, default: 'chevron-down', }, prependIcon: String, avatarIcon: String, variant: { type: String, default: DropdownVariant.default, }, type: String, size: { type: String, default: 'md', }, emptyMessage: String, disabled: { type: Boolean, default: false, }, name: String, error: Boolean, errorMessages: Array as () => string[], dropDownItems: { type: Array, required: true, }, ...makeComponentProps(), }, 'UDropdown' ) export type UDropdownSlots = { default: never header: never } interface DropdownItem { id: string image: string text: string supportingText: string } export const UDropdown = genericComponent()({ name: 'UDropdown', props: makeUDropdownProps(), emits: { submit: (e: string) => true, blur: () => true, 'keydown:escape': () => true, 'update:modelValue': (value: object) => true, }, setup(props, { emit, slots }) { const isActive = ref(false) const selectedItem = ref() const selectedItems: Ref = ref([]) const isHover = ref(false) const inputValue = ref('') const dropdownRef = ref(null) const searchInputRef = ref(null) const tagsInputRef = ref(null) const tagsInputComponentRef = ref() const tagsInputContentRef = ref(null) const dropdownContentRef = ref(null) const inputFieldRef = ref(null) const toggleActive = () => { if (!props.disabled) isActive.value = !isActive.value } const getDropDownItems = (): DropdownItem[] => { const dropdownItems: DropdownItem[] = props.dropDownItems if (Array.isArray(dropdownItems)) { const updatedItems = dropdownItems.map((item) => { if (item.image || item.text || item.supportingText) { return item } else { return { id: item.id || '', image: item.image || '', text: item.text || '', supportingText: item.supportingText || '', } } }) const filteredItems = updatedItems.filter((item) => { return ( item.image !== '' || item.text !== '' || item.supportingText !== '' ) }) return filteredItems } else { return [] } } const isEnabled = computed( (): boolean => !!props.dropDownItems.length && !props.disabled ) if (props.type === 'header' && isEnabled.value) { isActive.value = true } const inputHandler = () => { if (isEnabled.value) { isActive.value = true } } const inputClickHandler = () => { if (isEnabled.value) { if (isActive.value !== true) isActive.value = true } } const removeTag = (tagIndex: number) => { const dropDownItems = getDropDownItems() if (isEnabled.value) { selectedItems.value.splice(tagIndex, 1) emit( 'update:modelValue', selectedItems.value.map((index: number) => dropDownItems[index]) ) } } const filteredDropDownItems = computed(() => { const dropDownItems = getDropDownItems() if ( props.variant !== DropdownVariant.search && props.variant !== DropdownVariant.tags ) { return dropDownItems } else { const inputValueLower = inputValue.value && inputValue.value.toLowerCase() const filteredItems = dropDownItems.filter( (item) => item.text?.toLowerCase().startsWith(inputValueLower) || item.supportingText?.toLowerCase().includes(inputValueLower) ) return filteredItems.length > 0 ? filteredItems : null } }) const selectItem = (index: number) => { const dropDownItems = getDropDownItems() if (!isEnabled.value) { return false } if (props.variant === DropdownVariant.tags) { const selectedItemIndex = dropDownItems.indexOf( filteredDropDownItems.value?.[index] as DropdownItem ) const selectedItemPosition = selectedItems.value.indexOf(selectedItemIndex) if (selectedItemPosition === -1) { selectedItems.value.push(selectedItemIndex) } else { selectedItems.value.splice(selectedItemPosition, 1) } emit( 'update:modelValue', selectedItems.value.map((index) => dropDownItems[index]) ) tagsInputComponentRef?.value?.target?.focus() return true } if ( selectedItem.value === dropDownItems.indexOf( filteredDropDownItems.value?.[index] as DropdownItem ) ) { selectedItem.value = undefined emit('update:modelValue', selectedItem.value) } else { const filteredItems = filteredDropDownItems.value if ( filteredItems !== null && index >= 0 && index < filteredItems.length ) { const selectedItemIndex = dropDownItems.indexOf( filteredItems[index] as DropdownItem ) selectedItem.value = selectedItemIndex inputValue.value = '' emit('update:modelValue', dropDownItems[selectedItemIndex]) } } } const handleKeyDown = (event: KeyboardEvent) => { const escapeKey = 'Escape' if ( isActive.value && isEnabled.value && event.key === 'Backspace' && props.variant !== DropdownVariant.tags && props.type !== 'header' ) { selectedItem.value = undefined emit('update:modelValue', selectedItem.value) } if (isEnabled.value && event.key === escapeKey) { if (isActive.value) { toggleActive() } emit('keydown:escape') } } const handleClickOutside = (event: MouseEvent) => { if ( (isActive.value && (!dropdownRef.value || (!dropdownRef.value.contains(event.target as Node) && !dropdownContentRef.value?.contains(event.target as Node))) && !searchInputRef.value?.contains(event.target as Node) && !tagsInputRef.value?.contains(event.target as Node)) || tagsInputContentRef.value?.contains(event.target as Node) ) { isActive.value = false emit('blur') } if (dropdownContentRef.value?.contains(event.target as Node)) { isActive.value = true } } useEventListener('click', handleClickOutside) useEventListener('keydown', handleKeyDown) const classes = computed(() => ({ 'field box-border flex items-center gap-2 font-regular rounded-md border px-3.5': true, ...(props.size == 'md' && { 'max-h-[44px]': true, }), ...(props.size == 'sm' && { 'max-h-[40px]': true, }), ...(isActive.value === false && props.error === false && isEnabled.value === true && { 'border-gray-300': true, }), ...(isActive.value === true && props.error === false && isEnabled.value === true && { 'border-primary-300 shadow-xs-btn shadow-primary-50': true, }), ...(isActive.value === true && props.error === true && isEnabled.value === true && { 'shadow-md shadow-error-50': true, }), ...(props.error === true && isEnabled.value === true && { 'border-error-300': true, }), ...(isEnabled.value === false && { 'border-gray-300 bg-gray-50 cursor-not-allowed': true, }), })) const inputClasses = computed(() => ({ // eslint-disable-next-line max-len 'box-border flex-grow select-none bg-transparent font-regular text-gray-900 text-text-md py-2.5 overflow-hidden': true, ...(isEnabled.value === false && { 'cursor-not-allowed': true, }), })) const placeholderClasses = computed(() => ({ 'placeholder font-regular text-gray-500 text-text-md': true, })) const textClasses = computed(() => ({ // eslint-disable-next-line max-len 'font-medium text-gray-900 text-text-md whitespace-nowrap text-ellipsis overflow-hidden max-w-[70%]': true, })) const supportingTextClasses = computed(() => ({ // eslint-disable-next-line max-len 'font-regular text-gray-600 whitespace-nowrap text-ellipsis overflow-hidden max-w-[30%]': true, ...(props.size == 'md' && { 'text-text-md': true, }), ...(props.size == 'sm' && { 'text-text-sm': true, }), })) const inputValueClasses = computed(() => ({ 'flex flex-row gap-2 items-center': true, })) const dropdownClasses = computed(() => ({ 'dropdownContent absolute right-0 flex flex-col gap-1 p-1.5 pr-0 w-full': true, 'overflow-y-scroll bg-white rounded-md border border-gray-300 max-h-[320px]': true, ...(props.label && props.size == 'md' && { 'top-[75px]': true, }), ...(props.label && props.size == 'sm' && { 'top-[71px]': true, }), ...(!props.label && props.size == 'md' && { 'top-[49px]': true, }), ...(!props.label && props.size == 'sm' && { 'top-[44px]': true, }), ...(isEnabled.value === false && { 'cursor-not-allowed': true, }), })) const dropdownItemClasses = computed(() => { let classes = `dropdownItem flex gap-2 px-2 py-2.5 cursor-pointer rounded-md hover:bg-gray-50 items-center select-none` if (!isEnabled.value) { classes += ' cursor-not-allowed' } return classes }) const fieldClasses = computed(() => ({ 'box-border relative flex flex-col gap-1.5 w-full': true, })) const labelClasses = computed(() => ({ 'label font-medium text-gray-700 text-text-sm': true, })) const hintClasses = computed(() => ({ 'hint font-regular text-gray-600 text-text-sm': true, })) const errorMessageClasses = computed(() => ({ 'error font-regular text-error-500 text-text-sm': true, })) const dotClasses = computed(() => ({ 'm-0.5 w-2 h-2 bg-success-500 rounded': true, })) const tagsInputContentClasses = computed(() => ({ 'flex gap-1.5 items-center h-44': true, })) useRender(() => (
{props.label ? (
{props.label}
) : null} {(props.variant === 'search' && selectedItem.value !== undefined) || (props.variant !== 'search' && props.variant !== 'tags') ? (
{props.prependIcon && props.variant !== DropdownVariant.avatarLeading && props.variant !== DropdownVariant.dotLeading ? (
) : null} {props.variant == DropdownVariant.avatarLeading && selectedItem.value == undefined ? (
) : null} {props.variant === DropdownVariant.avatarLeading && selectedItem.value !== null && selectedItem.value !== undefined ? ( ) : null} {props.variant === DropdownVariant.dotLeading ? (
) : null}
{selectedItem.value == null && selectedItem.value == undefined ? (
{props.placeholder}
) : null} {selectedItem.value !== null && selectedItem.value !== undefined ? (
{getDropDownItems()[selectedItem.value]?.text}
{getDropDownItems()[selectedItem.value]?.supportingText}
) : null}
) : null} {props.variant == DropdownVariant.search && selectedItem.value == undefined ? (
) : null} {props.variant == DropdownVariant.tags ? (
{selectedItems.value.map( (itemIndex: number, tagIndex: number) => ( removeTag(tagIndex)} key={tagIndex} class="whitespace-nowrap" avatarImagePath={getDropDownItems()[itemIndex]?.image} closed={isEnabled.value} size={Sizes.sm} > {getDropDownItems()[itemIndex]?.supportingText} ) )}
) : null} {(!props.error && props.hint && !isActive.value) || (!filteredDropDownItems.value && props.hint) ? (
{props.hint}
) : null} {isActive.value && filteredDropDownItems.value ? (
{filteredDropDownItems.value.map((item, index) => (
selectItem(index)} > {props.prependIcon && props.variant !== DropdownVariant.avatarLeading && props.variant !== DropdownVariant.dotLeading && props.variant !== DropdownVariant.search && ( )} {props.variant === DropdownVariant.avatarLeading && ( )} {props.variant === DropdownVariant.dotLeading && (
)}
{item.text}
{item.supportingText}
{props.variant !== DropdownVariant.tags && index === selectedItem.value && ( )} {props.variant === DropdownVariant.tags && filteredDropDownItems.value && selectedItems.value.includes( getDropDownItems().indexOf( filteredDropDownItems.value[index] ) ) && ( )}
))}
) : null} {!filteredDropDownItems.value && isActive.value && props.emptyMessage ? (
{props.emptyMessage}
) : null} {props.error && props.errorMessages ? (
{props.errorMessages.map((errorMessage) => (
{errorMessage}
))}
) : null}
)) return { isActive, isEnabled, } }, }) export type UDropdown = InstanceType