complete Complete User Request Submission section RAD-1
This commit is contained in:
@@ -0,0 +1,613 @@
|
||||
<template>
|
||||
<div class="ui-input-dropdown-wrapper" :class="directionClass" ref="wrapperEl">
|
||||
<label v-if="label" :for="id" class="ui-input-label">
|
||||
{{ label }}
|
||||
<span v-if="required" class="ui-input-required-indicator">*</span>
|
||||
</label>
|
||||
|
||||
<div class="ui-input">
|
||||
<div v-if="hasPrefixes" class="ui-input-wrapper-border" :class="prefixClasses">
|
||||
<i v-if="iconEnd" :class="[iconEnd, iconColorClass]" class="ui-input-icon-end !mt-2"></i>
|
||||
<span v-if="prefixEnd" class="ui-input-prefix-end" :class="prefixColorClass">{{ prefixEnd }}</span>
|
||||
|
||||
<InputText
|
||||
:id="id"
|
||||
v-model="inputValue"
|
||||
:class="inputClassesNoBorder"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:type="type"
|
||||
:maxlength="maxlength"
|
||||
:minlength="minlength"
|
||||
:autocomplete="autocomplete"
|
||||
:autofocus="autofocus"
|
||||
:spellcheck="spellcheck"
|
||||
:tabindex="tabindex"
|
||||
@input="handleInput"
|
||||
@change="handleChange"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@keyup="handleKeyup"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="props.prefixOptions.length > 0"
|
||||
ref="buttonRef"
|
||||
type="button"
|
||||
class="ui-input-prefix-dropdown-button"
|
||||
:class="prefixColorClass"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
|
||||
<pre class="ui-input-prefix-text">{{ selectedPrefix.prefix }}</pre>
|
||||
<i v-if="selectedPrefix.icon" :class="selectedPrefix.icon" class="ui-input-prefix-icon"></i>
|
||||
</button>
|
||||
|
||||
<i v-if="iconStart" :class="[iconStart, iconColorClass]" class="ui-input-icon-start !mt-2"></i>
|
||||
</div>
|
||||
|
||||
<InputText
|
||||
v-else
|
||||
:id="id"
|
||||
v-model="inputValue"
|
||||
:class="inputClasses"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:type="type"
|
||||
:maxlength="maxlength"
|
||||
:minlength="minlength"
|
||||
:autocomplete="autocomplete"
|
||||
:autofocus="autofocus"
|
||||
:spellcheck="spellcheck"
|
||||
:tabindex="tabindex"
|
||||
@input="handleInput"
|
||||
@change="handleChange"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@keyup="handleKeyup"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
name="dropdown"
|
||||
@enter="onDropdownEnter"
|
||||
@leave="onDropdownLeave"
|
||||
@after-leave="onDropdownAfterLeave"
|
||||
>
|
||||
<div
|
||||
v-if="isDropdownOpen && props.prefixOptions.length > 0"
|
||||
ref="dropdownRef"
|
||||
class="ui-input-dropdown-menu"
|
||||
:class="[ `ui-input-dropdown-menu--${dropdownPosition}`, dropdownDirectionClass, dropdownTextAlignClass, dropdownClass ]"
|
||||
>
|
||||
<div v-if="showSearch" class="ui-input-dropdown-search" :class="dropdownSearchWrapperClass">
|
||||
<InputText
|
||||
v-model="searchQuery"
|
||||
:placeholder="searchPlaceholder"
|
||||
class="ui-input-dropdown-search-input"
|
||||
:class="dropdownSearchInputClass"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="ui-input-dropdown-list">
|
||||
<button
|
||||
v-for="item in filteredOptions"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="ui-input-dropdown-item"
|
||||
:class="[ selectedPrefix.value === item.value ? 'ui-input-dropdown-item-selected' : '', dropdownItemClass, selectedPrefix.value === item.value ? dropdownItemSelectedClass : '' ]"
|
||||
@click="selectOption(item)"
|
||||
>
|
||||
<i v-if="item.icon" :class="item.icon" class="ui-input-dropdown-item-icon"></i>
|
||||
|
||||
<span class="ui-input-dropdown-item-label">{{ item.enLabel }}</span>
|
||||
<span class="ui-input-dropdown-item-prefix">({{ item.prefix }})</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<small v-if="error" class="ui-input-error-message">
|
||||
{{ error }}
|
||||
</small>
|
||||
|
||||
<small v-if="hint" class="ui-input-hint-message">
|
||||
{{ hint }}
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import InputText from 'primevue/inputtext'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'outlined',
|
||||
validator: (value) => ['outlined', 'filled'].includes(value)
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxlength: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
minlength: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: 'off'
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
spellcheck: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
prefixEnd: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
iconStart: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
iconEnd: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
prefixStart: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'rtl',
|
||||
validator: (value) => ['rtl', 'ltr'].includes(value)
|
||||
},
|
||||
prefixColor: {
|
||||
type: String,
|
||||
default: 'dominant-700'
|
||||
},
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: 'dominant-700'
|
||||
},
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
default: 'جستجو...'
|
||||
},
|
||||
dropdownDirection: {
|
||||
type: String,
|
||||
default: '',
|
||||
validator: (value) => ['', 'rtl', 'ltr'].includes(value)
|
||||
},
|
||||
dropdownTextAlign: {
|
||||
type: String,
|
||||
default: '',
|
||||
validator: (value) => ['', 'left', 'right', 'start', 'end'].includes(value)
|
||||
},
|
||||
dropdownClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
dropdownSearchWrapperClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
dropdownSearchInputClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
dropdownItemClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
dropdownItemSelectedClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
prefixOptions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
validator: (value) => {
|
||||
return value.every(item =>
|
||||
typeof item === 'object' &&
|
||||
item.value &&
|
||||
item.prefix &&
|
||||
item.label
|
||||
)
|
||||
}
|
||||
},
|
||||
defaultPrefixValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue',
|
||||
'input',
|
||||
'change',
|
||||
'focus',
|
||||
'blur',
|
||||
'keyup',
|
||||
'keydown',
|
||||
'prefix-change'
|
||||
])
|
||||
|
||||
const inputValue = ref(props.modelValue)
|
||||
const isDropdownOpen = ref(false)
|
||||
const isDropdownClosing = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const dropdownPosition = ref('bottom')
|
||||
const dropdownRef = ref(null)
|
||||
const buttonRef = ref(null)
|
||||
const wrapperEl = ref(null)
|
||||
|
||||
const id = `input-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
const directionClass = computed(() => {
|
||||
return `ui-input--${props.direction}`
|
||||
})
|
||||
|
||||
const hasPrefixes = computed(() => {
|
||||
return props.prefixOptions.length > 0 || props.prefixEnd || props.prefixStart || props.iconStart || props.iconEnd
|
||||
})
|
||||
|
||||
const prefixClasses = computed(() => {
|
||||
const classes = []
|
||||
|
||||
if (props.prefixOptions.length > 0) {
|
||||
classes.push('ui-input-with-prefixes')
|
||||
}
|
||||
|
||||
if (props.prefixEnd) {
|
||||
classes.push('ui-input-with-prefix-end')
|
||||
}
|
||||
|
||||
if (props.iconStart) {
|
||||
classes.push('ui-input-with-icon-start')
|
||||
}
|
||||
|
||||
if (props.iconEnd) {
|
||||
classes.push('ui-input-with-icon-end')
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
})
|
||||
|
||||
const prefixColorClass = computed(() => {
|
||||
return `text-${props.prefixColor}`
|
||||
})
|
||||
|
||||
const iconColorClass = computed(() => {
|
||||
return `text-${props.iconColor}`
|
||||
})
|
||||
|
||||
const effectiveDropdownDirection = computed(() => {
|
||||
return props.dropdownDirection || props.direction
|
||||
})
|
||||
|
||||
const dropdownDirectionClass = computed(() => {
|
||||
return `ui-input-dropdown--${effectiveDropdownDirection.value}`
|
||||
})
|
||||
|
||||
const dropdownTextAlignClass = computed(() => {
|
||||
if (props.dropdownTextAlign === 'left') return 'text-left'
|
||||
if (props.dropdownTextAlign === 'right') return 'text-right'
|
||||
if (props.dropdownTextAlign === 'start') {
|
||||
return effectiveDropdownDirection.value === 'rtl' ? 'text-right' : 'text-left'
|
||||
}
|
||||
if (props.dropdownTextAlign === 'end') {
|
||||
return effectiveDropdownDirection.value === 'rtl' ? 'text-left' : 'text-right'
|
||||
}
|
||||
return effectiveDropdownDirection.value === 'rtl' ? 'text-right' : 'text-left'
|
||||
})
|
||||
|
||||
const selectedPrefix = computed(() => {
|
||||
if (props.defaultPrefixValue) {
|
||||
const found = props.prefixOptions.find(option => option.value === props.defaultPrefixValue)
|
||||
if (found) return found
|
||||
}
|
||||
return props.prefixOptions[0] || { value: '', prefix: '', label: '', icon: '' }
|
||||
})
|
||||
|
||||
const filteredOptions = computed(() => {
|
||||
if (!searchQuery.value) return props.prefixOptions
|
||||
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
|
||||
if (query.length <= 2) {
|
||||
return props.prefixOptions.filter(option =>
|
||||
option.prefix.startsWith(query) ||
|
||||
option.value.toLowerCase().startsWith(query)
|
||||
)
|
||||
}
|
||||
|
||||
return props.prefixOptions.filter(option => {
|
||||
if (option.prefix === query ||
|
||||
option.value.toLowerCase() === query ||
|
||||
option.label.toLowerCase() === query ||
|
||||
(option.enLabel && option.enLabel.toLowerCase() === query)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (option.prefix.startsWith(query) ||
|
||||
option.label.toLowerCase().startsWith(query) ||
|
||||
(option.enLabel && option.enLabel.toLowerCase().startsWith(query))) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (option.label.toLowerCase().includes(query) ||
|
||||
(option.enLabel && option.enLabel.toLowerCase().includes(query)) ||
|
||||
option.prefix.includes(query) ||
|
||||
option.value.toLowerCase().includes(query)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const cleanQuery = query.replace(/[\s\-_\-]/g, '')
|
||||
const cleanLabel = option.label.replace(/[\s\-_\-]/g, '').toLowerCase()
|
||||
const cleanEnLabel = option.enLabel ? option.enLabel.replace(/[\s\-_\-]/g, '').toLowerCase() : ''
|
||||
|
||||
if (cleanLabel.includes(cleanQuery) ||
|
||||
cleanEnLabel.includes(cleanQuery)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
})
|
||||
|
||||
const inputClasses = computed(() => {
|
||||
const classes = []
|
||||
|
||||
switch (props.size) {
|
||||
case 'small':
|
||||
classes.push('p-2', 'text-xs')
|
||||
break
|
||||
case 'large':
|
||||
classes.push('p-4', 'text-base')
|
||||
break
|
||||
default:
|
||||
classes.push('p-3', 'text-sm')
|
||||
}
|
||||
|
||||
if (props.variant === 'filled') {
|
||||
classes.push('bg-gray-100')
|
||||
}
|
||||
|
||||
if (props.error) {
|
||||
classes.push('border-red-500')
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
classes.push('opacity-50', 'cursor-not-allowed')
|
||||
}
|
||||
|
||||
if (props.customClass) {
|
||||
classes.push(props.customClass)
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
})
|
||||
|
||||
const inputClassesNoBorder = computed(() => {
|
||||
const classes = []
|
||||
|
||||
switch (props.size) {
|
||||
case 'small':
|
||||
classes.push('p-2', 'text-xs')
|
||||
break
|
||||
case 'large':
|
||||
classes.push('p-4', 'text-base')
|
||||
break
|
||||
default:
|
||||
classes.push('p-3', 'text-sm')
|
||||
}
|
||||
|
||||
if (props.variant === 'filled') {
|
||||
classes.push('bg-gray-100')
|
||||
}
|
||||
|
||||
if (props.error) {
|
||||
classes.push('border-red-500')
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
classes.push('opacity-50', 'cursor-not-allowed')
|
||||
}
|
||||
|
||||
if (props.customClass) {
|
||||
classes.push(props.customClass)
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
inputValue.value = newValue
|
||||
})
|
||||
|
||||
watch(inputValue, (newValue) => {
|
||||
emit('update:modelValue', newValue)
|
||||
})
|
||||
|
||||
const calculateDropdownPosition = () => {
|
||||
if (!buttonRef.value || !dropdownRef.value) return 'bottom'
|
||||
|
||||
const buttonRect = buttonRef.value.getBoundingClientRect()
|
||||
const dropdownHeight = 300 // تقریبی ارتفاع dropdown
|
||||
const windowHeight = window.innerHeight
|
||||
const spaceBelow = windowHeight - buttonRect.bottom
|
||||
const spaceAbove = buttonRect.top
|
||||
|
||||
if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
|
||||
return 'top'
|
||||
}
|
||||
return 'bottom'
|
||||
}
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (props.disabled) return
|
||||
|
||||
if (isDropdownOpen.value) {
|
||||
closeDropdown()
|
||||
} else {
|
||||
openDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
const openDropdown = () => {
|
||||
isDropdownClosing.value = false
|
||||
isDropdownOpen.value = true
|
||||
searchQuery.value = ''
|
||||
nextTick(() => {
|
||||
dropdownPosition.value = calculateDropdownPosition()
|
||||
const searchInput = document.querySelector('.ui-input-dropdown-search-input')
|
||||
if (searchInput) searchInput.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const closeDropdown = () => {
|
||||
isDropdownClosing.value = true
|
||||
isDropdownOpen.value = false
|
||||
searchQuery.value = ''
|
||||
}
|
||||
|
||||
const onDropdownEnter = (el) => {
|
||||
el.style.animation = 'dropdownSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
}
|
||||
|
||||
const onDropdownLeave = (el) => {
|
||||
el.style.animation = 'dropdownSlideOut 0.2s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
}
|
||||
|
||||
const onDropdownAfterLeave = () => {
|
||||
isDropdownClosing.value = false
|
||||
}
|
||||
|
||||
const closeOnOutsideClick = (event) => {
|
||||
const root = event.target.closest('.ui-input-dropdown-wrapper')
|
||||
if (!root || root !== wrapperEl.value) {
|
||||
closeDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
const onKeydown = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
const selectOption = (option) => {
|
||||
emit('prefix-change', option)
|
||||
closeDropdown()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
|
||||
}
|
||||
|
||||
const handleInput = (event) => {
|
||||
emit('input', event)
|
||||
}
|
||||
|
||||
const handleChange = (event) => {
|
||||
emit('change', event)
|
||||
}
|
||||
|
||||
const handleFocus = (event) => {
|
||||
emit('focus', event)
|
||||
}
|
||||
|
||||
const handleBlur = (event) => {
|
||||
emit('blur', event)
|
||||
}
|
||||
|
||||
const handleKeyup = (event) => {
|
||||
emit('keyup', event)
|
||||
}
|
||||
|
||||
const handleKeydown = (event) => {
|
||||
emit('keydown', event)
|
||||
}
|
||||
|
||||
const handleWindowResize = () => {
|
||||
if (isDropdownOpen.value) {
|
||||
dropdownPosition.value = calculateDropdownPosition()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleWindowResize)
|
||||
document.addEventListener('click', closeOnOutsideClick, true)
|
||||
document.addEventListener('keydown', onKeydown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleWindowResize)
|
||||
document.removeEventListener('click', closeOnOutsideClick, true)
|
||||
document.removeEventListener('keydown', onKeydown)
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user