136 lines
3 KiB
Vue
136 lines
3 KiB
Vue
<template>
|
|
<div
|
|
ref="wrapperRef"
|
|
:class="`rating-stars-wrapper position-relative bg-tertiary ${wrapperClass} px-2`"
|
|
@mouseleave="hovered = 0"
|
|
@focusout="onFocusOut"
|
|
>
|
|
<!-- Yellow background -->
|
|
<div class="position-relative h-100">
|
|
<div
|
|
class="position-absolute bg-warning h-100"
|
|
:style="`width: ${(computedRating / 5) * 100}%;`"
|
|
/>
|
|
</div>
|
|
<!-- SVG mask with cut-out stars -->
|
|
<svg
|
|
class="position-absolute top-0 start-0"
|
|
viewBox="0 0 142 32"
|
|
>
|
|
<defs>
|
|
<mask
|
|
id="star-mask"
|
|
maskUnits="userSpaceOnUse"
|
|
>
|
|
<rect
|
|
class="w-100 h-100"
|
|
fill="white"
|
|
/>
|
|
<g>
|
|
<template
|
|
v-for="i in 5"
|
|
:key="i"
|
|
>
|
|
<path
|
|
:transform="`translate(${10 + (i - 1) * 25},5)`"
|
|
d="M11.0905 0.45459L14.654 6.1861L21.2062 7.80414L16.8565 12.9644L17.3424 19.696L11.0905 17.1537L4.83857 19.696L5.32447 12.9644L0.974682 7.80414L7.52688 6.1861L11.0905 0.45459Z"
|
|
fill="black"
|
|
/>
|
|
</template>
|
|
</g>
|
|
</mask>
|
|
</defs>
|
|
<rect
|
|
class="w-100 h-100"
|
|
fill="white"
|
|
mask="url(#star-mask)"
|
|
/>
|
|
</svg>
|
|
<!-- Selected div when read only is false -->
|
|
<div
|
|
v-if="!readOnly"
|
|
class="buttons-wrapper position-absolute top-0 start-0 d-flex justify-content-center px-2"
|
|
>
|
|
<template
|
|
v-for="i in 5"
|
|
:key="i"
|
|
>
|
|
<button
|
|
@click="selectRating(i)"
|
|
@mouseover="hovered = i"
|
|
@focus="hovered = i"
|
|
class="btn btn-warning bg-transparent border-0 p-0"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, computed, defineProps, defineEmits, watch } from "vue";
|
|
|
|
const emit = defineEmits(["update:rating"]);
|
|
|
|
const props = defineProps({
|
|
rating: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
wrapperClass: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
readOnly: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
});
|
|
|
|
const wrapperRef = ref<HTMLElement | null>(null);
|
|
|
|
const hovered = ref(0);
|
|
const selected = ref<number | null>(props.rating);
|
|
|
|
const computedRating = computed(() => {
|
|
if (props.readOnly) return props.rating ?? 0;
|
|
return hovered.value || selected.value || 0;
|
|
});
|
|
|
|
watch(() => props.rating, (val) => {
|
|
selected.value = val;
|
|
});
|
|
|
|
const selectRating = (value: number) => {
|
|
if (!props.readOnly) {
|
|
selected.value = value;
|
|
emit("update:rating", value);
|
|
}
|
|
};
|
|
|
|
const onFocusOut = (e: FocusEvent) => {
|
|
const newFocused = e.relatedTarget as HTMLElement | null;
|
|
if (!wrapperRef.value?.contains(newFocused)) {
|
|
hovered.value = 0;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.rating-stars-wrapper {
|
|
&, svg {
|
|
width: 142px;
|
|
height: 32px;
|
|
}
|
|
.buttons-wrapper {
|
|
width: 142px;
|
|
height: 32px;
|
|
z-index: 10;
|
|
button {
|
|
flex: 1;
|
|
&:not(:focus) {
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|