|
|
@@ -1,30 +1,69 @@
|
|
|
<template>
|
|
|
<span
|
|
|
- class="star-rating"
|
|
|
+ class="star-rating text-warning"
|
|
|
:class="sizeClass"
|
|
|
:style="{ cursor: disabled ? 'default' : 'pointer' }"
|
|
|
@click="handleClick"
|
|
|
+ @mouseover="handleHover($event)"
|
|
|
+ @mouseleave="hovered = 0"
|
|
|
>
|
|
|
- <i v-for="i in max" :key="i" class="star" :class="starClass(i)"></i>
|
|
|
+ <span v-for="i in max" :key="i" class="star">
|
|
|
+ <svg v-if="isFull(i)" class="fill" viewBox="0 0 576 512" aria-hidden="true"><path :d="starPath" /></svg>
|
|
|
+ <svg v-else-if="isHalf(i)" class="fill half" viewBox="0 0 576 512" aria-hidden="true"><path :d="starPath" /></svg>
|
|
|
+ <svg class="base" viewBox="0 0 576 512" aria-hidden="true"><path :d="starPath" /></svg>
|
|
|
+ </span>
|
|
|
+ <i
|
|
|
+ v-if="!disabled && value > 0"
|
|
|
+ class="fa-solid fa-circle-xmark text-secondary ms-1 star-clear"
|
|
|
+ role="button"
|
|
|
+ title="Supprimer la note"
|
|
|
+ @click.stop="clearNote"
|
|
|
+ ></i>
|
|
|
</span>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, watch, computed } from 'vue';
|
|
|
+import { ref, watch, computed, onMounted } from 'vue';
|
|
|
|
|
|
const props = defineProps({
|
|
|
value: { type: Number, default: 0 },
|
|
|
max: { type: Number, default: 5 },
|
|
|
step: { type: Number, default: 1 },
|
|
|
disabled: { type: Boolean, default: false },
|
|
|
- size: { type: String, default: 'md' }
|
|
|
+ size: { type: String, default: 'md' },
|
|
|
+ // Optionnel : id d'un input (caché) dont la valeur doit rester synchronisée
|
|
|
+ input: { type: String, default: '' }
|
|
|
});
|
|
|
|
|
|
const emit = defineEmits(['change']);
|
|
|
|
|
|
+// Chemin de l'étoile Font Awesome (icône "star", viewBox 0 0 576 512)
|
|
|
+const starPath = 'M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-21.9 10.5-25.7 22.2s-2 24.5 6.9 33.3l104.8 104.5-24.8 145.5c-2 12 3 24.2 12.9 31.6s23 8 33.9 2.3l128.6-68.5 128.6 68.5c10.8 5.7 23.9 4.9 33.9-2.3s14.9-19.3 12.9-31.6L438.5 381.7 543.3 277.2c8.9-8.9 10.7-22.1 6.9-33.3s-13.7-20.4-25.7-22.2L380.9 150.3 316.9 18z';
|
|
|
+
|
|
|
const value = ref(props.value);
|
|
|
watch(() => props.value, (newValue) => { value.value = newValue; });
|
|
|
|
|
|
+function syncInput() {
|
|
|
+ if (!props.input) return;
|
|
|
+ const input = document.getElementById(props.input);
|
|
|
+ if (input) {
|
|
|
+ // Une note à 0 est envoyée vide pour rester null côté back-end
|
|
|
+ input.value = value.value > 0 ? String(value.value) : '';
|
|
|
+ }
|
|
|
+}
|
|
|
+onMounted(syncInput);
|
|
|
+
|
|
|
+// Aperçu au survol : reflète la note que produirait un clic
|
|
|
+const hovered = ref(0);
|
|
|
+const displayValue = computed(() => hovered.value || value.value);
|
|
|
+
|
|
|
+function clearNote() {
|
|
|
+ value.value = 0;
|
|
|
+ hovered.value = 0;
|
|
|
+ emit('change', 0);
|
|
|
+ syncInput();
|
|
|
+}
|
|
|
+
|
|
|
const sizeClass = computed(() => ({
|
|
|
'xs': 'fa-xs',
|
|
|
'sm': 'fa-sm',
|
|
|
@@ -35,18 +74,29 @@ const sizeClass = computed(() => ({
|
|
|
'3x': 'fa-3x'
|
|
|
}[props.size] || ''));
|
|
|
|
|
|
-function starClass(index) {
|
|
|
- const faStar = 'fa-star';
|
|
|
+function isFull(index) {
|
|
|
+ return displayValue.value >= index;
|
|
|
+}
|
|
|
+
|
|
|
+function isHalf(index) {
|
|
|
+ return props.step === 0.5 && !isFull(index) && displayValue.value >= index - 0.5;
|
|
|
+}
|
|
|
+
|
|
|
+function handleHover(event) {
|
|
|
+ if (props.disabled) return;
|
|
|
+
|
|
|
+ const star = event.target.closest('.star');
|
|
|
+ if (!star) return;
|
|
|
+
|
|
|
+ const stars = Array.from(event.currentTarget.querySelectorAll('.star'));
|
|
|
+ const starValue = stars.indexOf(star) + 1;
|
|
|
+
|
|
|
if (props.step === 0.5) {
|
|
|
- if (value.value >= index) {
|
|
|
- return `fa-solid ${faStar}`;
|
|
|
- }
|
|
|
- if (value.value >= index - 0.5) {
|
|
|
- return `fa-solid ${faStar}-half-stroke`;
|
|
|
- }
|
|
|
- return `fa-regular ${faStar}`;
|
|
|
+ const rect = star.getBoundingClientRect();
|
|
|
+ hovered.value = event.clientX - rect.left < rect.width / 2 ? starValue - 0.5 : starValue;
|
|
|
+ } else {
|
|
|
+ hovered.value = starValue;
|
|
|
}
|
|
|
- return value.value >= index ? `fa-solid ${faStar}` : `fa-regular ${faStar}`;
|
|
|
}
|
|
|
|
|
|
function handleClick(event) {
|
|
|
@@ -55,6 +105,8 @@ function handleClick(event) {
|
|
|
const clickedStar = event.target.closest('.star');
|
|
|
if (!clickedStar) return;
|
|
|
|
|
|
+ const previous = value.value;
|
|
|
+
|
|
|
const stars = Array.from(event.currentTarget.querySelectorAll('.star'));
|
|
|
const starValue = stars.indexOf(clickedStar) + 1;
|
|
|
|
|
|
@@ -67,10 +119,55 @@ function handleClick(event) {
|
|
|
value.value = starValue;
|
|
|
}
|
|
|
|
|
|
+ // Re-cliquer sur la note en cours remet à zéro (équivalent du "clear")
|
|
|
+ if (value.value === previous) {
|
|
|
+ value.value = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ hovered.value = 0;
|
|
|
+
|
|
|
emit('change', value.value);
|
|
|
+ syncInput();
|
|
|
event.currentTarget.dispatchEvent(new CustomEvent('change', {
|
|
|
bubbles: true,
|
|
|
detail: { value: value.value }
|
|
|
}));
|
|
|
}
|
|
|
</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.star {
|
|
|
+ position: relative;
|
|
|
+ display: inline-block;
|
|
|
+ line-height: 1;
|
|
|
+ vertical-align: -0.125em;
|
|
|
+}
|
|
|
+
|
|
|
+.star svg {
|
|
|
+ display: block;
|
|
|
+ height: 1em;
|
|
|
+ width: 1.125em; /* ratio du viewBox 576/512, comme la glyphe FA */
|
|
|
+}
|
|
|
+
|
|
|
+/* Remplissage jaune, superposé sous le contour */
|
|
|
+.star .fill {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ fill: var(--bs-warning, #ffc107);
|
|
|
+}
|
|
|
+
|
|
|
+/* Demi-étoile : moitié gauche uniquement, le contour droit reste celui de la base */
|
|
|
+.star .fill.half {
|
|
|
+ clip-path: inset(0 50% 0 0);
|
|
|
+}
|
|
|
+
|
|
|
+/* Contour gris, peint au-dessus du remplissage — épaisseur réglable via --star-stroke */
|
|
|
+.star .base {
|
|
|
+ position: relative;
|
|
|
+ fill: none;
|
|
|
+ stroke: var(--bs-secondary, #6c757d);
|
|
|
+ stroke-width: var(--star-stroke, 40);
|
|
|
+ stroke-linejoin: round;
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|