177 lines
3.3 KiB
Vue
177 lines
3.3 KiB
Vue
<template>
|
|
<div
|
|
:id="wrapperId"
|
|
class="choice-checklist"
|
|
>
|
|
<div class="choice-checklist-controls">
|
|
<button
|
|
type="button"
|
|
class="choice-checklist-control"
|
|
@click="selectAll"
|
|
>
|
|
Select all
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="choice-checklist-control"
|
|
@click="selectNone"
|
|
>
|
|
Select none
|
|
</button>
|
|
</div>
|
|
<fieldset class="choice-checklist-fieldset">
|
|
<legend class="visually-hidden">
|
|
{{ label || name }}
|
|
</legend>
|
|
<label
|
|
v-for="(choice, index) in choices"
|
|
:key="choice.value"
|
|
class="choice-checklist-item"
|
|
:for="optionId(index)"
|
|
>
|
|
<input
|
|
:id="optionId(index)"
|
|
type="checkbox"
|
|
:checked="isSelected(choice.value)"
|
|
@change="handleToggle(choice.value)"
|
|
>
|
|
<span>{{ choiceLabel(choice) }}</span>
|
|
</label>
|
|
</fieldset>
|
|
<input
|
|
:id="valueId"
|
|
:name="name"
|
|
type="text"
|
|
class="visually-hidden choice-checklist-value"
|
|
:value="modelValue"
|
|
:required="required"
|
|
tabindex="-1"
|
|
aria-hidden="true"
|
|
>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
import {
|
|
allChoiceValues,
|
|
choiceLabel,
|
|
formatChecklistValue,
|
|
parseChecklistValue,
|
|
toggleChoice
|
|
} from '../utils/choiceChecklistHelpers.js'
|
|
import {
|
|
argumentFieldOptionId,
|
|
argumentFieldValueId,
|
|
argumentFieldWrapperId
|
|
} from '../utils/argumentFieldIds.js'
|
|
|
|
const props = defineProps({
|
|
name: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
label: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
choices: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
modelValue: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
required: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const selectedValues = computed(() => parseChecklistValue(props.modelValue))
|
|
const wrapperId = computed(() => argumentFieldWrapperId(props.name))
|
|
const valueId = computed(() => argumentFieldValueId(props.name))
|
|
|
|
function optionId (index) {
|
|
return argumentFieldOptionId(props.name, index)
|
|
}
|
|
|
|
function isSelected (value) {
|
|
return selectedValues.value.includes(value)
|
|
}
|
|
|
|
function emitSelection (selected) {
|
|
emit('update:modelValue', formatChecklistValue(selected))
|
|
}
|
|
|
|
function handleToggle (value) {
|
|
emitSelection(toggleChoice(selectedValues.value, value))
|
|
}
|
|
|
|
function selectAll () {
|
|
emitSelection(allChoiceValues(props.choices))
|
|
}
|
|
|
|
function selectNone () {
|
|
emitSelection([])
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.choice-checklist {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5em;
|
|
}
|
|
|
|
.choice-checklist-controls {
|
|
display: flex;
|
|
gap: 0.75em;
|
|
}
|
|
|
|
.choice-checklist-control {
|
|
background: none;
|
|
border: none;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
padding: 0;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.choice-checklist-fieldset {
|
|
border: none;
|
|
display: grid;
|
|
gap: 0.5em 1em;
|
|
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.choice-checklist-item {
|
|
align-items: center;
|
|
display: flex;
|
|
gap: 0.4em;
|
|
margin: 0;
|
|
}
|
|
|
|
.choice-checklist-item input[type="checkbox"] {
|
|
margin: 0;
|
|
}
|
|
|
|
.visually-hidden {
|
|
border: 0;
|
|
clip: rect(0 0 0 0);
|
|
height: 1px;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
padding: 0;
|
|
position: absolute;
|
|
white-space: nowrap;
|
|
width: 1px;
|
|
}
|
|
</style>
|