소스 검색

Remplacement bootstrap-star-rating réussi

Sangfroid 4 일 전
부모
커밋
c09dd0aea0
3개의 변경된 파일과 133개의 추가작업 그리고 27개의 파일을 삭제
  1. 111 14
      assets/vue/controllers/StarRating.vue
  2. 2 12
      src/Form/CommentaireType.php
  3. 20 1
      templates/videotheque/form_commentaire.html.twig

+ 111 - 14
assets/vue/controllers/StarRating.vue

@@ -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>
+

+ 2 - 12
src/Form/CommentaireType.php

@@ -3,7 +3,7 @@
 namespace App\Form;
 
 use App\Entity\Commentaire;
-use Symfony\Component\Form\Extension\Core\Type\NumberType;
+use Symfony\Component\Form\Extension\Core\Type\HiddenType;
 use Symfony\Component\Form\AbstractType;
 use Symfony\Component\Form\Extension\Core\Type\TextareaType;
 use Symfony\Component\Form\FormBuilderInterface;
@@ -20,19 +20,9 @@ class CommentaireType extends AbstractType
         array $options,
     ): void {
         $builder
-            ->add("note", NumberType::class, [
+            ->add("note", HiddenType::class, [
                 "required" => false,
                 "label" => false,
-                "attr" => [
-                    "min" => 0,
-                    "max" => 5,
-                    "class" => "rating",
-                    "data-step" => 1,
-                    "data-show-clear" => "true",
-                    "data-show-caption" => "false",
-                    "data-size" => "sm",
-                    "data-theme" => "krajee-fa",
-                ],
             ])
             ->add("contenu", TextareaType::class, [
                 "required" => false,

+ 20 - 1
templates/videotheque/form_commentaire.html.twig

@@ -1 +1,20 @@
-{{ form(form) }}
+{# Formulaire de commentaire : la note est choisie via le composant Vue StarRating #}
+{{ form_start(form) }}
+    {{ form_errors(form) }}
+
+    <div class="mb-3">
+        {{ form_widget(form.note) }}
+        <span {{ vue_component('StarRating', {
+            value: (form.note.vars.value ?: 0) * 1,
+            max: 5,
+            step: 1,
+            disabled: false,
+            size: 'lg',
+            input: form.note.vars.id
+        }) }}></span>
+        {{ form_errors(form.note) }}
+    </div>
+
+    {{ form_row(form.contenu) }}
+    {{ form_row(form.save) }}
+{{ form_end(form) }}