designtopack/src/components/Selector.vue
isUnknown 6b80e242b8 Fix virtual sample routing and refactor for clarity
Virtual sample variations now display correctly when loading from URL hash.
Old URLs with underscores are normalized to hyphens on load. URL hash
updates automatically when navigating between variations.

Refactored both DynamicView and Selector components with explicit function
names, removed unnecessary comments, and improved code organization.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-15 13:54:36 +01:00

425 lines
10 KiB
Vue

<template>
<div
class="selector-dropdown | flex flex-col"
:class="{ 'has-image': getFrontViewUrl(currentValue) }"
:style="setImage()"
>
<label for="selector-select" class="text-sm">{{ label }}</label>
<MultiSelect
v-if="isCompareModeEnabled"
id="selector-multiselect"
v-model="currentValue"
:options="items"
optionLabel="title"
:placeholder="'Sélectionnez une déclinaison'"
:maxSelectedLabels="3"
class="font-serif"
:class="{ active: currentValue }"
data-icon="chevron-single-down"
checkmark
>
<template #option="slotProps">
<img
alt=""
:src="getFrontViewUrl(slotProps.option)"
width="28"
height="28"
/>
<p>{{ slotProps.option.title }}</p>
</template>
</MultiSelect>
<Select
v-else
id="selector-select"
v-model="currentValue"
:options="items"
optionLabel="title"
class="font-serif"
:class="{ active: currentValue }"
data-icon="chevron-single-down"
checkmark
>
<template #value="slotProps">
<p v-if="currentValue">
{{ currentValue.title }}
</p>
<p v-else>Sélectionnez une déclinaison</p>
</template>
<template #option="slotProps">
<img
alt=""
:src="getFrontViewUrl(slotProps.option)"
width="28"
height="28"
/>
<p>{{ slotProps.option.title }}</p>
</template>
</Select>
</div>
</template>
<script setup>
import { onBeforeMount, ref, watch, nextTick } from 'vue';
import { storeToRefs } from 'pinia';
import { useDialogStore } from '../stores/dialog';
// Props
const { items, label, isCompareModeEnabled, index } = defineProps({
label: String,
items: Array,
isCompareModeEnabled: { type: Boolean, default: false },
index: Number,
});
// Local state
const currentValue = ref(null);
const syncing = ref(false);
const { activeTracks } = storeToRefs(useDialogStore());
function normalizeSlug(slug) {
return slug.replace(/_/g, '-');
}
function areVariationsEqual(variationA, variationB) {
if (!variationA || !variationB) return false;
if (variationA.slug && variationB.slug) {
return normalizeSlug(variationA.slug) === normalizeSlug(variationB.slug);
}
return variationA.title === variationB.title;
}
function extractVariation(value) {
if (!value) return null;
return Array.isArray(value) ? value[value.length - 1] || null : value;
}
function convertValueForCompareMode(value, shouldBeArray) {
if (shouldBeArray) {
return value && !Array.isArray(value) ? [value] : value;
} else {
return Array.isArray(value) ? value[0] || null : value;
}
}
function findMatchingVariationsInStore(storeVariations) {
return storeVariations.filter((storeVar) =>
items.some((item) => areVariationsEqual(item, storeVar))
);
}
function syncCurrentValueFromStore(storeVariations) {
syncing.value = true;
const matchedVariations = findMatchingVariationsInStore(storeVariations);
if (isCompareModeEnabled) {
currentValue.value = matchedVariations.length ? [...matchedVariations] : [];
} else {
currentValue.value = matchedVariations[0] || null;
}
nextTick(() => (syncing.value = false));
}
function detectVariationChanges(newValues, oldValues) {
const newList = Array.isArray(newValues)
? newValues
: newValues
? [newValues]
: [];
const oldList = Array.isArray(oldValues)
? oldValues
: oldValues
? [oldValues]
: [];
const addedVariation = newList.find(
(n) => !oldList.some((o) => areVariationsEqual(o, n))
);
const removedVariation = oldList.find(
(o) => !newList.some((n) => areVariationsEqual(n, o))
);
return { addedVariation, removedVariation };
}
function handleVariationChange(newValue, oldValue) {
if (syncing.value) return;
const { addedVariation, removedVariation } = detectVariationChanges(
newValue,
oldValue
);
if (
addedVariation &&
items.some((item) => areVariationsEqual(item, addedVariation))
) {
updateActiveTracks(addedVariation, 'add');
} else if (
removedVariation &&
items.some((item) => areVariationsEqual(item, removedVariation))
) {
updateActiveTracks(removedVariation, 'remove');
}
}
watch(
() => isCompareModeEnabled,
(shouldBeArray) => {
syncing.value = true;
currentValue.value = convertValueForCompareMode(
currentValue.value,
shouldBeArray
);
nextTick(() => (syncing.value = false));
}
);
watch(currentValue, handleVariationChange, { deep: true });
watch(
activeTracks,
(storeVariations) => {
const variationsList = Array.isArray(storeVariations)
? storeVariations
: [];
syncCurrentValueFromStore(variationsList);
},
{ deep: true, immediate: true }
);
function removeVariationFromActiveTracks(variation) {
activeTracks.value = activeTracks.value.filter(
(track) => !areVariationsEqual(track, variation)
);
}
function addVariationToActiveTracks(variation) {
const isAlreadyPresent = activeTracks.value.some((track) =>
areVariationsEqual(track, variation)
);
if (isAlreadyPresent) return;
if (activeTracks.value.length === 0) {
activeTracks.value = [variation];
} else if (activeTracks.value.length === 1) {
activeTracks.value = [activeTracks.value[0], variation];
} else {
activeTracks.value = [activeTracks.value[0], variation];
}
}
function updateActiveTracks(track, action = 'add') {
const variation = extractVariation(track);
if (!variation) return;
if (!isCompareModeEnabled) {
activeTracks.value = [variation];
return;
}
if (action === 'remove') {
removeVariationFromActiveTracks(variation);
} else {
addVariationToActiveTracks(variation);
}
}
function getFrontViewUrl(item) {
if (!item) return '';
if (Array.isArray(item)) {
return item.length > 0 ? getFrontViewUrl(item[0]) : '';
}
if (item.files && item.files.length > 1) {
return item.files[7]?.url || item.files[0].url;
} else {
return item.files && item.files[0] ? item.files[0].url : '';
}
}
function setImage() {
return getFrontViewUrl(currentValue.value)
? "--image: url('" + getFrontViewUrl(currentValue.value) + "')"
: undefined;
}
</script>
<style>
.selector-dropdown {
--selector-width: 21rem;
--row-gap: 0;
align-items: flex-start;
position: relative;
background: var(--color-background);
border-radius: var(--rounded-lg);
height: 3.75rem;
min-width: var(--selector-width, 21rem);
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);
}
.selector-dropdown.has-image:before {
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);
background-image: var(--image);
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
[role='combobox'],
[id*='select_list'] {
border: 1px solid var(--color-grey-200);
}
[role='combobox']:hover {
outline: 0px solid var(--color-grey-400);
border-color: var(--color-grey-400);
}
[role='combobox'][aria-expanded='true'] {
outline: 2px solid var(--color-focus-ring);
outline-offset: -2px;
border-color: transparent;
}
#selector-select,
#selector-multiselect,
[role='combobox'] {
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1.875rem var(--space-48) var(--space-8) var(--space-16);
cursor: pointer;
}
[role='combobox'] p,
.selector-dropdown
[data-pc-section='labelcontainer']
> [data-pc-section='label'] {
max-height: 1lh;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#selector-select.active [role='combobox'] {
border-color: var(--color-focus-ring);
}
/* Icon */
[data-pc-section='dropdown'] {
display: none; /* Hide default component svg */
}
.selector-dropdown [data-icon]::before {
--icon-color: var(--color-grey-700);
position: absolute;
right: var(--space-8);
top: 0.625rem;
width: 2.5rem;
height: 2.5rem;
padding: 0.625rem;
}
.selector-dropdown label {
color: var(--color-grey-700);
letter-spacing: var(--tracking-wider);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
height: 1lh;
padding-right: 1em;
}
/* Options */
[id*='select_list'] {
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'] > * {
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'] > * + * {
margin-top: var(--space-4);
}
[id*='select_list'] > *::before {
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);
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
[id*='select_list'] > *:hover {
background-color: var(--color-grey-50);
}
[id*='select_list'] > *:focus,
[id*='select_list'] > *:focus-visible,
[id*='select_list'] > [data-p-focused='true'] {
outline: 2px solid var(--color-focus-ring);
}
/* Check */
#selector-multiselect_list input[type='checkbox'] {
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 {
--icon-color: var(--color-focus-ring);
}
[id*='select_list'] > * > svg {
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'] {
display: none;
}
#selector-multiselect_list
[aria-selected='false']
input[type='checkbox']::before {
--icon-color: transparent;
}
/* Overlay */
[data-pc-section='overlay'] [data-pc-section='header'] {
display: none;
}
</style>