2020-08-04 15:27:37 +08:00
|
|
|
<template>
|
|
|
|
<div
|
2022-05-05 22:04:32 +08:00
|
|
|
:id="inputId"
|
2022-03-14 19:34:49 +08:00
|
|
|
:class="[rateClasses, ns.is('disabled', rateDisabled)]"
|
2020-08-04 15:27:37 +08:00
|
|
|
role="slider"
|
2022-05-05 22:04:32 +08:00
|
|
|
:aria-label="!isLabeledByFormItem ? label || 'rating' : undefined"
|
2022-07-20 17:37:04 +08:00
|
|
|
:aria-labelledby="
|
|
|
|
isLabeledByFormItem ? formItemContext?.labelId : undefined
|
|
|
|
"
|
2020-08-04 15:27:37 +08:00
|
|
|
:aria-valuenow="currentValue"
|
2022-05-05 22:04:32 +08:00
|
|
|
:aria-valuetext="text || undefined"
|
2020-08-04 15:27:37 +08:00
|
|
|
aria-valuemin="0"
|
|
|
|
:aria-valuemax="max"
|
|
|
|
tabindex="0"
|
2022-03-14 19:34:49 +08:00
|
|
|
:style="rateStyles"
|
2020-08-04 15:27:37 +08:00
|
|
|
@keydown="handleKey"
|
|
|
|
>
|
|
|
|
<span
|
|
|
|
v-for="(item, key) in max"
|
|
|
|
:key="key"
|
2022-01-18 18:14:17 +08:00
|
|
|
:class="ns.e('item')"
|
2020-08-04 15:27:37 +08:00
|
|
|
@mousemove="setCurrentValue(item, $event)"
|
|
|
|
@mouseleave="resetCurrentValue"
|
|
|
|
@click="selectValue(item)"
|
|
|
|
>
|
2021-10-27 23:17:13 +08:00
|
|
|
<el-icon
|
2022-03-14 19:34:49 +08:00
|
|
|
:class="[
|
|
|
|
ns.e('icon'),
|
|
|
|
{ hover: hoverIndex === item },
|
|
|
|
ns.is('active', item <= currentValue),
|
|
|
|
]"
|
2020-08-04 15:27:37 +08:00
|
|
|
>
|
2022-02-16 10:41:59 +08:00
|
|
|
<component
|
|
|
|
:is="iconComponents[item - 1]"
|
|
|
|
v-if="!showDecimalIcon(item)"
|
|
|
|
/>
|
2021-10-27 23:17:13 +08:00
|
|
|
<el-icon
|
2020-08-04 15:27:37 +08:00
|
|
|
v-if="showDecimalIcon(item)"
|
|
|
|
:style="decimalStyle"
|
2022-01-18 18:14:17 +08:00
|
|
|
:class="[ns.e('icon'), ns.e('decimal')]"
|
2020-08-04 15:27:37 +08:00
|
|
|
>
|
2021-10-27 23:17:13 +08:00
|
|
|
<component :is="decimalIconComponent" />
|
|
|
|
</el-icon>
|
|
|
|
</el-icon>
|
2020-08-04 15:27:37 +08:00
|
|
|
</span>
|
2022-03-14 19:34:49 +08:00
|
|
|
<span v-if="showText || showScore" :class="ns.e('text')">
|
2021-11-06 01:55:23 +08:00
|
|
|
{{ text }}
|
|
|
|
</span>
|
2020-08-04 15:27:37 +08:00
|
|
|
</div>
|
|
|
|
</template>
|
2022-03-13 20:20:21 +08:00
|
|
|
<script lang="ts" setup>
|
2022-03-25 15:35:56 +08:00
|
|
|
import { type CSSProperties, computed, inject, ref, watch } from 'vue'
|
2022-02-11 11:03:15 +08:00
|
|
|
import { EVENT_CODE, UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
2022-03-25 15:35:56 +08:00
|
|
|
import { hasClass, isArray, isObject } from '@element-plus/utils'
|
2022-05-05 22:04:32 +08:00
|
|
|
import { formContextKey, formItemContextKey } from '@element-plus/tokens'
|
2021-10-27 23:17:13 +08:00
|
|
|
import { ElIcon } from '@element-plus/components/icon'
|
2022-05-05 22:04:32 +08:00
|
|
|
import { useFormItemInputId, useNamespace, useSize } from '@element-plus/hooks'
|
2022-03-25 15:35:56 +08:00
|
|
|
import { rateEmits, rateProps } from './rate'
|
2022-03-06 22:20:56 +08:00
|
|
|
|
2021-11-06 01:55:23 +08:00
|
|
|
function getValueFromMap<T>(
|
|
|
|
value: number,
|
|
|
|
map: Record<string, T | { excluded?: boolean; value: T }>
|
|
|
|
) {
|
|
|
|
const isExcludedObject = (
|
|
|
|
val: unknown
|
|
|
|
): val is { excluded?: boolean } & Record<any, unknown> => isObject(val)
|
|
|
|
|
|
|
|
const matchedKeys = Object.keys(map)
|
|
|
|
.map((key) => +key)
|
|
|
|
.filter((key) => {
|
|
|
|
const val = map[key]
|
|
|
|
const excluded = isExcludedObject(val) ? val.excluded : false
|
|
|
|
return excluded ? value < key : value <= key
|
|
|
|
})
|
|
|
|
.sort((a, b) => a - b)
|
|
|
|
const matchedValue = map[matchedKeys[0]]
|
2021-11-18 22:33:36 +08:00
|
|
|
return (isExcludedObject(matchedValue) && matchedValue.value) || matchedValue
|
2021-11-06 01:55:23 +08:00
|
|
|
}
|
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
defineOptions({
|
2020-08-04 15:27:37 +08:00
|
|
|
name: 'ElRate',
|
2022-03-13 20:20:21 +08:00
|
|
|
})
|
2021-11-06 01:55:23 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
const props = defineProps(rateProps)
|
|
|
|
const emit = defineEmits(rateEmits)
|
2022-03-06 22:20:56 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
const formContext = inject(formContextKey, undefined)
|
2022-05-05 22:04:32 +08:00
|
|
|
const formItemContext = inject(formItemContextKey, undefined)
|
2022-03-13 20:20:21 +08:00
|
|
|
const rateSize = useSize()
|
|
|
|
const ns = useNamespace('rate')
|
2022-05-05 22:04:32 +08:00
|
|
|
const { inputId, isLabeledByFormItem } = useFormItemInputId(props, {
|
|
|
|
formItemContext,
|
|
|
|
})
|
2022-01-18 18:14:17 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
const currentValue = ref(props.modelValue)
|
|
|
|
const hoverIndex = ref(-1)
|
|
|
|
const pointerAtLeftHalf = ref(true)
|
2020-08-04 15:27:37 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
const rateClasses = computed(() => [ns.b(), ns.m(rateSize.value)])
|
|
|
|
const rateDisabled = computed(() => props.disabled || formContext?.disabled)
|
2022-03-14 19:34:49 +08:00
|
|
|
const rateStyles = computed(() => {
|
2022-04-26 17:19:09 +08:00
|
|
|
return ns.cssVarBlock({
|
|
|
|
'void-color': props.voidColor,
|
|
|
|
'disabled-void-color': props.disabledVoidColor,
|
|
|
|
'fill-color': activeColor.value,
|
|
|
|
}) as CSSProperties
|
2022-03-14 19:34:49 +08:00
|
|
|
})
|
2022-01-07 15:43:38 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
const text = computed(() => {
|
|
|
|
let result = ''
|
|
|
|
if (props.showScore) {
|
|
|
|
result = props.scoreTemplate.replace(
|
|
|
|
/\{\s*value\s*\}/,
|
|
|
|
rateDisabled.value ? `${props.modelValue}` : `${currentValue.value}`
|
2021-09-04 19:29:28 +08:00
|
|
|
)
|
2022-03-13 20:20:21 +08:00
|
|
|
} else if (props.showText) {
|
|
|
|
result = props.texts[Math.ceil(currentValue.value) - 1]
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
})
|
|
|
|
const valueDecimal = computed(
|
|
|
|
() => props.modelValue * 100 - Math.floor(props.modelValue) * 100
|
|
|
|
)
|
|
|
|
const colorMap = computed(() =>
|
|
|
|
isArray(props.colors)
|
|
|
|
? {
|
|
|
|
[props.lowThreshold]: props.colors[0],
|
|
|
|
[props.highThreshold]: { value: props.colors[1], excluded: true },
|
|
|
|
[props.max]: props.colors[2],
|
2020-08-04 15:27:37 +08:00
|
|
|
}
|
2022-03-13 20:20:21 +08:00
|
|
|
: props.colors
|
|
|
|
)
|
2022-03-14 19:34:49 +08:00
|
|
|
const activeColor = computed(() => {
|
|
|
|
const color = getValueFromMap(currentValue.value, colorMap.value)
|
|
|
|
// {value: '', excluded: true} returned
|
|
|
|
return isObject(color) ? '' : color
|
|
|
|
})
|
2022-03-13 20:20:21 +08:00
|
|
|
const decimalStyle = computed(() => {
|
|
|
|
let width = ''
|
|
|
|
if (rateDisabled.value) {
|
|
|
|
width = `${valueDecimal.value}%`
|
|
|
|
} else if (props.allowHalf) {
|
|
|
|
width = '50%'
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
color: activeColor.value,
|
|
|
|
width,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
const componentMap = computed(() =>
|
|
|
|
isArray(props.icons)
|
|
|
|
? {
|
|
|
|
[props.lowThreshold]: props.icons[0],
|
|
|
|
[props.highThreshold]: {
|
|
|
|
value: props.icons[1],
|
|
|
|
excluded: true,
|
|
|
|
},
|
|
|
|
[props.max]: props.icons[2],
|
2020-08-04 15:27:37 +08:00
|
|
|
}
|
2022-03-13 20:20:21 +08:00
|
|
|
: props.icons
|
|
|
|
)
|
|
|
|
const decimalIconComponent = computed(() =>
|
|
|
|
getValueFromMap(props.modelValue, componentMap.value)
|
|
|
|
)
|
|
|
|
const voidComponent = computed(() =>
|
|
|
|
rateDisabled.value ? props.disabledVoidIcon : props.voidIcon
|
|
|
|
)
|
|
|
|
const activeComponent = computed(() =>
|
|
|
|
getValueFromMap(currentValue.value, componentMap.value)
|
|
|
|
)
|
|
|
|
const iconComponents = computed(() => {
|
|
|
|
const result = Array.from({ length: props.max })
|
|
|
|
const threshold = currentValue.value
|
|
|
|
result.fill(activeComponent.value, 0, threshold)
|
|
|
|
result.fill(voidComponent.value, threshold, props.max)
|
|
|
|
return result
|
|
|
|
})
|
2020-08-04 15:27:37 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
function showDecimalIcon(item: number) {
|
|
|
|
const showWhenDisabled =
|
|
|
|
rateDisabled.value &&
|
|
|
|
valueDecimal.value > 0 &&
|
|
|
|
item - 1 < props.modelValue &&
|
|
|
|
item > props.modelValue
|
|
|
|
const showWhenAllowHalf =
|
|
|
|
props.allowHalf &&
|
|
|
|
pointerAtLeftHalf.value &&
|
|
|
|
item - 0.5 <= currentValue.value &&
|
|
|
|
item > currentValue.value
|
|
|
|
return showWhenDisabled || showWhenAllowHalf
|
|
|
|
}
|
2020-08-04 15:27:37 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
function selectValue(value: number) {
|
|
|
|
if (rateDisabled.value) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (props.allowHalf && pointerAtLeftHalf.value) {
|
|
|
|
emit(UPDATE_MODEL_EVENT, currentValue.value)
|
|
|
|
if (props.modelValue !== currentValue.value) {
|
|
|
|
emit('change', currentValue.value)
|
2020-08-04 15:27:37 +08:00
|
|
|
}
|
2022-03-13 20:20:21 +08:00
|
|
|
} else {
|
|
|
|
emit(UPDATE_MODEL_EVENT, value)
|
|
|
|
if (props.modelValue !== value) {
|
|
|
|
emit('change', value)
|
2020-08-04 15:27:37 +08:00
|
|
|
}
|
2022-03-13 20:20:21 +08:00
|
|
|
}
|
|
|
|
}
|
2020-08-04 15:27:37 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
function handleKey(e: KeyboardEvent) {
|
|
|
|
if (rateDisabled.value) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let _currentValue = currentValue.value
|
|
|
|
const code = e.code
|
|
|
|
if (code === EVENT_CODE.up || code === EVENT_CODE.right) {
|
|
|
|
if (props.allowHalf) {
|
|
|
|
_currentValue += 0.5
|
|
|
|
} else {
|
|
|
|
_currentValue += 1
|
2020-08-04 15:27:37 +08:00
|
|
|
}
|
2022-03-13 20:20:21 +08:00
|
|
|
e.stopPropagation()
|
|
|
|
e.preventDefault()
|
|
|
|
} else if (code === EVENT_CODE.left || code === EVENT_CODE.down) {
|
|
|
|
if (props.allowHalf) {
|
|
|
|
_currentValue -= 0.5
|
|
|
|
} else {
|
|
|
|
_currentValue -= 1
|
|
|
|
}
|
|
|
|
e.stopPropagation()
|
|
|
|
e.preventDefault()
|
|
|
|
}
|
|
|
|
_currentValue = _currentValue < 0 ? 0 : _currentValue
|
|
|
|
_currentValue = _currentValue > props.max ? props.max : _currentValue
|
|
|
|
emit(UPDATE_MODEL_EVENT, _currentValue)
|
|
|
|
emit('change', _currentValue)
|
|
|
|
return _currentValue
|
|
|
|
}
|
2020-08-04 15:27:37 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
function setCurrentValue(value: number, event: MouseEvent) {
|
|
|
|
if (rateDisabled.value) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (props.allowHalf) {
|
|
|
|
// TODO: use cache via computed https://github.com/element-plus/element-plus/pull/5456#discussion_r786472092
|
|
|
|
let target = event.target as HTMLElement
|
|
|
|
if (hasClass(target, ns.e('item'))) {
|
|
|
|
target = target.querySelector(`.${ns.e('icon')}`)!
|
|
|
|
}
|
|
|
|
if (target.clientWidth === 0 || hasClass(target, ns.e('decimal'))) {
|
|
|
|
target = target.parentNode as HTMLElement
|
2020-08-04 15:27:37 +08:00
|
|
|
}
|
2022-03-13 20:20:21 +08:00
|
|
|
pointerAtLeftHalf.value = event.offsetX * 2 <= target.clientWidth
|
|
|
|
currentValue.value = pointerAtLeftHalf.value ? value - 0.5 : value
|
|
|
|
} else {
|
|
|
|
currentValue.value = value
|
|
|
|
}
|
|
|
|
hoverIndex.value = value
|
|
|
|
}
|
2020-08-04 15:27:37 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
function resetCurrentValue() {
|
|
|
|
if (rateDisabled.value) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (props.allowHalf) {
|
|
|
|
pointerAtLeftHalf.value = props.modelValue !== Math.floor(props.modelValue)
|
|
|
|
}
|
|
|
|
currentValue.value = props.modelValue
|
|
|
|
hoverIndex.value = -1
|
|
|
|
}
|
2021-11-06 01:55:23 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
watch(
|
|
|
|
() => props.modelValue,
|
|
|
|
(val) => {
|
|
|
|
currentValue.value = val
|
|
|
|
pointerAtLeftHalf.value = props.modelValue !== Math.floor(props.modelValue)
|
|
|
|
}
|
|
|
|
)
|
2021-11-06 01:55:23 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
if (!props.modelValue) {
|
|
|
|
emit(UPDATE_MODEL_EVENT, 0)
|
|
|
|
}
|
2020-08-04 15:27:37 +08:00
|
|
|
|
2022-03-13 20:20:21 +08:00
|
|
|
defineExpose({
|
|
|
|
/** @description set current value */
|
|
|
|
setCurrentValue,
|
|
|
|
/** @description reset current value */
|
|
|
|
resetCurrentValue,
|
2020-08-04 15:27:37 +08:00
|
|
|
})
|
|
|
|
</script>
|