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
>
2025-06-05 18:41:01 +02:00
< label for = "selector-select" class = "text-sm" > { { label } } < / label >
2025-06-06 09:33:08 +02:00
< MultiSelect
v - if = "isCompareModeEnabled"
2025-06-06 10:04:06 +02:00
id = "selector-multiselect"
2025-06-06 09:33:08 +02:00
v - model = "currentValue"
: options = "items"
optionLabel = "title"
2025-10-01 12:03:31 +02:00
: placeholder = "'Sélectionnez une déclinaison'"
2025-06-06 09:33:08 +02:00
: 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 >
2025-06-06 09:33:08 +02:00
2025-06-05 18:53:42 +02:00
< Select
2025-06-06 09:33:08 +02:00
v - else
2025-05-27 15:29:57 +02:00
id = "selector-select"
2025-06-05 18:41:01 +02:00
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" >
2025-06-05 18:41:01 +02:00
{ { 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-06-05 18:41:01 +02:00
2025-05-28 15:11:00 +02:00
< template # option = "slotProps" >
2025-06-05 18:41:01 +02:00
< img
alt = ""
: src = "getFrontViewUrl(slotProps.option)"
width = "28"
height = "28"
/ >
2025-05-28 16:08:01 +02:00
< p > { { slotProps . option . title } } < / p >
2025-05-28 15:11:00 +02:00
< / template >
2025-06-05 18:53:42 +02:00
< / 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' ;
2025-06-05 18:41:01 +02:00
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-06-05 18:41:01 +02:00
2025-10-01 13:10:28 +02:00
// Store
2025-06-05 18:41:01 +02:00
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-06-05 18:41:01 +02:00
}
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 ) {
2025-06-05 18:41:01 +02:00
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 ) {
2025-06-05 18:41:01 +02:00
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 : 21 rem ;
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.75 rem ;
2025-06-06 11:15:48 +02:00
min - width : var ( -- selector - width , 21 rem ) ;
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.75 rem ;
height : 2.75 rem ;
border - radius : var ( -- rounded - md ) ;
2025-05-28 16:08:01 +02:00
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 ;
}
2025-10-01 12:03:31 +02:00
[ role = 'combobox' ] ,
[ id *= 'select_list' ] {
2025-05-27 15:29:57 +02:00
border : 1 px solid var ( -- color - grey - 200 ) ;
}
2025-10-01 12:03:31 +02:00
[ role = 'combobox' ] : hover {
2025-06-06 11:15:48 +02:00
outline : 0 px solid var ( -- color - grey - 400 ) ;
border - color : var ( -- color - grey - 400 ) ;
2025-05-27 15:29:57 +02:00
}
2025-10-01 12:03:31 +02:00
[ role = 'combobox' ] [ aria - expanded = 'true' ] {
2025-05-27 15:29:57 +02:00
outline : 2 px solid var ( -- color - focus - ring ) ;
outline - offset : - 2 px ;
border - color : transparent ;
}
# selector - select ,
2025-06-18 12:06:33 +02:00
# selector - multiselect ,
2025-10-01 12:03:31 +02:00
[ 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.875 rem var ( -- space - 48 ) var ( -- space - 8 ) var ( -- space - 16 ) ;
2025-05-27 15:29:57 +02:00
cursor : pointer ;
}
2025-10-01 16:17:40 +02:00
[ role = 'combobox' ] p ,
2025-10-01 16:23:08 +02:00
. selector - dropdown [ data - pc - section = "labelcontainer" ] > [ data - pc - section = 'label' ] {
2025-06-06 11:29:53 +02:00
max - height : 1 lh ;
overflow : hidden ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
2025-10-01 12:03:31 +02:00
# 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 */
2025-10-01 12:03:31 +02:00
[ 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.625 rem ;
width : 2.5 rem ;
height : 2.5 rem ;
padding : 0.625 rem ;
}
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 ) ;
2025-05-28 16:08:01 +02:00
overflow : hidden ;
white - space : nowrap ;
text - overflow : ellipsis ;
height : 1 lh ;
padding - right : 1 em ;
2025-05-27 15:29:57 +02:00
}
/* Options */
2025-10-01 12:03:31 +02:00
[ 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 ) ;
}
2025-10-01 12:03:31 +02:00
[ 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.75 rem ;
cursor : pointer ;
}
2025-10-01 12:03:31 +02:00
[ id *= 'select_list' ] > * + * {
2025-05-27 15:29:57 +02:00
margin - top : var ( -- space - 4 ) ;
}
2025-10-01 12:03:31 +02:00
[ id *= 'select_list' ] > * : : before {
2025-05-27 15:29:57 +02:00
content : '' ;
position : absolute ;
left : var ( -- space - 8 ) ;
width : 1.75 rem ;
height : 1.75 rem ;
border - radius : var ( -- rounded - sm ) ;
background - image : var ( -- image ) ;
2025-05-28 16:08:01 +02:00
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 ;
}
2025-10-01 12:03:31 +02:00
[ id *= 'select_list' ] > * : hover {
2025-05-27 15:29:57 +02:00
background - color : var ( -- color - grey - 50 ) ;
}
2025-10-01 12:03:31 +02:00
[ 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 : 2 px solid var ( -- color - focus - ring ) ;
}
/* Check */
2025-10-01 12:03:31 +02:00
# 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 ) + 1 px ) ;
width : 1.75 rem ;
height : 1.75 rem ;
border - radius : var ( -- rounded - sm ) ;
}
2025-10-01 12:03:31 +02:00
# selector - multiselect _list input [ type = 'checkbox' ] : checked {
2025-06-18 12:06:33 +02:00
-- icon - color : var ( -- color - focus - ring ) ;
}
2025-10-01 12:03:31 +02:00
[ id *= 'select_list' ] > * > svg {
2025-05-27 15:29:57 +02:00
position : absolute ;
left : 0.875 rem ;
width : 1 rem ;
height : 1 rem ;
color : var ( -- color - grey - 700 ) ;
}
2025-10-01 12:03:31 +02:00
[ id *= 'select_list' ] img {
2025-05-28 16:08:01 +02:00
position : absolute ;
left : 0.5 rem ;
width : 1.75 rem ;
height : 1.75 rem ;
mix - blend - mode : multiply ;
}
2025-10-01 12:03:31 +02:00
[ 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 ;
}
2025-10-01 12:03:31 +02:00
# selector - multiselect _list
[ aria - selected = 'false' ]
input [ type = 'checkbox' ] : : before {
2025-06-18 12:06:33 +02:00
-- icon - color : transparent ;
}
/* Overlay */
2025-10-01 12:03:31 +02:00
[ data - pc - section = 'overlay' ] [ data - pc - section = 'header' ] {
2025-05-28 16:08:01 +02:00
display : none ;
}
2025-05-27 15:29:57 +02:00
< / style >