@xy-planning-network/trees 0.7.5-dev → 0.7.5-rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/trees.es.js +4058 -3743
- package/dist/trees.umd.js +10 -10
- package/package.json +2 -2
- package/src/lib-components/forms/BaseInput.vue +80 -42
- package/src/lib-components/forms/Checkbox.vue +36 -26
- package/src/lib-components/forms/DateRangePicker.vue +50 -30
- package/src/lib-components/forms/FieldsetLegend.vue +23 -3
- package/src/lib-components/forms/InputHelp.vue +1 -1
- package/src/lib-components/forms/InputLabel.vue +24 -4
- package/src/lib-components/forms/MultiCheckboxes.vue +95 -44
- package/src/lib-components/forms/Radio.vue +59 -37
- package/src/lib-components/forms/RadioCards.vue +45 -57
- package/src/lib-components/forms/Select.vue +37 -40
- package/src/lib-components/forms/TextArea.vue +36 -28
- package/src/lib-components/forms/Toggle.vue +9 -6
- package/src/lib-components/forms/YesOrNoRadio.vue +49 -35
- package/src/lib-components/lists/DynamicTable.vue +43 -20
- package/types/composables/forms.d.ts +110 -2
- package/types/lib-components/forms/BaseInput.vue.d.ts +60 -34
- package/types/lib-components/forms/Checkbox.vue.d.ts +50 -29
- package/types/lib-components/forms/DateRangePicker.vue.d.ts +71 -39
- package/types/lib-components/forms/FieldsetLegend.vue.d.ts +28 -27
- package/types/lib-components/forms/InputHelp.vue.d.ts +19 -27
- package/types/lib-components/forms/InputLabel.vue.d.ts +28 -27
- package/types/lib-components/forms/MultiCheckboxes.vue.d.ts +75 -47
- package/types/lib-components/forms/Radio.vue.d.ts +62 -47
- package/types/lib-components/forms/RadioCards.vue.d.ts +68 -71
- package/types/lib-components/forms/Select.vue.d.ts +55 -39
- package/types/lib-components/forms/TextArea.vue.d.ts +50 -32
- package/types/lib-components/forms/Toggle.vue.d.ts +27 -32
- package/types/lib-components/forms/YesOrNoRadio.vue.d.ts +50 -32
- package/types/lib-components/indicators/XYSpinner.vue.d.ts +1 -1
- package/types/lib-components/layout/DateFilter.vue.d.ts +28 -20
- package/types/lib-components/layout/SidebarLayout.vue.d.ts +38 -32
- package/types/lib-components/layout/StackedLayout.vue.d.ts +45 -33
- package/types/lib-components/lists/Cards.vue.d.ts +14 -17
- package/types/lib-components/lists/DataTable.vue.d.ts +31 -31
- package/types/lib-components/lists/DetailList.vue.d.ts +38 -34
- package/types/lib-components/lists/DownloadCell.vue.d.ts +18 -15
- package/types/lib-components/lists/DynamicTable.vue.d.ts +32 -32
- package/types/lib-components/lists/TableActionButtons.vue.d.ts +11 -23
- package/types/lib-components/navigation/ActionsDropdown.vue.d.ts +11 -23
- package/types/lib-components/navigation/Paginator.vue.d.ts +12 -15
- package/types/lib-components/navigation/Steps.vue.d.ts +54 -39
- package/types/lib-components/navigation/Tabs.vue.d.ts +34 -34
- package/types/lib-components/overlays/ContentModal.vue.d.ts +22 -28
- package/types/lib-components/overlays/Modal.vue.d.ts +47 -42
- package/types/lib-components/overlays/Popover/Popover.vue.d.ts +23 -31
- package/types/lib-components/overlays/Popover/PopoverContent.vue.d.ts +1 -1
- package/types/lib-components/overlays/Slideover.vue.d.ts +30 -22
- package/types/lib-components/overlays/Tooltip.vue.d.ts +20 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xy-planning-network/trees",
|
|
3
|
-
"version": "0.7.5-
|
|
3
|
+
"version": "0.7.5-rc2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:xy-planning-network/trees",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"tsc-alias": "^1.8.5",
|
|
53
53
|
"typescript": "^5.0.4",
|
|
54
54
|
"vite": "^4.3.9",
|
|
55
|
-
"vue-tsc": "^1.
|
|
55
|
+
"vue-tsc": "^1.8.18"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@floating-ui/vue": "^1.0.1",
|
|
@@ -1,69 +1,107 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import InputLabel from "./InputLabel.vue"
|
|
3
3
|
import InputHelp from "./InputHelp.vue"
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useInputField,
|
|
6
|
+
defaultInputProps,
|
|
7
|
+
emailPattern,
|
|
8
|
+
looseToNumber,
|
|
9
|
+
passwordPattern,
|
|
10
|
+
phonePattern,
|
|
11
|
+
} from "@/composables/forms"
|
|
12
|
+
import type { TextLikeInput } from "@/composables/forms"
|
|
13
|
+
import { computed, ref } from "vue"
|
|
5
14
|
|
|
6
15
|
defineOptions({
|
|
7
16
|
inheritAttrs: false,
|
|
8
17
|
})
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
| "date"
|
|
12
|
-
| "datetime-local"
|
|
13
|
-
| "email"
|
|
14
|
-
| "month"
|
|
15
|
-
| "number"
|
|
16
|
-
| "password"
|
|
17
|
-
| "search"
|
|
18
|
-
| "tel"
|
|
19
|
-
| "text"
|
|
20
|
-
| "time"
|
|
21
|
-
| "url"
|
|
22
|
-
| "week"
|
|
19
|
+
const props = withDefaults(defineProps<TextLikeInput>(), defaultInputProps)
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
21
|
+
defineEmits(["update:modelValue", "update:error"])
|
|
22
|
+
const targetInput = ref<HTMLInputElement | null>(null)
|
|
23
|
+
const {
|
|
24
|
+
errorState,
|
|
25
|
+
modelState,
|
|
26
|
+
inputID,
|
|
27
|
+
isRequired,
|
|
28
|
+
onInvalid,
|
|
29
|
+
inputValidation,
|
|
30
|
+
} = useInputField({ props, targetInput })
|
|
31
|
+
|
|
32
|
+
// A wrapper component may need to have direct access
|
|
33
|
+
// to the underlying HTMLInputElement that BaseInput binds to
|
|
34
|
+
// example: GoogleMaps Autocomplete inputs
|
|
35
|
+
defineExpose({ input: targetInput })
|
|
36
|
+
|
|
37
|
+
const typeAttributes = computed(() => {
|
|
38
|
+
switch (props.type) {
|
|
39
|
+
case "number":
|
|
40
|
+
return {
|
|
41
|
+
max: Number.MAX_SAFE_INTEGER,
|
|
42
|
+
min: Number.MIN_SAFE_INTEGER,
|
|
43
|
+
}
|
|
44
|
+
case "email":
|
|
45
|
+
return {
|
|
46
|
+
pattern: emailPattern,
|
|
47
|
+
}
|
|
48
|
+
case "password":
|
|
49
|
+
return {
|
|
50
|
+
pattern: passwordPattern,
|
|
51
|
+
}
|
|
52
|
+
case "tel":
|
|
53
|
+
return {
|
|
54
|
+
pattern: phonePattern,
|
|
55
|
+
}
|
|
56
|
+
default:
|
|
57
|
+
return {}
|
|
35
58
|
}
|
|
36
|
-
)
|
|
59
|
+
})
|
|
37
60
|
|
|
38
|
-
const
|
|
39
|
-
|
|
61
|
+
const onInput = (e: Event) => {
|
|
62
|
+
let val = (e.target as HTMLInputElement).value
|
|
63
|
+
|
|
64
|
+
if (props.type === "number") {
|
|
65
|
+
val = looseToNumber(val)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
modelState.value = val
|
|
69
|
+
|
|
70
|
+
inputValidation(e)
|
|
71
|
+
}
|
|
40
72
|
</script>
|
|
41
73
|
|
|
42
74
|
<template>
|
|
43
75
|
<div>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
76
|
+
<InputLabel
|
|
77
|
+
:id="`${inputID}-label`"
|
|
78
|
+
class="mb-2"
|
|
79
|
+
:for="inputID"
|
|
80
|
+
:label="label"
|
|
81
|
+
:required="isRequired"
|
|
82
|
+
/>
|
|
47
83
|
<input
|
|
48
84
|
:id="inputID"
|
|
85
|
+
ref="targetInput"
|
|
49
86
|
:aria-labelledby="label ? `${inputID}-label` : undefined"
|
|
50
87
|
:aria-describedby="help ? `${inputID}-help` : undefined"
|
|
51
88
|
:class="[
|
|
52
89
|
'block w-full rounded-md border-0 py-2 shadow-sm ring-1 ring-inset focus:ring-2 sm:text-sm sm:leading-6',
|
|
53
|
-
'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-
|
|
54
|
-
|
|
55
|
-
? 'text-
|
|
56
|
-
: 'text-
|
|
90
|
+
'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-700 disabled:ring-gray-200',
|
|
91
|
+
errorState
|
|
92
|
+
? 'text-red-900 ring-red-700 placeholder:text-red-300 focus:ring-red-700'
|
|
93
|
+
: 'text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-xy-blue-500',
|
|
57
94
|
]"
|
|
95
|
+
:placeholder="placeholder"
|
|
58
96
|
:type="type"
|
|
59
|
-
:value="
|
|
60
|
-
v-bind="
|
|
61
|
-
@input="
|
|
62
|
-
|
|
63
|
-
"
|
|
97
|
+
:value="modelState"
|
|
98
|
+
v-bind="{ ...typeAttributes, ...$attrs }"
|
|
99
|
+
@input="onInput"
|
|
100
|
+
@invalid="onInvalid"
|
|
64
101
|
/>
|
|
65
|
-
<
|
|
66
|
-
|
|
102
|
+
<InputHelp :id="`${inputID}-help`" class="mt-1" :text="help" />
|
|
103
|
+
<div v-if="errorState" class="mt-0.5">
|
|
104
|
+
<p class="text-sm text-red-700">{{ errorState }}</p>
|
|
67
105
|
</div>
|
|
68
106
|
</div>
|
|
69
107
|
</template>
|
|
@@ -1,49 +1,55 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import InputLabel from "./InputLabel.vue"
|
|
3
3
|
import InputHelp from "./InputHelp.vue"
|
|
4
|
-
import { useInputField } from "@/composables/forms"
|
|
4
|
+
import { useInputField, defaultInputProps } from "@/composables/forms"
|
|
5
|
+
import type { BooleanInput } from "@/composables/forms"
|
|
6
|
+
import { ref } from "vue"
|
|
5
7
|
|
|
6
8
|
defineOptions({
|
|
7
9
|
inheritAttrs: false,
|
|
8
10
|
})
|
|
9
11
|
|
|
10
|
-
withDefaults(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
const props = withDefaults(defineProps<BooleanInput>(), defaultInputProps)
|
|
13
|
+
|
|
14
|
+
defineEmits(["update:modelValue", "update:error"])
|
|
15
|
+
const targetInput = ref<HTMLInputElement | null>(null)
|
|
16
|
+
const {
|
|
17
|
+
inputID,
|
|
18
|
+
isDisabled,
|
|
19
|
+
isRequired,
|
|
20
|
+
errorState,
|
|
21
|
+
modelState,
|
|
22
|
+
validate,
|
|
23
|
+
onInvalid,
|
|
24
|
+
} = useInputField({ props, targetInput })
|
|
25
|
+
|
|
26
|
+
const onChange = (e: Event) => {
|
|
27
|
+
modelState.value = (e.target as HTMLInputElement).checked
|
|
28
|
+
validate(e)
|
|
29
|
+
}
|
|
23
30
|
</script>
|
|
24
31
|
|
|
25
32
|
<template>
|
|
26
33
|
<div class="relative flex items-start">
|
|
27
|
-
<div class="flex items-center h-
|
|
34
|
+
<div class="flex items-center h-5">
|
|
28
35
|
<input
|
|
29
36
|
:id="inputID"
|
|
37
|
+
ref="targetInput"
|
|
30
38
|
:aria-labelledby="label ? `${inputID}-label` : undefined"
|
|
31
39
|
:aria-describedby="help ? `${inputID}-help` : undefined"
|
|
32
|
-
:checked="
|
|
40
|
+
:checked="modelState || undefined"
|
|
33
41
|
:class="[
|
|
34
|
-
'h-4 w-4 rounded',
|
|
35
|
-
'border-gray-300 text-xy-blue',
|
|
42
|
+
'h-4 w-4 rounded text-xy-blue cursor-pointer',
|
|
36
43
|
'disabled:bg-gray-100 disabled:border-gray-200 disabled:cursor-not-allowed disabled:opacity-100',
|
|
37
44
|
'checked:disabled:bg-xy-blue checked:disabled:border-xy-blue checked:disabled:opacity-50',
|
|
38
|
-
|
|
45
|
+
errorState
|
|
46
|
+
? 'border-red-700 focus:ring-red-700'
|
|
47
|
+
: 'border-gray-300 focus:ring-xy-blue-500',
|
|
39
48
|
]"
|
|
40
49
|
type="checkbox"
|
|
41
|
-
v-bind="
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
...$attrs,
|
|
46
|
-
}"
|
|
50
|
+
v-bind="$attrs"
|
|
51
|
+
@change="onChange"
|
|
52
|
+
@invalid="onInvalid"
|
|
47
53
|
/>
|
|
48
54
|
</div>
|
|
49
55
|
<div class="ml-3">
|
|
@@ -51,9 +57,13 @@ const { inputID, isDisabled, isValid } = useInputField()
|
|
|
51
57
|
:id="`${inputID}-label`"
|
|
52
58
|
:for="inputID"
|
|
53
59
|
:label="label"
|
|
54
|
-
:class="isDisabled
|
|
60
|
+
:class="isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'"
|
|
61
|
+
:required="isRequired"
|
|
55
62
|
/>
|
|
56
63
|
<InputHelp :id="`${inputID}-help`" :text="help"></InputHelp>
|
|
64
|
+
<div v-if="errorState" class="mt-0.5">
|
|
65
|
+
<p class="text-sm text-red-700">{{ errorState }}</p>
|
|
66
|
+
</div>
|
|
57
67
|
</div>
|
|
58
68
|
</div>
|
|
59
69
|
</template>
|
|
@@ -1,42 +1,38 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import flatpickr from "flatpickr"
|
|
3
3
|
import "flatpickr/dist/flatpickr.min.css"
|
|
4
|
-
import { onMounted } from "vue"
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
4
|
+
import { onMounted, ref } from "vue"
|
|
5
|
+
import { defaultInputProps, useInputField } from "@/composables/forms"
|
|
6
|
+
import type { DateRangeInput } from "@/composables/forms"
|
|
7
7
|
|
|
8
8
|
defineOptions({
|
|
9
9
|
inheritAttrs: false,
|
|
10
10
|
})
|
|
11
11
|
|
|
12
|
-
const props = withDefaults(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const props = withDefaults(defineProps<DateRangeInput>(), {
|
|
13
|
+
...defaultInputProps,
|
|
14
|
+
maxRange: 0,
|
|
15
|
+
modelValue: () => {
|
|
16
|
+
return {
|
|
17
|
+
maxDate: 0,
|
|
18
|
+
minDate: 0,
|
|
17
19
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}>(),
|
|
23
|
-
{
|
|
24
|
-
maxRange: 0,
|
|
25
|
-
startDate: 0,
|
|
26
|
-
label: "",
|
|
27
|
-
help: "",
|
|
28
|
-
}
|
|
29
|
-
)
|
|
20
|
+
},
|
|
21
|
+
placeholder: "mm-dd-yyyy range",
|
|
22
|
+
startDate: 0,
|
|
23
|
+
})
|
|
30
24
|
|
|
31
|
-
const
|
|
32
|
-
const { inputID } =
|
|
25
|
+
const targetInput = ref<HTMLInputElement | null>(null)
|
|
26
|
+
const { errorState, modelState, inputID, isRequired, onInvalid, validate } =
|
|
27
|
+
useInputField({ props, targetInput })
|
|
33
28
|
|
|
34
29
|
const updateModelValue = (value: { minDate: number; maxDate: number }) => {
|
|
35
|
-
|
|
30
|
+
modelState.value = value
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
onMounted(() => {
|
|
39
34
|
const opts: flatpickr.Options.Options = {
|
|
35
|
+
allowInput: true,
|
|
40
36
|
dateFormat: "m-d-Y",
|
|
41
37
|
mode: "range",
|
|
42
38
|
maxDate: new Date(), // So far, we cannot have options past today for ranges
|
|
@@ -92,11 +88,35 @@ onMounted(() => {
|
|
|
92
88
|
</script>
|
|
93
89
|
|
|
94
90
|
<template>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
91
|
+
<div>
|
|
92
|
+
<InputLabel
|
|
93
|
+
:id="`${inputID}-label`"
|
|
94
|
+
class="mb-2"
|
|
95
|
+
:for="inputID"
|
|
96
|
+
:label="label"
|
|
97
|
+
:required="isRequired"
|
|
98
|
+
/>
|
|
99
|
+
<input
|
|
100
|
+
:id="inputID"
|
|
101
|
+
ref="targetInput"
|
|
102
|
+
:aria-labelledby="label ? `${inputID}-label` : undefined"
|
|
103
|
+
:aria-describedby="help ? `${inputID}-help` : undefined"
|
|
104
|
+
:class="[
|
|
105
|
+
'block w-full rounded-md border-0 py-2 shadow-sm ring-1 ring-inset focus:ring-2 sm:text-sm sm:leading-6',
|
|
106
|
+
'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-700 disabled:ring-gray-200',
|
|
107
|
+
errorState
|
|
108
|
+
? 'text-red-900 ring-red-700 placeholder:text-red-300 focus:ring-red-700'
|
|
109
|
+
: 'text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-xy-blue-500',
|
|
110
|
+
]"
|
|
111
|
+
:placeholder="placeholder"
|
|
112
|
+
v-bind="$attrs"
|
|
113
|
+
type="text"
|
|
114
|
+
@input="validate"
|
|
115
|
+
@invalid="onInvalid"
|
|
116
|
+
/>
|
|
117
|
+
<InputHelp :id="`${inputID}-help`" class="mt-1" :text="help" />
|
|
118
|
+
<div v-if="errorState" class="mt-0.5">
|
|
119
|
+
<p class="text-sm text-red-700">{{ errorState }}</p>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
102
122
|
</template>
|
|
@@ -1,23 +1,43 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
import { computed } from "vue"
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
3
5
|
defineProps<{
|
|
4
6
|
label?: string
|
|
7
|
+
required?: boolean
|
|
5
8
|
tag?: string
|
|
6
9
|
}>(),
|
|
7
10
|
{
|
|
8
11
|
label: "",
|
|
12
|
+
required: false,
|
|
9
13
|
tag: "legend",
|
|
10
14
|
}
|
|
11
15
|
)
|
|
16
|
+
|
|
17
|
+
const labelDisplay = computed((): string => {
|
|
18
|
+
if (!props.required) {
|
|
19
|
+
return props.label
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// remove trailing * on the existing label
|
|
23
|
+
const regex = /\*$/gm
|
|
24
|
+
|
|
25
|
+
if (regex.exec(props.label) !== null) {
|
|
26
|
+
return props.label.substring(0, props.label.length - 1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return props.label
|
|
30
|
+
})
|
|
12
31
|
</script>
|
|
13
32
|
|
|
14
33
|
<template>
|
|
15
34
|
<component
|
|
16
35
|
:is="tag"
|
|
17
36
|
v-if="label"
|
|
18
|
-
class="block text-base
|
|
37
|
+
class="block text-base leading-snug font-medium text-gray-800"
|
|
19
38
|
v-bind="$attrs"
|
|
20
39
|
>
|
|
21
|
-
{{
|
|
40
|
+
{{ labelDisplay }}
|
|
41
|
+
<span v-if="props.required" class="text-red-700/80">*</span>
|
|
22
42
|
</component>
|
|
23
43
|
</template>
|
|
@@ -1,23 +1,43 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
import { computed } from "vue"
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
3
5
|
defineProps<{
|
|
4
6
|
label?: string
|
|
7
|
+
required?: boolean
|
|
5
8
|
tag?: string
|
|
6
9
|
}>(),
|
|
7
10
|
{
|
|
8
11
|
label: "",
|
|
12
|
+
required: false,
|
|
9
13
|
tag: "label",
|
|
10
14
|
}
|
|
11
15
|
)
|
|
16
|
+
|
|
17
|
+
const labelDisplay = computed((): string => {
|
|
18
|
+
if (!props.required) {
|
|
19
|
+
return props.label
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// remove trailing * on the existing label
|
|
23
|
+
const regex = /\*$/gm
|
|
24
|
+
|
|
25
|
+
if (regex.exec(props.label) !== null) {
|
|
26
|
+
return props.label.substring(0, props.label.length - 1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return props.label
|
|
30
|
+
})
|
|
12
31
|
</script>
|
|
13
32
|
|
|
14
33
|
<template>
|
|
15
34
|
<component
|
|
16
35
|
:is="tag"
|
|
17
|
-
v-if="
|
|
18
|
-
class="block text-sm
|
|
36
|
+
v-if="labelDisplay"
|
|
37
|
+
class="block text-sm leading-snug font-medium text-gray-800"
|
|
19
38
|
v-bind="$attrs"
|
|
20
39
|
>
|
|
21
|
-
{{
|
|
40
|
+
{{ labelDisplay }}
|
|
41
|
+
<span v-if="props.required" class="text-red-700/80">*</span>
|
|
22
42
|
</component>
|
|
23
43
|
</template>
|
|
@@ -2,68 +2,117 @@
|
|
|
2
2
|
import FieldsetLegend from "./FieldsetLegend.vue"
|
|
3
3
|
import InputLabel from "./InputLabel.vue"
|
|
4
4
|
import InputHelp from "./InputHelp.vue"
|
|
5
|
-
import { useInputField } from "@/composables/forms"
|
|
5
|
+
import { useInputField, defaultInputProps } from "@/composables/forms"
|
|
6
|
+
import type { MultiChoiceInput, ColumnedInput } from "@/composables/forms"
|
|
7
|
+
import { computed, ref } from "vue"
|
|
6
8
|
|
|
7
9
|
defineOptions({
|
|
8
10
|
inheritAttrs: false,
|
|
9
11
|
})
|
|
10
12
|
|
|
11
|
-
type CheckboxValue = string | number
|
|
12
|
-
type ModelValue = CheckboxValue[]
|
|
13
|
-
|
|
14
13
|
const props = withDefaults(
|
|
15
|
-
defineProps<
|
|
16
|
-
|
|
17
|
-
disabled?: boolean
|
|
18
|
-
help?: string
|
|
19
|
-
label: string
|
|
20
|
-
value: CheckboxValue
|
|
21
|
-
}[]
|
|
22
|
-
help?: string
|
|
23
|
-
label?: string
|
|
24
|
-
modelValue: ModelValue
|
|
25
|
-
columns?: 2 | 3
|
|
26
|
-
}>(),
|
|
27
|
-
{
|
|
28
|
-
help: "",
|
|
29
|
-
label: "",
|
|
30
|
-
columns: undefined,
|
|
31
|
-
}
|
|
14
|
+
defineProps<MultiChoiceInput & ColumnedInput>(),
|
|
15
|
+
defaultInputProps
|
|
32
16
|
)
|
|
33
17
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
18
|
+
defineEmits(["update:modelValue", "update:error"])
|
|
19
|
+
const targetInput = ref<HTMLInputElement | null>(null)
|
|
20
|
+
const { inputID, isDisabled, modelState, errorState, validate } = useInputField(
|
|
21
|
+
{ props, targetInput }
|
|
22
|
+
)
|
|
37
23
|
|
|
38
|
-
const
|
|
24
|
+
const onChange = (e: Event, val: string | number) => {
|
|
25
|
+
const checked = (e.target as HTMLInputElement).checked
|
|
39
26
|
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
if (!Array.isArray(modelState.value)) {
|
|
28
|
+
modelState.value = []
|
|
29
|
+
}
|
|
42
30
|
|
|
43
31
|
if (checked) {
|
|
44
|
-
|
|
32
|
+
modelState.value.push(val)
|
|
45
33
|
} else {
|
|
46
|
-
|
|
34
|
+
modelState.value.splice(modelState.value.indexOf(val), 1)
|
|
47
35
|
}
|
|
48
36
|
|
|
49
|
-
|
|
37
|
+
validate(e)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const selectionCount = computed(() => {
|
|
41
|
+
if (Array.isArray(modelState.value)) {
|
|
42
|
+
return modelState.value.length
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return 0
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const minCount = computed(() => {
|
|
49
|
+
return props.min || 0
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const maxCount = computed(() => {
|
|
53
|
+
return (
|
|
54
|
+
props.max ||
|
|
55
|
+
props.options.filter((opt) => {
|
|
56
|
+
return !opt.disabled
|
|
57
|
+
}).length
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const countError = computed(() => {
|
|
62
|
+
if (selectionCount.value < minCount.value) {
|
|
63
|
+
return `Please select ${minCount.value} of these option${
|
|
64
|
+
minCount.value > 1 ? "s" : ""
|
|
65
|
+
}.`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (selectionCount.value > maxCount.value) {
|
|
69
|
+
return `Please select only ${maxCount.value} of these option${
|
|
70
|
+
maxCount.value > 1 ? "s" : ""
|
|
71
|
+
}.`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return ""
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const setValidationError = () => {
|
|
78
|
+
if (!errorState.value) {
|
|
79
|
+
errorState.value = countError.value
|
|
80
|
+
}
|
|
50
81
|
}
|
|
51
82
|
</script>
|
|
52
83
|
|
|
53
84
|
<template>
|
|
54
85
|
<fieldset
|
|
55
|
-
class="space-y-
|
|
86
|
+
class="relative space-y-4"
|
|
56
87
|
:aria-labelledby="label ? `${inputID}-legend` : undefined"
|
|
57
88
|
:aria-describedby="help ? `${inputID}-help` : undefined"
|
|
58
89
|
>
|
|
59
90
|
<div v-if="label">
|
|
60
|
-
<FieldsetLegend
|
|
91
|
+
<FieldsetLegend
|
|
92
|
+
:id="`${inputID}-legend`"
|
|
93
|
+
:label="label"
|
|
94
|
+
:required="minCount > 0"
|
|
95
|
+
/>
|
|
61
96
|
<InputHelp v-if="help" :id="`${inputID}-help`" tag="p" :text="help" />
|
|
62
97
|
</div>
|
|
63
98
|
|
|
99
|
+
<div v-if="errorState" class="mt-0.5">
|
|
100
|
+
<p class="text-sm text-red-700">{{ errorState }}</p>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<input
|
|
104
|
+
v-if="countError || errorState"
|
|
105
|
+
ref="targetInput"
|
|
106
|
+
required
|
|
107
|
+
class="sr-only top-1 left-1"
|
|
108
|
+
aria-hidden
|
|
109
|
+
type="checkbox"
|
|
110
|
+
@invalid="setValidationError"
|
|
111
|
+
/>
|
|
112
|
+
|
|
64
113
|
<div class="flex">
|
|
65
114
|
<div
|
|
66
|
-
class="grid gap-y-
|
|
115
|
+
class="grid gap-y-6"
|
|
67
116
|
:class="{
|
|
68
117
|
'sm:grid sm:gap-x-5 sm:space-y-0': columns !== undefined,
|
|
69
118
|
'sm:grid-cols-2': columns === 2,
|
|
@@ -75,28 +124,26 @@ const onChange = (checked: boolean, val: CheckboxValue) => {
|
|
|
75
124
|
:key="option.value"
|
|
76
125
|
class="flex items-start"
|
|
77
126
|
>
|
|
78
|
-
<div class="flex items-center h-
|
|
127
|
+
<div class="flex items-center h-5">
|
|
79
128
|
<input
|
|
80
129
|
:id="`${inputID}-${index}`"
|
|
81
130
|
:aria-labelledby="`${inputID}-${index}-label`"
|
|
82
131
|
:aria-describedby="
|
|
83
132
|
option.help ? `${inputID}-${index}-help` : undefined
|
|
84
133
|
"
|
|
85
|
-
:checked="modelValue
|
|
134
|
+
:checked="modelValue?.includes(option.value)"
|
|
86
135
|
:disabled="option.disabled"
|
|
87
136
|
:class="[
|
|
88
|
-
'h-4 w-4 rounded',
|
|
89
|
-
'border-gray-300 text-xy-blue focus:ring-xy-blue-500',
|
|
137
|
+
'h-4 w-4 rounded cursor-pointer',
|
|
90
138
|
'disabled:bg-gray-100 disabled:border-gray-200 disabled:cursor-not-allowed disabled:opacity-100',
|
|
91
139
|
'checked:disabled:bg-xy-blue checked:disabled:border-xy-blue checked:disabled:opacity-50',
|
|
140
|
+
errorState
|
|
141
|
+
? 'border-red-700 focus:ring-red-700'
|
|
142
|
+
: 'border-gray-300 focus:ring-xy-blue-500',
|
|
92
143
|
]"
|
|
93
144
|
type="checkbox"
|
|
94
|
-
v-bind="
|
|
95
|
-
onChange
|
|
96
|
-
onChange(($event.target as HTMLInputElement).checked, option.value)
|
|
97
|
-
},
|
|
98
|
-
...$attrs,
|
|
99
|
-
}"
|
|
145
|
+
v-bind="$attrs"
|
|
146
|
+
@change="onChange($event, option.value)"
|
|
100
147
|
/>
|
|
101
148
|
</div>
|
|
102
149
|
<div class="ml-3">
|
|
@@ -104,7 +151,11 @@ const onChange = (checked: boolean, val: CheckboxValue) => {
|
|
|
104
151
|
:id="`${inputID}-${index}-label`"
|
|
105
152
|
:for="`${inputID}-${index}`"
|
|
106
153
|
:label="option.label"
|
|
107
|
-
:class="
|
|
154
|
+
:class="
|
|
155
|
+
isDisabled || option.disabled
|
|
156
|
+
? 'cursor-not-allowed'
|
|
157
|
+
: 'cursor-pointer'
|
|
158
|
+
"
|
|
108
159
|
/>
|
|
109
160
|
<InputHelp :id="`${inputID}-${index}-help`" :text="option.help" />
|
|
110
161
|
</div>
|