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>
425 lines
10 KiB
Vue
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>
|