designtopack/src/components/Selector.vue

408 lines
11 KiB
Vue
Raw Normal View History

2025-05-27 15:29:57 +02:00
<template>
<div
2025-09-18 16:28:24 +02:00
class="selector-dropdown | flex flex-col"
2025-10-07 15:41:48 +02:00
:class="{ 'has-image': getFrontViewUrl(currentValue) }"
:style="setImage()"
2025-05-27 15:29:57 +02:00
>
<label for="selector-select" class="text-sm">{{ label }}</label>
<MultiSelect
v-if="isCompareModeEnabled"
2025-06-06 10:04:06 +02:00
id="selector-multiselect"
v-model="currentValue"
:options="items"
optionLabel="title"
:placeholder="'Sélectionnez une déclinaison'"
:maxSelectedLabels="3"
2025-06-06 10:04:06 +02:00
class="font-serif"
2025-06-06 10:55:03 +02:00
:class="{ active: currentValue }"
2025-06-18 12:06:33 +02:00
data-icon="chevron-single-down"
2025-06-06 10:04:06 +02:00
checkmark
2025-06-18 12:06:33 +02:00
>
<template #option="slotProps">
<img
alt=""
:src="getFrontViewUrl(slotProps.option)"
width="28"
height="28"
/>
<p>{{ slotProps.option.title }}</p>
</template>
</MultiSelect>
<Select
v-else
2025-05-27 15:29:57 +02:00
id="selector-select"
v-model="currentValue"
2025-05-27 15:29:57 +02:00
:options="items"
optionLabel="title"
class="font-serif"
2025-06-06 10:55:03 +02:00
:class="{ active: currentValue }"
2025-05-27 15:29:57 +02:00
data-icon="chevron-single-down"
checkmark
2025-05-28 15:11:00 +02:00
>
<template #value="slotProps">
2025-06-06 10:04:06 +02:00
<p v-if="currentValue">
{{ currentValue.title }}
</p>
2025-10-01 12:51:17 +02:00
<p v-else>Sélectionnez une déclinaison</p>
2025-05-28 15:11:00 +02:00
</template>
2025-05-28 15:11:00 +02:00
<template #option="slotProps">
<img
alt=""
:src="getFrontViewUrl(slotProps.option)"
width="28"
height="28"
/>
<p>{{ slotProps.option.title }}</p>
2025-05-28 15:11:00 +02:00
</template>
</Select>
2025-05-27 15:29:57 +02:00
</div>
</template>
<script setup>
2025-10-01 13:10:28 +02:00
import { onBeforeMount, ref, watch, nextTick } from 'vue';
import { storeToRefs } from 'pinia';
import { useDialogStore } from '../stores/dialog';
2025-05-27 15:29:57 +02:00
2025-06-11 14:45:11 +02:00
// Props
const { items, label, isCompareModeEnabled, index } = defineProps({
2025-05-27 15:29:57 +02:00
label: String,
items: Array,
2025-10-01 13:10:28 +02:00
isCompareModeEnabled: { type: Boolean, default: false },
2025-06-11 14:45:11 +02:00
index: Number,
2025-05-27 15:29:57 +02:00
});
2025-10-01 13:10:28 +02:00
// Local state
2025-06-06 10:04:06 +02:00
const currentValue = ref(null);
2025-10-01 13:10:28 +02:00
const syncing = ref(false); // empêche les réactions pendant les mises à jour programmatiques
2025-10-01 13:10:28 +02:00
// Store
const { activeTracks } = storeToRefs(useDialogStore());
2025-05-27 15:29:57 +02:00
2025-10-01 13:10:28 +02:00
// Utils
function isSame(a, b) {
if (!a || !b) return false;
if (a.slug && b.slug) return a.slug === b.slug;
return a.title === b.title;
}
function toVariation(v) {
if (!v) return null;
return Array.isArray(v) ? v[v.length - 1] || null : v;
}
// Initialisation : remplir le 1er select localement ET initialiser le store
2025-06-11 14:45:11 +02:00
onBeforeMount(() => {
2025-10-01 13:10:28 +02:00
syncing.value = true;
2025-06-11 14:45:11 +02:00
if (index === 0) {
2025-10-01 13:10:28 +02:00
currentValue.value = items[0] || null;
// si le store est vide, initialiser avec la variation du premier sélecteur
if (!activeTracks.value || activeTracks.value.length === 0) {
const v = toVariation(items[0]);
if (v) activeTracks.value = [v];
}
} else {
// les autres ne forcent pas le store ; leur currentValue restera à null
currentValue.value = null;
2025-06-11 14:45:11 +02:00
}
2025-10-01 13:10:28 +02:00
nextTick(() => (syncing.value = false));
2025-06-11 14:45:11 +02:00
});
2025-10-01 13:10:28 +02:00
// Quand on bascule compare mode (objet <-> tableau)
2025-06-06 10:04:06 +02:00
watch(
() => isCompareModeEnabled,
2025-10-01 13:10:28 +02:00
(flag) => {
syncing.value = true;
if (flag) {
2025-10-01 12:51:17 +02:00
if (currentValue.value && !Array.isArray(currentValue.value)) {
currentValue.value = [currentValue.value];
}
} else {
if (Array.isArray(currentValue.value)) {
currentValue.value = currentValue.value[0] || null;
}
2025-06-11 15:34:30 +02:00
}
2025-10-01 13:10:28 +02:00
nextTick(() => (syncing.value = false));
2025-06-06 10:04:06 +02:00
}
);
2025-10-01 13:10:28 +02:00
// Détection ajout / suppression dans le MultiSelect (côté composant)
// On n'agit que si l'ajout/suppression concerne une variation appartenant à `items`
watch(
currentValue,
(newVal, oldVal) => {
if (syncing.value) return;
const newItems = Array.isArray(newVal) ? newVal : newVal ? [newVal] : [];
const oldItems = Array.isArray(oldVal) ? oldVal : oldVal ? [oldVal] : [];
const added = newItems.find((n) => !oldItems.some((o) => isSame(o, n)));
const removed = oldItems.find((o) => !newItems.some((n) => isSame(n, o)));
if (added && items.some((it) => isSame(it, added))) {
selectTrack(added, 'add');
} else if (removed && items.some((it) => isSame(it, removed))) {
selectTrack(removed, 'remove');
}
},
{ deep: true }
);
2025-05-27 15:29:57 +02:00
2025-10-01 13:10:28 +02:00
// Quand activeTracks change elsewhere -> synchroniser l'affichage local
// Mais n'adopter que les variations qui appartiennent à ce Selector (`items`)
2025-06-11 15:34:30 +02:00
watch(
activeTracks,
2025-10-01 13:10:28 +02:00
(newVal) => {
syncing.value = true;
2025-06-11 15:34:30 +02:00
2025-10-01 13:10:28 +02:00
const storeList = Array.isArray(newVal) ? newVal : [];
// ne garder que les variations du store qui sont dans `items`
const matched = storeList.filter((av) =>
items.some((it) => isSame(it, av))
);
if (isCompareModeEnabled) {
currentValue.value = matched.length ? [...matched] : [];
} else {
currentValue.value = matched[0] || null;
}
nextTick(() => (syncing.value = false));
2025-06-11 15:34:30 +02:00
},
{ deep: true }
);
2025-10-01 13:10:28 +02:00
// Logique centrale de sélection (ajout / suppression)
// Règles :
// - mode normal -> activeTracks = [variation]
// - mode comparaison -> conserver activeTracks[0] si possible; second élément ajouté/remplacé; suppression gère le cas de la suppression de la première
function selectTrack(track, action = 'add') {
const variation = toVariation(track);
if (!variation) return;
2025-06-11 15:34:30 +02:00
if (!isCompareModeEnabled) {
2025-10-01 13:10:28 +02:00
activeTracks.value = [variation];
2025-06-11 15:34:30 +02:00
return;
}
2025-10-01 13:10:28 +02:00
if (action === 'remove') {
const wasFirst =
activeTracks.value.length && isSame(activeTracks.value[0], variation);
activeTracks.value = activeTracks.value.filter(
(t) => !isSame(t, variation)
);
2025-06-11 15:34:30 +02:00
2025-10-01 13:10:28 +02:00
// si on a retiré la première et qu'il reste une piste, elle devient naturellement index 0
// pas d'action supplémentaire nécessaire ici (déjà assuré par le filter)
2025-06-11 15:34:30 +02:00
return;
}
2025-10-01 13:10:28 +02:00
// action === 'add'
if (activeTracks.value.some((t) => isSame(t, variation))) {
// déjà présent -> ignore
return;
2025-06-11 15:34:30 +02:00
}
2025-10-01 13:10:28 +02:00
if (activeTracks.value.length === 0) {
activeTracks.value = [variation];
} else if (activeTracks.value.length === 1) {
activeTracks.value = [activeTracks.value[0], variation];
} else {
// remplacer le 2e
activeTracks.value = [activeTracks.value[0], variation];
}
2025-10-01 13:10:28 +02:00
}
2025-05-27 15:29:57 +02:00
2025-10-01 13:10:28 +02:00
// Helpers pour affichage (inchangés)
2025-05-28 15:11:00 +02:00
function getFrontViewUrl(item) {
if (!item) return '';
if (Array.isArray(item)) {
return item.length > 0 ? getFrontViewUrl(item[0]) : '';
}
2025-10-01 13:10:28 +02:00
if (item.files && item.files.length > 1) {
return item.files[7]?.url || item.files[0].url;
2025-05-27 15:29:57 +02:00
} else {
2025-10-01 13:10:28 +02:00
return item.files && item.files[0] ? item.files[0].url : '';
2025-05-27 15:29:57 +02:00
}
}
2025-10-07 15:41:48 +02:00
function setImage() {
return getFrontViewUrl(currentValue.value)
? '--image: url(\'' + getFrontViewUrl(currentValue.value) + '\')'
: undefined;
}
2025-05-27 15:29:57 +02:00
</script>
<style>
2025-09-18 16:28:24 +02:00
.selector-dropdown {
2025-06-06 11:15:48 +02:00
--selector-width: 21rem;
2025-05-27 15:29:57 +02:00
--row-gap: 0;
align-items: flex-start;
position: relative;
background: var(--color-background);
border-radius: var(--rounded-lg);
height: 3.75rem;
2025-06-06 11:15:48 +02:00
min-width: var(--selector-width, 21rem);
2025-10-07 15:41:48 +02:00
padding: var(--space-8) var(--space-48) var(--space-8) var(--space-16);
}
.selector-dropdown.has-image,
.selector-dropdown.has-image :is(#selector-select, #selector-multiselect, [role='combobox']) {
padding-left: var(--space-64);
2025-05-27 15:29:57 +02:00
}
2025-10-07 15:41:48 +02:00
.selector-dropdown.has-image:before {
2025-05-27 15:29:57 +02:00
content: '';
position: absolute;
left: var(--space-8);
width: 2.75rem;
height: 2.75rem;
border-radius: var(--rounded-md);
background-color: var(--dialog-inner-background, #f7f7f7);
2025-05-27 15:29:57 +02:00
background-image: var(--image);
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
[role='combobox'],
[id*='select_list'] {
2025-05-27 15:29:57 +02:00
border: 1px solid var(--color-grey-200);
}
[role='combobox']:hover {
2025-06-06 11:15:48 +02:00
outline: 0px solid var(--color-grey-400);
border-color: var(--color-grey-400);
2025-05-27 15:29:57 +02:00
}
[role='combobox'][aria-expanded='true'] {
2025-05-27 15:29:57 +02:00
outline: 2px solid var(--color-focus-ring);
outline-offset: -2px;
border-color: transparent;
}
#selector-select,
2025-06-18 12:06:33 +02:00
#selector-multiselect,
[role='combobox'] {
2025-05-27 15:29:57 +02:00
position: absolute;
inset: 0;
border-radius: inherit;
2025-10-07 15:41:48 +02:00
padding: 1.875rem var(--space-48) var(--space-8) var(--space-16);
2025-05-27 15:29:57 +02:00
cursor: pointer;
}
[role='combobox'] p,
.selector-dropdown [data-pc-section="labelcontainer"] > [data-pc-section='label'] {
2025-06-06 11:29:53 +02:00
max-height: 1lh;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#selector-select.active [role='combobox'] {
2025-06-18 12:06:33 +02:00
border-color: var(--color-focus-ring);
}
2025-05-27 15:29:57 +02:00
/* Icon */
[data-pc-section='dropdown'] {
2025-05-27 15:29:57 +02:00
display: none; /* Hide default component svg */
}
2025-09-18 16:28:24 +02:00
.selector-dropdown [data-icon]::before {
2025-05-27 15:29:57 +02:00
--icon-color: var(--color-grey-700);
position: absolute;
right: var(--space-8);
top: 0.625rem;
width: 2.5rem;
height: 2.5rem;
padding: 0.625rem;
}
2025-09-18 16:28:24 +02:00
.selector-dropdown label {
2025-05-27 15:29:57 +02:00
color: var(--color-grey-700);
letter-spacing: var(--tracking-wider);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
height: 1lh;
padding-right: 1em;
2025-05-27 15:29:57 +02:00
}
/* Options */
[id*='select_list'] {
2025-05-27 15:29:57 +02:00
margin-top: var(--space-4);
border-radius: var(--rounded-md);
background: var(--color-background);
box-shadow: var(--shadow);
padding: var(--space-8);
}
[id*='select_list'] > * {
2025-05-27 15:29:57 +02:00
font-family: var(--font-serif);
padding: var(--space-8) var(--space-8) var(--space-8) var(--space-48);
border-radius: var(--rounded-sm);
position: relative;
display: flex;
align-items: center;
height: 2.75rem;
cursor: pointer;
}
[id*='select_list'] > * + * {
2025-05-27 15:29:57 +02:00
margin-top: var(--space-4);
}
[id*='select_list'] > *::before {
2025-05-27 15:29:57 +02:00
content: '';
position: absolute;
left: var(--space-8);
width: 1.75rem;
height: 1.75rem;
border-radius: var(--rounded-sm);
background-image: var(--image);
background-color: var(--dialog-inner-background, #f7f7f7);
2025-05-27 15:29:57 +02:00
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
[id*='select_list'] > *:hover {
2025-05-27 15:29:57 +02:00
background-color: var(--color-grey-50);
}
[id*='select_list'] > *:focus,
[id*='select_list'] > *:focus-visible,
[id*='select_list'] > [data-p-focused='true'] {
2025-05-27 15:29:57 +02:00
outline: 2px solid var(--color-focus-ring);
}
/* Check */
#selector-multiselect_list input[type='checkbox'] {
2025-06-18 12:06:33 +02:00
position: absolute;
left: var(--space-4);
top: calc(var(--space-4) + 1px);
width: 1.75rem;
height: 1.75rem;
border-radius: var(--rounded-sm);
}
#selector-multiselect_list input[type='checkbox']:checked {
2025-06-18 12:06:33 +02:00
--icon-color: var(--color-focus-ring);
}
[id*='select_list'] > * > svg {
2025-05-27 15:29:57 +02:00
position: absolute;
left: 0.875rem;
width: 1rem;
height: 1rem;
color: var(--color-grey-700);
}
[id*='select_list'] img {
position: absolute;
left: 0.5rem;
width: 1.75rem;
height: 1.75rem;
mix-blend-mode: multiply;
}
[id*='select_list'] [aria-selected='true'] img,
#selector-multiselect_list input[type='checkbox'] + [data-pc-section='box'] {
2025-06-18 12:06:33 +02:00
display: none;
}
#selector-multiselect_list
[aria-selected='false']
input[type='checkbox']::before {
2025-06-18 12:06:33 +02:00
--icon-color: transparent;
}
/* Overlay */
[data-pc-section='overlay'] [data-pc-section='header'] {
display: none;
}
2025-05-27 15:29:57 +02:00
</style>