complete Complete User Request Submission section RAD-1
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import {Link} from "@inertiajs/inertia-vue3";
|
||||
const props = defineProps([
|
||||
'currentPage',
|
||||
'lastPage',
|
||||
'total',
|
||||
'perPage',
|
||||
'links',
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="pagination" v-if="lastPage > 1">
|
||||
<template v-for="(link , index) in links" :key="index">
|
||||
<div class="paginate" v-if="link.label == 'قبلی'">
|
||||
<Link v-if="link.url" :href="link.url">
|
||||
<i class="fi fi-rr-angle-small-right flex text-xl"></i>
|
||||
</Link>
|
||||
</div>
|
||||
<div class="paginate" v-else-if="link.label == 'بعدی'">
|
||||
<Link v-if="link.url" :href="link.url">
|
||||
<i class="fi fi-rr-angle-small-left flex text-xl"></i>
|
||||
</Link>
|
||||
</div>
|
||||
<div class="paginate flex items-center" :class="{'!text-white !bg-client-primary' : currentPage == link.label }" v-else>
|
||||
<Link v-if="link.url" :href="link.url" class="flex pt-1">{{link.label}}</Link>
|
||||
<Link v-else href="" class="flex">{{link.label}}</Link>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
section.pagination{
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
section.pagination div.paginate:first-child a{
|
||||
border-top-right-radius: .4rem;
|
||||
border-bottom-right-radius: .4rem;
|
||||
}
|
||||
section.pagination div.paginate:last-child a{
|
||||
border-top-left-radius: .4rem;
|
||||
border-bottom-left-radius: .4rem;
|
||||
}
|
||||
section.pagination div.paginate a{
|
||||
display: flex;
|
||||
height: 2.2rem;
|
||||
width: 2.2rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #e6e6e6;
|
||||
@apply text-client-Fontcolor
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,179 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import Pagination from './Pagination.vue';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Toolbar from 'primevue/toolbar';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
pagination: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
searchValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'selection-change', 'page-change', 'search-change']);
|
||||
|
||||
const localSelection = ref(props.modelValue);
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
localSelection.value = newValue;
|
||||
}, { immediate: true });
|
||||
|
||||
const handleSelectionChange = (event) => {
|
||||
localSelection.value = event;
|
||||
emit('update:modelValue', event);
|
||||
emit('selection-change', event);
|
||||
};
|
||||
|
||||
const localSearchValue = ref(props.searchValue);
|
||||
|
||||
watch(() => props.searchValue, (newValue) => {
|
||||
localSearchValue.value = newValue;
|
||||
}, { immediate: true });
|
||||
|
||||
|
||||
const handleSearchClick = () => {
|
||||
emit('search-change', localSearchValue.value);
|
||||
};
|
||||
|
||||
const renderColumnContent = (column, data) => {
|
||||
let content = '';
|
||||
if (column.render && typeof column.render === 'function') {
|
||||
content = column.render(data);
|
||||
} else {
|
||||
content = data[column.key];
|
||||
}
|
||||
|
||||
const defaultStyle = '!text-[11px] !font-normal';
|
||||
const customStyle = column.style || defaultStyle;
|
||||
|
||||
return `<span class="${customStyle} text-right wrap-break-word text-sm">${content}</span>`;
|
||||
};
|
||||
|
||||
const getColumnStyle = (column) => {
|
||||
const baseStyle = { minWidth: '120px' };
|
||||
|
||||
if (column.width) {
|
||||
baseStyle.width = column.width + 'px';
|
||||
baseStyle.minWidth = column.width + 'px';
|
||||
baseStyle.maxWidth = column.width + 'px';
|
||||
}
|
||||
|
||||
return baseStyle;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="table-generic">
|
||||
<div v-if="searchable" class="p-4">
|
||||
<Toolbar class="">
|
||||
<template #start>
|
||||
<div class="grid grid-cols-2a justify-start gap-2">
|
||||
<InputText
|
||||
size="small"
|
||||
class="placeholder:!font-light placeholder:!text-xs"
|
||||
placeholder="جستجو..."
|
||||
type="text"
|
||||
v-model="localSearchValue"
|
||||
/>
|
||||
<Button
|
||||
:pt="{icon : 'flex'}"
|
||||
icon="fi fi-rr-search"
|
||||
class="!text-xs"
|
||||
severity="secondary"
|
||||
@click="handleSearchClick"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="localSelection && localSelection.length > 0" class="mr-2 !h-9 grid grid-cols-1">
|
||||
<slot name="bulk-action" :selection="localSelection"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<slot name="toolbar-actions"></slot>
|
||||
</template>
|
||||
</Toolbar>
|
||||
</div>
|
||||
<DataTable
|
||||
:value="data"
|
||||
dataKey="id"
|
||||
v-model:selection="localSelection"
|
||||
:showGridlines="false"
|
||||
:stripedRows="false"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="flex justify-center py-4 items-center h-full">
|
||||
<span class="text-center text-sm">هیچ دادهای پیدا نشده!</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column
|
||||
v-if="selectable"
|
||||
selectionMode="multiple"
|
||||
headerStyle="width: 4rem"
|
||||
/>
|
||||
|
||||
<Column
|
||||
v-for="column in columns"
|
||||
:key="column.key"
|
||||
:field="column.key"
|
||||
:header="column.title"
|
||||
:sortable="column.hasSort === true"
|
||||
:style="getColumnStyle(column)"
|
||||
>
|
||||
<template #body="{ data }">
|
||||
<slot v-if="column.slot" :name="`column-${column.key}`" :data="data" :column="column">
|
||||
<div v-html="renderColumnContent(column, data)"></div>
|
||||
</slot>
|
||||
<div v-else v-html="renderColumnContent(column, data)"></div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
||||
<Pagination
|
||||
v-if="pagination && pagination.total"
|
||||
:total="pagination.total"
|
||||
:current-page="pagination.current_page"
|
||||
:perPage="pagination.per_page"
|
||||
:lastPage="pagination.last_page"
|
||||
:links="pagination.links"
|
||||
@page-change="(page) => emit('page-change', page)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-generic {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="ui-button">
|
||||
<Button
|
||||
:class="buttonClasses"
|
||||
:style="buttonStyle"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
:label="buttonLabel"
|
||||
:size="size"
|
||||
:severity="severity"
|
||||
:outlined="outlined"
|
||||
:text="text"
|
||||
:raised="raised"
|
||||
:rounded="rounded"
|
||||
:link="link"
|
||||
:plain="plain"
|
||||
:pt="pt"
|
||||
@click="handleClick"
|
||||
>
|
||||
<template v-if="icon && iconPosition === 'start'" #icon>
|
||||
<i :class="[icon, iconClass]" class="ui-button-icon"></i>
|
||||
</template>
|
||||
|
||||
<template v-if="icon && iconPosition === 'end'" #default>
|
||||
{{ label }}
|
||||
<i :class="[icon, iconClass]" class="ui-button-icon-end"></i>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
iconPosition: {
|
||||
type: String,
|
||||
default: 'start',
|
||||
validator: (value) => ['start', 'end'].includes(value)
|
||||
},
|
||||
iconClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value) => ['small', 'default', 'large'].includes(value)
|
||||
},
|
||||
severity: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator: (value) => ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'dominant'].includes(value)
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'filled',
|
||||
validator: (value) => ['filled', 'outlined', 'text', 'link', 'plain'].includes(value)
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
outlined: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
text: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
raised: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
link: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
plain: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
pt: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const buttonClasses = computed(() => {
|
||||
const classes = []
|
||||
|
||||
if (props.fullWidth) {
|
||||
classes.push('ui-button--full-width')
|
||||
}
|
||||
|
||||
if (props.severity === 'dominant') {
|
||||
classes.push('ui-button--dominant')
|
||||
}
|
||||
if (props.severity === 'secondary') {
|
||||
classes.push('!font-[600] !text-[11px] !rounded-[6px]')
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
})
|
||||
|
||||
const buttonLabel = computed(() => {
|
||||
if (props.icon && props.iconPosition === 'end') {
|
||||
return ''
|
||||
}
|
||||
return props.label
|
||||
})
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
const style = {}
|
||||
|
||||
if (props.height) {
|
||||
style.height = props.height
|
||||
}
|
||||
|
||||
return style
|
||||
})
|
||||
|
||||
const handleClick = (event) => {
|
||||
emit('click', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<Card :class="[cardClasses, 'border-[1px] border-complement-50']" :pt="pt">
|
||||
<template #header>
|
||||
<div class="ui-card-header grid grid-cols-a pb-2 justify-start items-center">
|
||||
<h1 class="font-bold text-[11px] text-complement-700 title-generic">{{ title }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<slot />
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Card from 'primevue/card'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value) => ['default', 'elevated', 'outlined', 'filled'].includes(value)
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value) => ['small', 'default', 'large'].includes(value)
|
||||
},
|
||||
shadow: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value) => ['none', 'sm', 'default', 'md', 'lg', 'xl'].includes(value)
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hover: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
pt: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const cardClasses = computed(() => {
|
||||
const classes = ['ui-card']
|
||||
|
||||
switch (props.size) {
|
||||
case 'small':
|
||||
classes.push('ui-card--small')
|
||||
break
|
||||
case 'large':
|
||||
classes.push('ui-card--large')
|
||||
break
|
||||
default:
|
||||
classes.push('ui-card--default')
|
||||
}
|
||||
|
||||
switch (props.variant) {
|
||||
case 'elevated':
|
||||
classes.push('ui-card--elevated')
|
||||
break
|
||||
case 'outlined':
|
||||
classes.push('ui-card--outlined')
|
||||
break
|
||||
case 'filled':
|
||||
classes.push('ui-card--filled')
|
||||
break
|
||||
default:
|
||||
classes.push('ui-card--default')
|
||||
}
|
||||
|
||||
if (props.shadow !== 'none') {
|
||||
switch (props.shadow) {
|
||||
case 'sm':
|
||||
classes.push('ui-card--shadow-sm')
|
||||
break
|
||||
case 'md':
|
||||
classes.push('ui-card--shadow-md')
|
||||
break
|
||||
case 'lg':
|
||||
classes.push('ui-card--shadow-lg')
|
||||
break
|
||||
case 'xl':
|
||||
classes.push('ui-card--shadow-xl')
|
||||
break
|
||||
default:
|
||||
classes.push('ui-card--shadow')
|
||||
}
|
||||
}
|
||||
|
||||
if (!props.border) {
|
||||
classes.push('ui-card--no-border')
|
||||
}
|
||||
|
||||
if (props.rounded) {
|
||||
classes.push('ui-card--rounded')
|
||||
}
|
||||
|
||||
if (props.hover) {
|
||||
classes.push('ui-card--hover')
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,515 @@
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div class="ui-select-wrapper" :class="directionClass">
|
||||
<label v-if="label" :for="id" class="ui-select-label">
|
||||
{{ label }}
|
||||
<span v-if="required" class="ui-select-required-indicator">*</span>
|
||||
</label>
|
||||
|
||||
<div class="ui-select">
|
||||
<div v-if="hasPrefixes" class="ui-select-wrapper-border" :class="prefixClasses">
|
||||
<template v-if="direction === 'rtl'">
|
||||
<i v-if="iconEnd" :class="[iconEnd, iconColorClass]" class="ui-select-icon-end !mt-2"></i>
|
||||
<span v-if="prefixEnd" class="ui-select-prefix-end" :class="prefixColorClass">{{ prefixEnd }}</span>
|
||||
<Dropdown
|
||||
:id="id"
|
||||
:model-value="internalModelValue"
|
||||
:options="options"
|
||||
:option-label="optionLabel"
|
||||
:option-value="optionValue"
|
||||
:dataKey="optionValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="selectClassesNoBorder"
|
||||
empty-filter-message="نتیجهای یافت نشد"
|
||||
:empty-message="emptyMessage"
|
||||
:filter="filter"
|
||||
:reset-filter-on-hide="true"
|
||||
:filter-placeholder="filterPlaceholder"
|
||||
:auto-filter-focus="false"
|
||||
:show-clear="showClear"
|
||||
class="ui-select-dropdown"
|
||||
@update:model-value="handleUpdate"
|
||||
@change="handleUpdate"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
<span v-if="prefixStart" class="ui-select-prefix-start" :class="prefixColorClass">{{ prefixStart }}</span>
|
||||
<i v-if="iconStart" :class="[iconStart, iconColorClass]" class="ui-select-icon-start !mt-2"></i>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<i v-if="iconStart" :class="[iconStart, iconColorClass]" class="ui-select-icon-start !mt-2"></i>
|
||||
<span v-if="prefixStart" class="ui-select-prefix-start" :class="prefixColorClass">{{ prefixStart }}</span>
|
||||
<Dropdown
|
||||
:id="id"
|
||||
:model-value="internalModelValue"
|
||||
:options="options"
|
||||
:option-label="optionLabel"
|
||||
:option-value="optionValue"
|
||||
:dataKey="optionValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="selectClassesNoBorder"
|
||||
empty-filter-message="نتیجهای یافت نشد"
|
||||
:empty-message="emptyMessage"
|
||||
:filter="filter"
|
||||
:reset-filter-on-hide="true"
|
||||
:filter-placeholder="filterPlaceholder"
|
||||
:auto-filter-focus="false"
|
||||
:show-clear="showClear"
|
||||
class="ui-select-dropdown"
|
||||
@update:model-value="handleUpdate"
|
||||
@change="handleUpdate"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
<span v-if="prefixEnd" class="ui-select-prefix-end" :class="prefixColorClass">{{ prefixEnd }}</span>
|
||||
<i v-if="iconEnd" :class="[iconEnd, iconColorClass]" class="ui-select-icon-end !mt-2"></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-else-if="prefix" class="ui-select-wrapper-border" :class="directionClass">
|
||||
<span class="ui-select-prefix">{{ prefix }}</span>
|
||||
<Dropdown
|
||||
:id="id"
|
||||
:model-value="internalModelValue"
|
||||
:options="options"
|
||||
:option-label="optionLabel"
|
||||
:option-value="optionValue"
|
||||
:dataKey="optionValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="selectClassesNoBorder"
|
||||
empty-filter-message="نتیجهای یافت نشد"
|
||||
:empty-message="emptyMessage"
|
||||
:filter="filter"
|
||||
:reset-filter-on-hide="true"
|
||||
:filter-placeholder="filterPlaceholder"
|
||||
:show-clear="showClear"
|
||||
:auto-filter-focus="false"
|
||||
class="ui-select-dropdown"
|
||||
@update:model-value="handleUpdate"
|
||||
@change="handleUpdate"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="relative select">
|
||||
<Dropdown
|
||||
:id="id"
|
||||
:model-value="internalModelValue"
|
||||
:options="options"
|
||||
:option-label="optionLabel"
|
||||
:option-value="optionValue"
|
||||
:dataKey="optionValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="selectClasses"
|
||||
empty-filter-message="نتیجهای یافت نشد"
|
||||
:empty-message="emptyMessage"
|
||||
:filter="filter"
|
||||
:reset-filter-on-hide="true"
|
||||
:filter-placeholder="filterPlaceholder"
|
||||
:show-clear="showClear"
|
||||
class="ui-select-dropdown"
|
||||
@update:model-value="handleUpdate"
|
||||
@change="handleUpdate"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small v-if="error" class="ui-select-error-message">{{ error }}</small>
|
||||
<small v-if="hint" class="ui-select-hint-message">{{ hint }}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: () => `ui-select-${Math.random().toString(36).substr(2, 9)}`
|
||||
},
|
||||
modelValue: { type: [String, Number, Object, null], default: null },
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
optionLabel: { type: String, default: 'name' },
|
||||
optionValue: { type: String, default: undefined },
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'rtl',
|
||||
validator: (value) => ['rtl', 'ltr'].includes(value)
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
prefixStart: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
prefixEnd: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
prefixColor: {
|
||||
type: String,
|
||||
default: 'dominant-600'
|
||||
},
|
||||
iconStart: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
iconEnd: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: 'dominant-600'
|
||||
},
|
||||
filter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
filterPlaceholder: {
|
||||
type: String,
|
||||
default: 'جستجو...'
|
||||
},
|
||||
emptyMessage: {
|
||||
type: String,
|
||||
default: 'نتیجهای یافت نشد'
|
||||
},
|
||||
showClear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue','change','focus','blur'])
|
||||
|
||||
const internalModelValue = computed(() => {
|
||||
if (props.optionValue && props.modelValue && typeof props.modelValue === 'object') {
|
||||
return props.modelValue[props.optionValue]
|
||||
}
|
||||
return props.modelValue
|
||||
})
|
||||
|
||||
const extractValue = (e) => (e && typeof e === 'object' && 'value' in e) ? e.value : e
|
||||
|
||||
const handleUpdate = (e) => {
|
||||
const val = extractValue(e)
|
||||
if (props.optionValue && val !== null && val !== undefined) {
|
||||
const selectedOption = props.options.find(o => o[props.optionValue] === val) ?? null
|
||||
emit('update:modelValue', selectedOption)
|
||||
emit('change', selectedOption)
|
||||
} else {
|
||||
emit('update:modelValue', val)
|
||||
emit('change', val)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFocus = (e) => emit('focus', e)
|
||||
const handleBlur = (e) => emit('blur', e)
|
||||
|
||||
const directionClass = computed(() => {
|
||||
return props.direction === 'ltr' ? 'ui-select--ltr' : 'ui-select--rtl'
|
||||
})
|
||||
|
||||
const hasPrefixes = computed(() => {
|
||||
return props.prefixStart || props.prefixEnd || props.iconStart || props.iconEnd
|
||||
})
|
||||
|
||||
const prefixClasses = computed(() => {
|
||||
const classes = [directionClass.value]
|
||||
|
||||
if (props.prefixStart) classes.push('ui-select-with-prefix-start')
|
||||
if (props.prefixEnd) classes.push('ui-select-with-prefix-end')
|
||||
if (props.iconStart) classes.push('ui-select-with-icon-start')
|
||||
if (props.iconEnd) classes.push('ui-select-with-icon-end')
|
||||
|
||||
return classes.join(' ')
|
||||
})
|
||||
|
||||
const prefixColorClass = computed(() => {
|
||||
return `text-${props.prefixColor}`
|
||||
})
|
||||
|
||||
const iconColorClass = computed(() => {
|
||||
return `text-${props.iconColor}`
|
||||
})
|
||||
|
||||
const selectClassesNoBorder = computed(() => {
|
||||
const classes = []
|
||||
|
||||
if (props.error) {
|
||||
classes.push('has-error')
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
classes.push('disabled')
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
})
|
||||
|
||||
const selectClasses = computed(() => {
|
||||
const classes = []
|
||||
|
||||
if (props.error) {
|
||||
classes.push('has-error')
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
classes.push('disabled')
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user