@koumoul/vjsf 3.14.0 → 3.15.0
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/package.json +2 -2
- package/src/components/fragments/text-field-menu.vue +13 -2
- package/src/components/nodes/date-picker.vue +33 -16
- package/src/components/nodes/number-combobox.vue +1 -1
- package/src/utils/dates.js +68 -1
- package/types/components/fragments/text-field-menu.vue.d.ts +5 -2
- package/types/components/fragments/text-field-menu.vue.d.ts.map +1 -1
- package/types/utils/dates.d.ts +4 -0
- package/types/utils/dates.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koumoul/vjsf",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.0",
|
|
4
4
|
"description": "Generate forms for the vuetify UI library (vuejs) based on annotated JSON schemas.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "vitest run",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"vuetify": "^3.6.13"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@json-layout/core": "~1.10.
|
|
74
|
+
"@json-layout/core": "~1.10.1",
|
|
75
75
|
"@json-layout/vocabulary": "~2.3.0",
|
|
76
76
|
"@vueuse/core": "^12.5.0",
|
|
77
77
|
"debug": "^4.3.4"
|
|
@@ -15,24 +15,32 @@ const props = defineProps({
|
|
|
15
15
|
type: Object,
|
|
16
16
|
required: true
|
|
17
17
|
},
|
|
18
|
-
|
|
18
|
+
readonly: {
|
|
19
|
+
/** @type import('vue').PropType<boolean> */
|
|
20
|
+
type: Boolean,
|
|
21
|
+
default: true
|
|
22
|
+
},
|
|
23
|
+
placeholder: {
|
|
19
24
|
/** @type import('vue').PropType<string | null> */
|
|
20
25
|
type: String,
|
|
21
26
|
default: null
|
|
22
27
|
}
|
|
23
28
|
})
|
|
24
29
|
|
|
30
|
+
const emits = defineEmits(['blur'])
|
|
31
|
+
|
|
25
32
|
const { inputProps, skeleton, compProps, data } = useField(
|
|
26
33
|
toRef(props, 'modelValue'), props.statefulLayout, { isMainComp: false, bindData: false }
|
|
27
34
|
)
|
|
28
35
|
|
|
29
36
|
const fieldProps = computed(() => {
|
|
30
37
|
const fieldProps = { ...inputProps.value }
|
|
31
|
-
fieldProps.readonly = true
|
|
32
38
|
fieldProps.clearable = fieldProps.clearable ?? !skeleton.value.required
|
|
39
|
+
if (props.placeholder) fieldProps.placeholder = props.placeholder
|
|
33
40
|
fieldProps['onClick:clear'] = () => {
|
|
34
41
|
props.statefulLayout.input(props.modelValue, null)
|
|
35
42
|
}
|
|
43
|
+
fieldProps.readonly = props.readonly
|
|
36
44
|
return fieldProps
|
|
37
45
|
})
|
|
38
46
|
|
|
@@ -47,6 +55,7 @@ const menuProps = computed(() => {
|
|
|
47
55
|
|
|
48
56
|
const textField = ref(null)
|
|
49
57
|
const menuOpened = defineModel('menuOpened', { type: Boolean, default: false })
|
|
58
|
+
const formattedValue = defineModel('formattedValue', { type: String, default: null })
|
|
50
59
|
|
|
51
60
|
</script>
|
|
52
61
|
|
|
@@ -56,6 +65,8 @@ const menuOpened = defineModel('menuOpened', { type: Boolean, default: false })
|
|
|
56
65
|
v-bind="fieldProps"
|
|
57
66
|
:model-value="formattedValue ?? data"
|
|
58
67
|
@click:control="e => {menuOpened = !menuOpened; e.stopPropagation()}"
|
|
68
|
+
@update:model-value="v => formattedValue = v"
|
|
69
|
+
@blur="emits('blur')"
|
|
59
70
|
>
|
|
60
71
|
<template #prepend-inner>
|
|
61
72
|
<slot name="prepend-inner" />
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import TextFieldMenu from '../fragments/text-field-menu.vue'
|
|
3
3
|
import { VIcon } from 'vuetify/components/VIcon'
|
|
4
4
|
import { VDatePicker } from 'vuetify/components/VDatePicker'
|
|
5
|
-
import {
|
|
6
|
-
import { computed, ref, toRef } from 'vue'
|
|
7
|
-
import { getDateTimeParts, getDateTimeWithOffset } from '../../utils/dates.js'
|
|
5
|
+
import { useDefaults } from 'vuetify'
|
|
6
|
+
import { computed, ref, toRef, watch } from 'vue'
|
|
7
|
+
import { getDateTimeParts, getDateTimeWithOffset, localeKeyboardFormat } from '../../utils/dates.js'
|
|
8
8
|
import useNode from '../../composables/use-node.js'
|
|
9
9
|
|
|
10
10
|
useDefaults({}, 'VjsfDatePicker')
|
|
@@ -22,40 +22,57 @@ const props = defineProps({
|
|
|
22
22
|
}
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
const vDate = useDate()
|
|
26
|
-
|
|
27
25
|
const menuOpened = ref(false)
|
|
28
26
|
|
|
29
27
|
const { compProps, localData } = useNode(toRef(props, 'modelValue'), props.statefulLayout)
|
|
30
28
|
|
|
29
|
+
const updateValue = (/** @type {Date | null} */value) => {
|
|
30
|
+
if (!value) return
|
|
31
|
+
const isoValue = props.modelValue.layout.format === 'date-time'
|
|
32
|
+
? getDateTimeWithOffset(value)
|
|
33
|
+
: getDateTimeParts(/** @type Date */(/** @type unknown */(value)))[0]
|
|
34
|
+
if (isoValue !== localData.value) {
|
|
35
|
+
props.statefulLayout.input(props.modelValue, isoValue)
|
|
36
|
+
menuOpened.value = false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
const datePickerProps = computed(() => {
|
|
41
|
+
/** @type Record<String, any> */
|
|
32
42
|
const datePickerProps = { ...compProps.value }
|
|
33
43
|
datePickerProps.hideActions = true
|
|
34
44
|
if (localData.value) datePickerProps.modelValue = new Date(/** @type {string} */(localData.value))
|
|
35
45
|
datePickerProps['onUpdate:modelValue'] = (/** @type {Date} */value) => {
|
|
36
|
-
|
|
37
|
-
if (props.modelValue.layout.format === 'date-time') {
|
|
38
|
-
props.statefulLayout.input(props.modelValue, getDateTimeWithOffset(value))
|
|
39
|
-
} else {
|
|
40
|
-
props.statefulLayout.input(props.modelValue, getDateTimeParts(/** @type Date */(/** @type unknown */(value)))[0])
|
|
41
|
-
}
|
|
42
|
-
menuOpened.value = false
|
|
46
|
+
updateValue(value)
|
|
43
47
|
}
|
|
44
48
|
return datePickerProps
|
|
45
49
|
})
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
/** @type {import('vue').Ref<string | null>} */
|
|
52
|
+
const formattedValue = ref('')
|
|
53
|
+
const setFormattedValue = () => {
|
|
54
|
+
formattedValue.value = localData.value ? localeKeyboardFormat(props.modelValue.options.locale).format(new Date(localData.value)) : null
|
|
55
|
+
}
|
|
56
|
+
watch(localData, setFormattedValue, { immediate: true })
|
|
57
|
+
const updateFormattedValue = () => {
|
|
58
|
+
if (formattedValue.value) {
|
|
59
|
+
const newValue = localeKeyboardFormat(props.modelValue.options.locale).parse(formattedValue.value)
|
|
60
|
+
if (!newValue) setFormattedValue()
|
|
61
|
+
else updateValue(newValue)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
50
64
|
|
|
51
65
|
</script>
|
|
52
66
|
|
|
53
67
|
<template>
|
|
54
68
|
<text-field-menu
|
|
55
69
|
v-model:menu-opened="menuOpened"
|
|
70
|
+
v-model:formatted-value="formattedValue"
|
|
56
71
|
:model-value="props.modelValue"
|
|
57
72
|
:stateful-layout="statefulLayout"
|
|
58
|
-
:
|
|
73
|
+
:readonly="false"
|
|
74
|
+
:placeholder="props.modelValue.messages.keyboardDate"
|
|
75
|
+
@blur="updateFormattedValue"
|
|
59
76
|
>
|
|
60
77
|
<template #prepend-inner>
|
|
61
78
|
<v-icon :icon="statefulLayout.options.icons.calendar" />
|
|
@@ -44,7 +44,7 @@ export default defineComponent({
|
|
|
44
44
|
fieldProps.chips = true
|
|
45
45
|
fieldProps.closableChips = true
|
|
46
46
|
}
|
|
47
|
-
fieldProps['onUpdate:modelValue'] = (/** @type string[] */value) => props.statefulLayout.input(props.modelValue, value && value.map(Number))
|
|
47
|
+
fieldProps['onUpdate:modelValue'] = (/** @type string[] */value) => props.statefulLayout.input(props.modelValue, value && (Array.isArray(value) ? value.map(Number) : Number(value)))
|
|
48
48
|
return fieldProps
|
|
49
49
|
})
|
|
50
50
|
|
package/src/utils/dates.js
CHANGED
|
@@ -21,7 +21,7 @@ export const getDateTimeWithOffset = (/** @type Date */date) => {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// get the the date and short time components expected by date-time picker from a full date
|
|
24
|
-
// 2020-04-03T21:07:43+02:00 => ['2020-04-03', '
|
|
24
|
+
// 2020-04-03T21:07:43+02:00 => ['2020-04-03', '21:07']
|
|
25
25
|
export const getDateTimeParts = (/** @type Date */date) => {
|
|
26
26
|
return [`${date.getFullYear()}-${padTimeComponent(date.getMonth() + 1)}-${padTimeComponent(date.getDate())}`, `${padTimeComponent(date.getHours())}:${padTimeComponent(date.getMinutes())}`]
|
|
27
27
|
}
|
|
@@ -50,3 +50,70 @@ export const getShortTime = (/** @type string | undefined */time) => {
|
|
|
50
50
|
export const getLongTime = (/** @type string */time) => {
|
|
51
51
|
return time + ':00Z'
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
const applyDateParts = (/** @type {string} */year, /** @type {string} */month, /** @type {string} */day) => {
|
|
55
|
+
if (!year || !month || !day) return null
|
|
56
|
+
const y = Number(year)
|
|
57
|
+
if (isNaN(y)) return null
|
|
58
|
+
const m = Number(month)
|
|
59
|
+
if (isNaN(m)) return null
|
|
60
|
+
if (m < 1 || m > 12) return null
|
|
61
|
+
const d = Number(day)
|
|
62
|
+
if (isNaN(d)) return null
|
|
63
|
+
if (d < 1 || d > 31) return null
|
|
64
|
+
|
|
65
|
+
const date = new Date()
|
|
66
|
+
date.setFullYear(y)
|
|
67
|
+
date.setMonth(m - 1)
|
|
68
|
+
date.setDate(d)
|
|
69
|
+
date.setHours(0)
|
|
70
|
+
date.setMinutes(0)
|
|
71
|
+
date.setSeconds(0)
|
|
72
|
+
date.setMilliseconds(0)
|
|
73
|
+
|
|
74
|
+
return isNaN(date.getTime()) ? null : date
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @type {Record<string, {format: (date: Date) => string, parse: (formateDate: string) => Date | null}>} */
|
|
78
|
+
const localeKeyboardFormats = {}
|
|
79
|
+
export const localeKeyboardFormat = (/** @type string */ locale) => {
|
|
80
|
+
// cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/composables/date/adapters/vuetify.ts#L239
|
|
81
|
+
const format = new Intl.DateTimeFormat(locale, { year: 'numeric', month: '2-digit', day: '2-digit' })
|
|
82
|
+
const parts = format.formatToParts(new Date())
|
|
83
|
+
const fns = {
|
|
84
|
+
format: (/** @type Date */ date) => format.format(date),
|
|
85
|
+
parse: (/** @type string */ formattedDate) => {
|
|
86
|
+
let remainingStr = formattedDate
|
|
87
|
+
let year = ''
|
|
88
|
+
let month = ''
|
|
89
|
+
let day = ''
|
|
90
|
+
for (let i = 0; i < parts.length; i++) {
|
|
91
|
+
const part = parts[i]
|
|
92
|
+
if (part.type !== 'literal') {
|
|
93
|
+
const nextSep = parts[i + 1]
|
|
94
|
+
if (nextSep && nextSep.type !== 'literal') {
|
|
95
|
+
console.error('failed to work on keyboard date format', parts)
|
|
96
|
+
throw new Error('failed to work on keyboard date format')
|
|
97
|
+
}
|
|
98
|
+
let matchValue = remainingStr
|
|
99
|
+
if (nextSep?.type === 'literal') {
|
|
100
|
+
const nextSepPos = remainingStr.indexOf(nextSep.value)
|
|
101
|
+
matchValue = remainingStr.substring(0, nextSepPos)
|
|
102
|
+
remainingStr = remainingStr.substring(nextSepPos + nextSep.value.length)
|
|
103
|
+
}
|
|
104
|
+
if (part.type === 'year') year = matchValue
|
|
105
|
+
if (part.type === 'month') month = matchValue
|
|
106
|
+
if (part.type === 'day') day = matchValue
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const date = applyDateParts(year, month, day)
|
|
110
|
+
if (date) return date
|
|
111
|
+
|
|
112
|
+
// also try iso format
|
|
113
|
+
const [y, m, d] = formattedDate.split('-')
|
|
114
|
+
return applyDateParts(y, m, d)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!localeKeyboardFormats[locale]) localeKeyboardFormats[locale] = fns
|
|
118
|
+
return fns
|
|
119
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<{}, {
|
|
2
|
+
$emit: (event: "blur", ...args: any[]) => void;
|
|
2
3
|
modelValue: import("../../types.js").VjsfNode;
|
|
4
|
+
readonly: boolean;
|
|
3
5
|
statefulLayout: import("../../types.js").VjsfStatefulLayout;
|
|
4
|
-
|
|
6
|
+
placeholder: string | null;
|
|
5
7
|
$props: {
|
|
6
8
|
readonly modelValue?: import("../../types.js").VjsfNode | undefined;
|
|
9
|
+
readonly readonly?: boolean | undefined;
|
|
7
10
|
readonly statefulLayout?: import("../../types.js").VjsfStatefulLayout | undefined;
|
|
8
|
-
readonly
|
|
11
|
+
readonly placeholder?: string | null | undefined;
|
|
9
12
|
};
|
|
10
13
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>, {
|
|
11
14
|
"prepend-inner"?(_: {}): any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-field-menu.vue.d.ts","sourceRoot":"","sources":["../../../src/components/fragments/text-field-menu.vue.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"text-field-menu.vue.d.ts","sourceRoot":"","sources":["../../../src/components/fragments/text-field-menu.vue.js"],"names":[],"mappings":";;;;;;;;;;;;;6BAiKsC,GAAG;;;QACX,GAAG"}
|
package/types/utils/dates.d.ts
CHANGED
|
@@ -4,4 +4,8 @@ export function getDateTimeParts(date: Date): string[];
|
|
|
4
4
|
export function getDateTime(parts: [string, string]): string;
|
|
5
5
|
export function getShortTime(time: string | undefined): string;
|
|
6
6
|
export function getLongTime(time: string): string;
|
|
7
|
+
export function localeKeyboardFormat(locale: string): {
|
|
8
|
+
format: (date: Date) => string;
|
|
9
|
+
parse: (formattedDate: string) => Date | null;
|
|
10
|
+
};
|
|
7
11
|
//# sourceMappingURL=dates.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dates.d.ts","sourceRoot":"","sources":["../../src/utils/dates.js"],"names":[],"mappings":"AAGO,sCAAoC,MAAM,UAGhD;AAMM,4CAAyC,IAAI,UAQnD;AAIM,uCAAoC,IAAI,YAE9C;AAIM,mCAA+B,CAAC,MAAM,EAAE,MAAM,CAAC,UAWrD;AAGM,mCAAgC,MAAM,GAAG,SAAS,UAGxD;AAEM,kCAA+B,MAAM,UAE3C"}
|
|
1
|
+
{"version":3,"file":"dates.d.ts","sourceRoot":"","sources":["../../src/utils/dates.js"],"names":[],"mappings":"AAGO,sCAAoC,MAAM,UAGhD;AAMM,4CAAyC,IAAI,UAQnD;AAIM,uCAAoC,IAAI,YAE9C;AAIM,mCAA+B,CAAC,MAAM,EAAE,MAAM,CAAC,UAWrD;AAGM,mCAAgC,MAAM,GAAG,SAAS,UAGxD;AAEM,kCAA+B,MAAM,UAE3C;AA2BM,6CAAwC,MAAM;mBAK9B,IAAI;2BACL,MAAM;EAkC3B"}
|