|
|
|
|
@@ -2,8 +2,7 @@
|
|
|
|
|
<div>
|
|
|
|
|
<div
|
|
|
|
|
ref="root"
|
|
|
|
|
class="relative w-full"
|
|
|
|
|
:class="[minWidth, maxWidth]"
|
|
|
|
|
:class="mergedGroupClass"
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
:id="buttonId"
|
|
|
|
|
@@ -26,7 +25,7 @@
|
|
|
|
|
? openDirection === 'down'
|
|
|
|
|
? 'rounded-b-none !border-2 !border-m-primary !border-b-0'
|
|
|
|
|
: 'rounded-t-none !border-2 !border-m-primary !border-t-0'
|
|
|
|
|
: isOptionSelected
|
|
|
|
|
: isOptionSelected
|
|
|
|
|
? 'border-black'
|
|
|
|
|
: 'border-m-muted',
|
|
|
|
|
disabled ? 'cursor-not-allowed border-m-muted text-black/60' : 'cursor-pointer',
|
|
|
|
|
@@ -45,7 +44,7 @@
|
|
|
|
|
v-if="label"
|
|
|
|
|
class="floating-label pointer-events-none absolute left-3 inline-block origin-left transition-transform duration-150 font-medium"
|
|
|
|
|
:class="[
|
|
|
|
|
shouldFloatLabel ? 'top-2 z-30' : 'top-1/2 -translate-y-1/2',
|
|
|
|
|
isOpen ? 'top-2 z-30' : 'top-2',
|
|
|
|
|
hasError
|
|
|
|
|
? 'text-m-danger'
|
|
|
|
|
: hasSuccess
|
|
|
|
|
@@ -206,6 +205,7 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import {computed, onBeforeUnmount, onMounted, ref, useId, nextTick} from 'vue'
|
|
|
|
|
import {Icon as IconifyIcon} from '@iconify/vue'
|
|
|
|
|
import {twMerge} from 'tailwind-merge'
|
|
|
|
|
import Checkbox from '../checkbox/Checkbox.vue'
|
|
|
|
|
|
|
|
|
|
defineOptions({name: 'MalioSelectCheckbox', inheritAttrs: false})
|
|
|
|
|
@@ -232,6 +232,7 @@ const props = withDefaults(defineProps<{
|
|
|
|
|
displaySelectAll?: boolean
|
|
|
|
|
selectAllLabel?: string
|
|
|
|
|
disabled?: boolean
|
|
|
|
|
groupClass?: string
|
|
|
|
|
}>(), {
|
|
|
|
|
options: () => [],
|
|
|
|
|
emptyOptionLabel: '',
|
|
|
|
|
@@ -249,6 +250,7 @@ const props = withDefaults(defineProps<{
|
|
|
|
|
displaySelectAll: false,
|
|
|
|
|
selectAllLabel: 'Tout sélectionner',
|
|
|
|
|
disabled: false,
|
|
|
|
|
groupClass: '',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
@@ -264,6 +266,9 @@ const listboxId = `custom-select-listbox-${uid}`
|
|
|
|
|
const listRef = ref<HTMLElement | null>(null)
|
|
|
|
|
const listHeight = ref(0)
|
|
|
|
|
const normalizedOptions = computed<Option[]>(() => props.options)
|
|
|
|
|
const mergedGroupClass = computed(() =>
|
|
|
|
|
twMerge('relative w-full', props.minWidth, props.maxWidth, props.groupClass),
|
|
|
|
|
)
|
|
|
|
|
const hasError = computed(() => !!props.error)
|
|
|
|
|
const hasSuccess = computed(() => !!props.success && !hasError.value)
|
|
|
|
|
const isOptionSelected = computed(() =>
|
|
|
|
|
@@ -281,6 +286,10 @@ const shouldFloatLabel = computed(() =>
|
|
|
|
|
const selectionSummary = computed(() =>
|
|
|
|
|
`${props.modelValue.length}/${normalizedOptions.value.length}`
|
|
|
|
|
)
|
|
|
|
|
const allSelected = computed(() =>
|
|
|
|
|
normalizedOptions.value.length > 0
|
|
|
|
|
&& normalizedOptions.value.every(opt => props.modelValue.includes(opt.value)),
|
|
|
|
|
)
|
|
|
|
|
const describedBy = computed(() =>
|
|
|
|
|
(hasError.value || hasSuccess.value || !!props.hint) ? `${buttonId}-describedby` : undefined,
|
|
|
|
|
)
|
|
|
|
|
@@ -320,18 +329,22 @@ function open() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const labelTransformStyle = computed(() => {
|
|
|
|
|
// label non flottant
|
|
|
|
|
if (!shouldFloatLabel.value) {
|
|
|
|
|
return undefined
|
|
|
|
|
return {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fermé ou ouverture vers le bas : comportement classique
|
|
|
|
|
if (!isOpen.value || openDirection.value === 'down') {
|
|
|
|
|
return {
|
|
|
|
|
transform: 'translateY(-1.15rem) scale(0.9)',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ouverture vers le haut : on remonte en fonction de la hauteur de la liste
|
|
|
|
|
const extraOffset = 8 // marge visuelle au-dessus de la liste en px
|
|
|
|
|
const total = 4 + listHeight.value + extraOffset
|
|
|
|
|
const total = 4 +listHeight.value + extraOffset
|
|
|
|
|
// 18 ≈ 1.15rem pour garder la même base que votre flottant actuel
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
transform: `translateY(-${total}px) scale(0.9)`,
|
|
|
|
|
@@ -351,19 +364,6 @@ function toggle() {
|
|
|
|
|
open()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const allSelected = computed(() =>
|
|
|
|
|
normalizedOptions.value.length > 0
|
|
|
|
|
&& normalizedOptions.value.every(opt => props.modelValue.includes(opt.value)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
function toggleAll() {
|
|
|
|
|
if (allSelected.value) {
|
|
|
|
|
emit('update:modelValue', [])
|
|
|
|
|
} else {
|
|
|
|
|
emit('update:modelValue', normalizedOptions.value.map(opt => opt.value))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isChecked(value: string | number) {
|
|
|
|
|
return props.modelValue.includes(value)
|
|
|
|
|
}
|
|
|
|
|
@@ -373,10 +373,17 @@ function toggleOption(value: string | number) {
|
|
|
|
|
emit('update:modelValue', props.modelValue.filter(item => item !== value))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit('update:modelValue', [...props.modelValue, value])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleAll() {
|
|
|
|
|
if (allSelected.value) {
|
|
|
|
|
emit('update:modelValue', [])
|
|
|
|
|
} else {
|
|
|
|
|
emit('update:modelValue', normalizedOptions.value.map(opt => opt.value))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onClickOutside(e: MouseEvent) {
|
|
|
|
|
if (!root.value) return
|
|
|
|
|
if (!root.value.contains(e.target as Node)) close()
|
|
|
|
|
|