ChoiceChecklist.vue 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. <template>
  2. <div
  3. :id="wrapperId"
  4. class="choice-checklist"
  5. >
  6. <div class="choice-checklist-controls">
  7. <button
  8. type="button"
  9. class="choice-checklist-control"
  10. @click="selectAll"
  11. >
  12. Select all
  13. </button>
  14. <button
  15. type="button"
  16. class="choice-checklist-control"
  17. @click="selectNone"
  18. >
  19. Select none
  20. </button>
  21. </div>
  22. <fieldset class="choice-checklist-fieldset">
  23. <legend class="visually-hidden">
  24. {{ label || name }}
  25. </legend>
  26. <label
  27. v-for="(choice, index) in choices"
  28. :key="choice.value"
  29. class="choice-checklist-item"
  30. :for="optionId(index)"
  31. >
  32. <input
  33. :id="optionId(index)"
  34. type="checkbox"
  35. :checked="isSelected(choice.value)"
  36. @change="handleToggle(choice.value)"
  37. >
  38. <span>{{ choiceLabel(choice) }}</span>
  39. </label>
  40. </fieldset>
  41. <input
  42. :id="valueId"
  43. :name="name"
  44. type="text"
  45. class="visually-hidden choice-checklist-value"
  46. :value="modelValue"
  47. :required="required"
  48. tabindex="-1"
  49. aria-hidden="true"
  50. >
  51. </div>
  52. </template>
  53. <script setup>
  54. import { computed } from 'vue'
  55. import {
  56. allChoiceValues,
  57. choiceLabel,
  58. formatChecklistValue,
  59. parseChecklistValue,
  60. toggleChoice
  61. } from '../utils/choiceChecklistHelpers.js'
  62. import {
  63. argumentFieldOptionId,
  64. argumentFieldValueId,
  65. argumentFieldWrapperId
  66. } from '../utils/argumentFieldIds.js'
  67. const props = defineProps({
  68. name: {
  69. type: String,
  70. required: true
  71. },
  72. label: {
  73. type: String,
  74. default: ''
  75. },
  76. choices: {
  77. type: Array,
  78. required: true
  79. },
  80. modelValue: {
  81. type: String,
  82. default: ''
  83. },
  84. required: {
  85. type: Boolean,
  86. default: false
  87. }
  88. })
  89. const emit = defineEmits(['update:modelValue'])
  90. const selectedValues = computed(() => parseChecklistValue(props.modelValue))
  91. const wrapperId = computed(() => argumentFieldWrapperId(props.name))
  92. const valueId = computed(() => argumentFieldValueId(props.name))
  93. function optionId (index) {
  94. return argumentFieldOptionId(props.name, index)
  95. }
  96. function isSelected (value) {
  97. return selectedValues.value.includes(value)
  98. }
  99. function emitSelection (selected) {
  100. emit('update:modelValue', formatChecklistValue(selected))
  101. }
  102. function handleToggle (value) {
  103. emitSelection(toggleChoice(selectedValues.value, value))
  104. }
  105. function selectAll () {
  106. emitSelection(allChoiceValues(props.choices))
  107. }
  108. function selectNone () {
  109. emitSelection([])
  110. }
  111. </script>
  112. <style scoped>
  113. .choice-checklist {
  114. display: flex;
  115. flex-direction: column;
  116. gap: 0.5em;
  117. }
  118. .choice-checklist-controls {
  119. display: flex;
  120. gap: 0.75em;
  121. }
  122. .choice-checklist-control {
  123. background: none;
  124. border: none;
  125. color: inherit;
  126. cursor: pointer;
  127. font: inherit;
  128. padding: 0;
  129. text-decoration: underline;
  130. }
  131. .choice-checklist-fieldset {
  132. border: none;
  133. display: grid;
  134. gap: 0.5em 1em;
  135. grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  136. margin: 0;
  137. padding: 0;
  138. }
  139. .choice-checklist-item {
  140. align-items: center;
  141. display: flex;
  142. gap: 0.4em;
  143. margin: 0;
  144. }
  145. .choice-checklist-item input[type="checkbox"] {
  146. margin: 0;
  147. }
  148. .visually-hidden {
  149. border: 0;
  150. clip: rect(0 0 0 0);
  151. height: 1px;
  152. margin: -1px;
  153. overflow: hidden;
  154. padding: 0;
  155. position: absolute;
  156. white-space: nowrap;
  157. width: 1px;
  158. }
  159. </style>