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>
This commit is contained in:
parent
dfb8d1038b
commit
6b80e242b8
2 changed files with 224 additions and 168 deletions
|
|
@ -76,112 +76,149 @@ const { items, label, isCompareModeEnabled, index } = defineProps({
|
|||
|
||||
// Local state
|
||||
const currentValue = ref(null);
|
||||
const syncing = ref(false); // empêche les réactions pendant les mises à jour programmatiques
|
||||
const syncing = ref(false);
|
||||
|
||||
// Store
|
||||
const { activeTracks } = storeToRefs(useDialogStore());
|
||||
|
||||
// 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 normalizeSlug(slug) {
|
||||
return slug.replace(/_/g, '-');
|
||||
}
|
||||
|
||||
function toVariation(v) {
|
||||
if (!v) return null;
|
||||
return Array.isArray(v) ? v[v.length - 1] || null : v;
|
||||
function areVariationsEqual(variationA, variationB) {
|
||||
if (!variationA || !variationB) return false;
|
||||
|
||||
if (variationA.slug && variationB.slug) {
|
||||
return normalizeSlug(variationA.slug) === normalizeSlug(variationB.slug);
|
||||
}
|
||||
|
||||
// Initialisation : remplir le 1er select localement ET initialiser le store
|
||||
onBeforeMount(() => {
|
||||
syncing.value = true;
|
||||
|
||||
if (index === 0) {
|
||||
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];
|
||||
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 {
|
||||
// les autres ne forcent pas le store ; leur currentValue restera à null
|
||||
currentValue.value = null;
|
||||
return Array.isArray(value) ? value[0] || null : value;
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => (syncing.value = false));
|
||||
});
|
||||
|
||||
// Quand on bascule compare mode (objet <-> tableau)
|
||||
watch(
|
||||
() => isCompareModeEnabled,
|
||||
(flag) => {
|
||||
syncing.value = true;
|
||||
if (flag) {
|
||||
if (currentValue.value && !Array.isArray(currentValue.value)) {
|
||||
currentValue.value = [currentValue.value];
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(currentValue.value)) {
|
||||
currentValue.value = currentValue.value[0] || null;
|
||||
}
|
||||
}
|
||||
nextTick(() => (syncing.value = false));
|
||||
}
|
||||
function findMatchingVariationsInStore(storeVariations) {
|
||||
return storeVariations.filter((storeVar) =>
|
||||
items.some((item) => areVariationsEqual(item, storeVar))
|
||||
);
|
||||
|
||||
// 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 }
|
||||
);
|
||||
|
||||
// Quand activeTracks change elsewhere -> synchroniser l'affichage local
|
||||
// Mais n'adopter que les variations qui appartiennent à ce Selector (`items`)
|
||||
watch(
|
||||
activeTracks,
|
||||
(newVal) => {
|
||||
function syncCurrentValueFromStore(storeVariations) {
|
||||
syncing.value = true;
|
||||
|
||||
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))
|
||||
);
|
||||
const matchedVariations = findMatchingVariationsInStore(storeVariations);
|
||||
|
||||
if (isCompareModeEnabled) {
|
||||
currentValue.value = matched.length ? [...matched] : [];
|
||||
currentValue.value = matchedVariations.length ? [...matchedVariations] : [];
|
||||
} else {
|
||||
currentValue.value = matched[0] || null;
|
||||
currentValue.value = matchedVariations[0] || null;
|
||||
}
|
||||
|
||||
nextTick(() => (syncing.value = false));
|
||||
},
|
||||
{ deep: true }
|
||||
}
|
||||
|
||||
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))
|
||||
);
|
||||
|
||||
// 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);
|
||||
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) {
|
||||
|
|
@ -190,34 +227,12 @@ function selectTrack(track, action = 'add') {
|
|||
}
|
||||
|
||||
if (action === 'remove') {
|
||||
const wasFirst =
|
||||
activeTracks.value.length && isSame(activeTracks.value[0], variation);
|
||||
activeTracks.value = activeTracks.value.filter(
|
||||
(t) => !isSame(t, variation)
|
||||
);
|
||||
|
||||
// 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)
|
||||
return;
|
||||
}
|
||||
|
||||
// action === 'add'
|
||||
if (activeTracks.value.some((t) => isSame(t, variation))) {
|
||||
// déjà présent -> ignore
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTracks.value.length === 0) {
|
||||
activeTracks.value = [variation];
|
||||
} else if (activeTracks.value.length === 1) {
|
||||
activeTracks.value = [activeTracks.value[0], variation];
|
||||
removeVariationFromActiveTracks(variation);
|
||||
} else {
|
||||
// remplacer le 2e
|
||||
activeTracks.value = [activeTracks.value[0], variation];
|
||||
addVariationToActiveTracks(variation);
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers pour affichage (inchangés)
|
||||
function getFrontViewUrl(item) {
|
||||
if (!item) return '';
|
||||
if (Array.isArray(item)) {
|
||||
|
|
@ -232,7 +247,7 @@ function getFrontViewUrl(item) {
|
|||
|
||||
function setImage() {
|
||||
return getFrontViewUrl(currentValue.value)
|
||||
? '--image: url(\'' + getFrontViewUrl(currentValue.value) + '\')'
|
||||
? "--image: url('" + getFrontViewUrl(currentValue.value) + "')"
|
||||
: undefined;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -250,7 +265,8 @@ function setImage() {
|
|||
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']) {
|
||||
.selector-dropdown.has-image
|
||||
:is(#selector-select, #selector-multiselect, [role='combobox']) {
|
||||
padding-left: var(--space-64);
|
||||
}
|
||||
.selector-dropdown.has-image:before {
|
||||
|
|
@ -290,7 +306,9 @@ function setImage() {
|
|||
cursor: pointer;
|
||||
}
|
||||
[role='combobox'] p,
|
||||
.selector-dropdown [data-pc-section="labelcontainer"] > [data-pc-section='label'] {
|
||||
.selector-dropdown
|
||||
[data-pc-section='labelcontainer']
|
||||
> [data-pc-section='label'] {
|
||||
max-height: 1lh;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
|||
|
|
@ -61,13 +61,14 @@ import { storeToRefs } from 'pinia';
|
|||
import { usePageStore } from '../../../stores/page';
|
||||
import { useDialogStore } from '../../../stores/dialog';
|
||||
import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Interactive360 from './Interactive360.vue';
|
||||
import SingleImage from './SingleImage.vue';
|
||||
import Selector from '../../Selector.vue';
|
||||
import slugify from 'slugify';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const { isCommentsOpen, isCommentPanelEnabled, activeTracks, openedFile } =
|
||||
|
|
@ -92,51 +93,74 @@ const tracks = computed(() => {
|
|||
return list;
|
||||
});
|
||||
|
||||
// ---------- INITIALISATION ----------
|
||||
// onBeforeMount : on initialise toujours activeTracks avec une VARIATION (jamais un track)
|
||||
onBeforeMount(() => {
|
||||
// essayer la hash en priorité
|
||||
let initialVariation = null;
|
||||
|
||||
if (route?.hash && route.hash.length > 0) {
|
||||
const variations = tracks.value.flatMap((t) => t.variations || []);
|
||||
const hashValue = route.hash.substring(1);
|
||||
|
||||
// Essayer de trouver la variation soit par slug direct, soit en normalisant le hash
|
||||
initialVariation = variations.find((v) => {
|
||||
// Comparaison directe
|
||||
if (v.slug === hashValue) return true;
|
||||
// Comparaison en convertissant underscores en tirets (slugify par défaut)
|
||||
if (v.slug === hashValue.replace(/_/g, '-')) return true;
|
||||
// Comparaison inverse : le slug du backend pourrait avoir des underscores
|
||||
if (v.slug.replace(/-/g, '_') === hashValue) return true;
|
||||
return false;
|
||||
}) || null;
|
||||
function normalizeSlug(slug) {
|
||||
return slug.replace(/_/g, '-');
|
||||
}
|
||||
|
||||
// fallback : première variation du premier track
|
||||
if (!initialVariation) {
|
||||
initialVariation = tracks.value[0]?.variations?.[0] || null;
|
||||
function getVariationSlug(variation) {
|
||||
return variation.slug || (variation.title ? slugify(variation.title) : null);
|
||||
}
|
||||
|
||||
if (initialVariation) {
|
||||
activeTracks.value = [initialVariation];
|
||||
} else {
|
||||
activeTracks.value = []; // aucun contenu disponible
|
||||
}
|
||||
function findVariationByHash(hashValue) {
|
||||
const allVariations = tracks.value.flatMap((track) => track.variations || []);
|
||||
const normalizedHash = normalizeSlug(hashValue);
|
||||
|
||||
return allVariations.find((variation) => {
|
||||
const variationSlug = getVariationSlug(variation);
|
||||
if (!variationSlug) return false;
|
||||
|
||||
const normalizedVariationSlug = normalizeSlug(variationSlug);
|
||||
return normalizedVariationSlug === normalizedHash;
|
||||
});
|
||||
}
|
||||
|
||||
// scroll si hash présent
|
||||
onMounted(() => {
|
||||
if (route.query?.comments) isCommentsOpen.value = true;
|
||||
function getInitialVariation() {
|
||||
if (route?.hash && route.hash.length > 0) {
|
||||
const hashValue = route.hash.substring(1);
|
||||
const variationFromHash = findVariationByHash(hashValue);
|
||||
if (variationFromHash) return variationFromHash;
|
||||
}
|
||||
|
||||
return tracks.value[0]?.variations?.[0] || null;
|
||||
}
|
||||
|
||||
function initializeActiveTracks() {
|
||||
const initialVariation = getInitialVariation();
|
||||
activeTracks.value = initialVariation ? [initialVariation] : [];
|
||||
}
|
||||
|
||||
function normalizeUrlHash() {
|
||||
if (route?.hash && route.hash.includes('_')) {
|
||||
const normalizedHash = normalizeSlug(route.hash);
|
||||
router.replace({ ...route, hash: normalizedHash });
|
||||
}
|
||||
}
|
||||
|
||||
function openCommentsIfRequested() {
|
||||
if (route.query?.comments) {
|
||||
isCommentsOpen.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToHashTarget() {
|
||||
if (!route?.hash || route.hash.length === 0) return;
|
||||
|
||||
const selector = route.hash.replace('#', '#track--');
|
||||
const targetBtn = document.querySelector(selector);
|
||||
if (targetBtn) targetBtn.scrollIntoView();
|
||||
const selectorId = route.hash.replace('#', '#track--');
|
||||
const targetButton = document.querySelector(selectorId);
|
||||
if (targetButton) {
|
||||
targetButton.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
initializeActiveTracks();
|
||||
});
|
||||
|
||||
// ---------- COMPUTED / WATCH ----------
|
||||
onMounted(() => {
|
||||
openCommentsIfRequested();
|
||||
normalizeUrlHash();
|
||||
scrollToHashTarget();
|
||||
});
|
||||
|
||||
const isSingleImage = computed(() => {
|
||||
return (
|
||||
|
|
@ -149,38 +173,52 @@ const singleFile = computed(() => {
|
|||
return isSingleImage.value ? activeTracks.value[0].files[0] : null;
|
||||
});
|
||||
|
||||
watch(
|
||||
singleFile,
|
||||
(newValue) => {
|
||||
if (newValue) openedFile.value = newValue;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
function updateOpenedFile(file) {
|
||||
if (file) {
|
||||
openedFile.value = file;
|
||||
}
|
||||
}
|
||||
|
||||
// gestion du mode comparaison : fermer les commentaires, etc.
|
||||
watch(isCompareModeEnabled, (newValue) => {
|
||||
if (newValue) {
|
||||
function enableCompareModeUI() {
|
||||
isCommentsOpen.value = false;
|
||||
isCommentPanelEnabled.value = false;
|
||||
} else {
|
||||
isCommentPanelEnabled.value = true;
|
||||
}
|
||||
|
||||
// quand on quitte le mode comparaison on retire l'élément secondaire si nécessaire
|
||||
if (!newValue && activeTracks.value.length === 2) {
|
||||
function disableCompareModeUI() {
|
||||
isCommentPanelEnabled.value = true;
|
||||
|
||||
if (activeTracks.value.length === 2) {
|
||||
activeTracks.value.pop();
|
||||
}
|
||||
}
|
||||
|
||||
function updateUrlHash(firstTrack) {
|
||||
const trackSlug = getVariationSlug(firstTrack);
|
||||
if (!trackSlug) return;
|
||||
|
||||
const currentHash = route.hash ? route.hash.substring(1) : '';
|
||||
const normalizedTrackSlug = normalizeSlug(trackSlug);
|
||||
|
||||
if (currentHash !== normalizedTrackSlug) {
|
||||
router.replace({ ...route, hash: '#' + normalizedTrackSlug });
|
||||
}
|
||||
}
|
||||
|
||||
watch(singleFile, updateOpenedFile, { immediate: true });
|
||||
|
||||
watch(isCompareModeEnabled, (isEnabled) => {
|
||||
isEnabled ? enableCompareModeUI() : disableCompareModeUI();
|
||||
});
|
||||
|
||||
// ---------- UTIL / helper ----------
|
||||
function getCommentsCount(track) {
|
||||
if (!track || !Array.isArray(track.files)) return undefined;
|
||||
let count = 0;
|
||||
for (const file of track.files) {
|
||||
count += file?.comments?.length || 0;
|
||||
}
|
||||
return count > 0 ? count : undefined;
|
||||
watch(
|
||||
activeTracks,
|
||||
(tracks) => {
|
||||
if (tracks && tracks.length > 0) {
|
||||
updateUrlHash(tracks[0]);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue