516 lines
12 KiB
Vue
516 lines
12 KiB
Vue
<template>
|
|
<div class="ui-input-wrapper" :class="directionClass">
|
|
<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', prefixClasses]">
|
|
<i v-if="actualIconEnd" :class="[actualIconEnd, iconColorClass]" class="ui-input-icon-end !mt-2"></i>
|
|
<div
|
|
v-if="isSvgIconEnd && svgIconEndContent"
|
|
class="ui-input-icon-end has-svg"
|
|
:class="iconColorClass"
|
|
v-html="svgIconEndContent"
|
|
/>
|
|
<span v-if="prefixEnd" class="ui-input-prefix-end" :class="prefixColorClass">{{ prefixEnd }}</span>
|
|
<InputText
|
|
:id="id"
|
|
:model-value="displayValue"
|
|
:class="inputClassesNoBorder"
|
|
:placeholder="placeholder"
|
|
:disabled="disabled"
|
|
:readonly="readonly"
|
|
:type="inputType"
|
|
:maxlength="maxlength"
|
|
:minlength="minlength"
|
|
:autocomplete="autocomplete"
|
|
:autofocus="autofocus"
|
|
:spellcheck="spellcheck"
|
|
:tabindex="tabindex"
|
|
@input="handleInput"
|
|
@change="handleChange"
|
|
@focus="handleFocus"
|
|
@blur="handleBlur"
|
|
@keyup="handleKeyup"
|
|
@keydown="handleKeydown"
|
|
/>
|
|
<span v-if="prefixStart" class="ui-input-prefix-start" :class="prefixColorClass">{{ prefixStart }}</span>
|
|
<i v-if="actualIconStart" :class="[actualIconStart, iconColorClass]" class="ui-input-icon-start !mt-2"></i>
|
|
<div
|
|
v-if="isSvgIconStart && svgIconStartContent"
|
|
class="ui-input-icon-start has-svg"
|
|
:class="iconColorClass"
|
|
v-html="svgIconStartContent"
|
|
/>
|
|
<i
|
|
v-if="showPasswordToggle"
|
|
:class="[passwordToggleIcon, iconColorClass]"
|
|
class="ui-input-password-toggle !mt-2 cursor-pointer"
|
|
@click="togglePasswordVisibility"
|
|
></i>
|
|
</div>
|
|
<div v-else-if="prefix" :class="['.ui-input-wrapper-border', props.wrapperBorderClass, directionClass]">
|
|
<span class="ui-input-prefix">{{ prefix }}</span>
|
|
<InputText
|
|
:id="id"
|
|
:model-value="displayValue"
|
|
:class="inputClassesNoBorder"
|
|
:placeholder="placeholder"
|
|
:disabled="disabled"
|
|
:readonly="readonly"
|
|
:type="inputType"
|
|
:maxlength="maxlength"
|
|
:minlength="minlength"
|
|
:autocomplete="autocomplete"
|
|
:autofocus="autofocus"
|
|
:spellcheck="spellcheck"
|
|
:tabindex="tabindex"
|
|
@input="handleInput"
|
|
@change="handleChange"
|
|
@focus="handleFocus"
|
|
@blur="handleBlur"
|
|
@keyup="handleKeyup"
|
|
@keydown="handleKeydown"
|
|
/>
|
|
<i
|
|
v-if="showPasswordToggle"
|
|
:class="[passwordToggleIcon, iconColorClass]"
|
|
class="ui-input-password-toggle !mt-2 cursor-pointer"
|
|
@click="togglePasswordVisibility"
|
|
></i>
|
|
</div>
|
|
<div v-else class="relative">
|
|
<InputText
|
|
:id="id"
|
|
:model-value="displayValue"
|
|
:class="inputClasses"
|
|
:placeholder="placeholder"
|
|
:disabled="disabled"
|
|
:readonly="readonly"
|
|
:type="inputType"
|
|
:maxlength="maxlength"
|
|
:minlength="minlength"
|
|
:autocomplete="autocomplete"
|
|
:autofocus="autofocus"
|
|
:spellcheck="spellcheck"
|
|
:tabindex="tabindex"
|
|
@input="handleInput"
|
|
@change="handleChange"
|
|
@focus="handleFocus"
|
|
@blur="handleBlur"
|
|
@keyup="handleKeyup"
|
|
@keydown="handleKeydown"
|
|
/>
|
|
<i
|
|
v-if="showPasswordToggle"
|
|
:class="[passwordToggleIcon, iconColorClass]"
|
|
class="ui-input-password-toggle absolute top-1/2 left-3 transform -translate-y-1/2 cursor-pointer"
|
|
@click="togglePasswordVisibility"
|
|
></i>
|
|
</div>
|
|
</div>
|
|
|
|
<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 } from 'vue'
|
|
import InputText from 'primevue/inputtext'
|
|
import { convertPersianToEnglish } from '../../utils/numberUtils.js'
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: [String, Number],
|
|
default: ''
|
|
},
|
|
label: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
placeholder: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
type: {
|
|
type: String,
|
|
default: 'text',
|
|
validator: (value) => ['text', 'password', 'email', 'number', 'tel', 'url', 'search'].includes(value)
|
|
},
|
|
id: {
|
|
type: String,
|
|
default: () => `input-${Math.random().toString(36).substr(2, 9)}`
|
|
},
|
|
required: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
readonly: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
error: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
hint: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
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
|
|
},
|
|
size: {
|
|
type: String,
|
|
default: 'default',
|
|
validator: (value) => ['small', 'default', 'large'].includes(value)
|
|
},
|
|
variant: {
|
|
type: String,
|
|
default: 'default',
|
|
validator: (value) => ['default', 'outlined', 'filled'].includes(value)
|
|
},
|
|
customClass: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
prefix: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
prefixStart: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
prefixEnd: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
iconStart: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
iconEnd: {
|
|
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'
|
|
},
|
|
passwordDisplayType: {
|
|
type: String,
|
|
default: 'dots',
|
|
validator: (value) => ['default', 'dots', 'asterisks'].includes(value)
|
|
},
|
|
convertPersianNumbers: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
wrapperBorderClass: {
|
|
type: String,
|
|
default: 'ui-input-wrapper-border'
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits([
|
|
'update:modelValue',
|
|
'input',
|
|
'change',
|
|
'focus',
|
|
'blur',
|
|
'keyup',
|
|
'keydown'
|
|
])
|
|
|
|
const inputValue = ref(props.modelValue)
|
|
const passwordVisible = ref(false)
|
|
|
|
watch(() => props.modelValue, (newValue) => {
|
|
inputValue.value = newValue
|
|
})
|
|
|
|
watch(inputValue, (newValue) => {
|
|
emit('update:modelValue', newValue)
|
|
})
|
|
|
|
const directionClass = computed(() => {
|
|
return props.direction === 'ltr' ? 'ui-input--ltr' : 'ui-input--rtl'
|
|
})
|
|
|
|
const hasPrefixes = computed(() => {
|
|
return props.prefixStart || props.prefixEnd || props.iconStart || props.iconEnd
|
|
})
|
|
|
|
const isSvgIconStart = computed(() => {
|
|
return props.iconStart && props.iconStart.startsWith('svg:')
|
|
})
|
|
|
|
const isSvgIconEnd = computed(() => {
|
|
return props.iconEnd && props.iconEnd.startsWith('svg:')
|
|
})
|
|
|
|
const svgIconStartName = computed(() => {
|
|
return isSvgIconStart.value ? props.iconStart.replace('svg:', '') : ''
|
|
})
|
|
|
|
const svgIconEndName = computed(() => {
|
|
return isSvgIconEnd.value ? props.iconEnd.replace('svg:', '') : ''
|
|
})
|
|
|
|
const actualIconStart = computed(() => {
|
|
return isSvgIconStart.value ? '' : props.iconStart
|
|
})
|
|
|
|
const actualIconEnd = computed(() => {
|
|
return isSvgIconEnd.value ? '' : props.iconEnd
|
|
})
|
|
|
|
const showPasswordToggle = computed(() => {
|
|
return props.type === 'password'
|
|
})
|
|
|
|
const inputType = computed(() => {
|
|
if (props.type === 'password') {
|
|
if (passwordVisible.value || props.passwordDisplayType !== 'default') {
|
|
return 'text'
|
|
}
|
|
return 'password'
|
|
}
|
|
return props.type
|
|
})
|
|
|
|
const displayValue = computed(() => {
|
|
if (props.type === 'password' && !passwordVisible.value && inputValue.value) {
|
|
if (props.passwordDisplayType === 'dots') {
|
|
return '•'.repeat(inputValue.value.length)
|
|
} else if (props.passwordDisplayType === 'asterisks') {
|
|
return '*'.repeat(inputValue.value.length)
|
|
}
|
|
}
|
|
return inputValue.value
|
|
})
|
|
|
|
const passwordToggleIcon = computed(() => {
|
|
return passwordVisible.value ? 'fi-rr-eye-crossed' : 'fi-rr-eye'
|
|
})
|
|
|
|
const prefixClasses = computed(() => {
|
|
const classes = [directionClass.value]
|
|
|
|
if (props.prefixStart) classes.push('ui-input-with-prefix-start')
|
|
if (props.prefixEnd) classes.push('ui-input-with-prefix-end')
|
|
if (props.iconStart || isSvgIconStart.value) classes.push('ui-input-with-icon-start')
|
|
if (props.iconEnd || isSvgIconEnd.value) classes.push('ui-input-with-icon-end')
|
|
if (showPasswordToggle.value) classes.push('ui-input-with-password-toggle')
|
|
|
|
if (props.wrapperBorderClass) {
|
|
classes.push(props.wrapperBorderClass + ' ui-input-wrapper-border')
|
|
}
|
|
|
|
return classes.join(' ')
|
|
})
|
|
|
|
const prefixColorClass = computed(() => {
|
|
return `text-${props.prefixColor}`
|
|
})
|
|
|
|
const iconColorClass = computed(() => {
|
|
return `text-${props.iconColor}`
|
|
})
|
|
|
|
const loadSvgContent = async (svgName) => {
|
|
try {
|
|
const svgModule = await import(`../../components/svg/${svgName}.svg?raw`)
|
|
return svgModule.default
|
|
} catch (error) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
const svgIconStartContent = ref('')
|
|
const svgIconEndContent = ref('')
|
|
|
|
watch(() => svgIconStartName.value, async (newName) => {
|
|
if (newName) {
|
|
const content = await loadSvgContent(newName)
|
|
svgIconStartContent.value = content
|
|
} else {
|
|
svgIconStartContent.value = ''
|
|
}
|
|
}, { immediate: true })
|
|
|
|
watch(() => svgIconEndName.value, async (newName) => {
|
|
if (newName) {
|
|
const content = await loadSvgContent(newName)
|
|
svgIconEndContent.value = content
|
|
} else {
|
|
svgIconEndContent.value = ''
|
|
}
|
|
}, { immediate: true })
|
|
|
|
const togglePasswordVisibility = () => {
|
|
passwordVisible.value = !passwordVisible.value
|
|
}
|
|
|
|
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(' ')
|
|
})
|
|
|
|
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')
|
|
}
|
|
|
|
switch (props.variant) {
|
|
case 'outlined':
|
|
classes.push('border-2')
|
|
break
|
|
case 'filled':
|
|
classes.push('bg-gray-100')
|
|
break
|
|
default:
|
|
classes.push('border')
|
|
}
|
|
|
|
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 handleInput = (event) => {
|
|
let newValue = event.target.value
|
|
|
|
if (props.convertPersianNumbers) {
|
|
newValue = convertPersianToEnglish(newValue)
|
|
event.target.value = newValue
|
|
}
|
|
|
|
if (props.type === 'password' && !passwordVisible.value && props.passwordDisplayType !== 'default') {
|
|
const currentLength = inputValue.value.length
|
|
const newLength = newValue.length
|
|
|
|
if (newLength > currentLength) {
|
|
inputValue.value = inputValue.value + newValue.slice(currentLength)
|
|
} else if (newLength < currentLength) {
|
|
inputValue.value = inputValue.value.slice(0, newLength)
|
|
}
|
|
} else {
|
|
inputValue.value = newValue
|
|
}
|
|
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)
|
|
}
|
|
</script>
|
|
|
|
|