@koumoul/vjsf 3.0.0-beta.8 → 3.0.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/README.md +21 -0
- package/package.json +13 -20
- package/src/compat/v2.js +126 -27
- package/src/components/fragments/child-subtitle.vue +25 -0
- package/src/components/fragments/help-message.vue +33 -8
- package/src/components/fragments/section-header.vue +9 -7
- package/src/components/fragments/select-item-icon.vue +2 -2
- package/src/components/fragments/select-item.vue +2 -1
- package/src/components/fragments/select-selection.vue +2 -1
- package/src/components/fragments/selection-group.vue +105 -0
- package/src/components/fragments/text-field-menu.vue +16 -7
- package/src/components/node.vue +58 -41
- package/src/components/nodes/autocomplete.vue +14 -60
- package/src/components/nodes/card.vue +39 -0
- package/src/components/nodes/checkbox-group.vue +39 -0
- package/src/components/nodes/checkbox.vue +31 -26
- package/src/components/nodes/color-picker.vue +10 -5
- package/src/components/nodes/combobox.vue +17 -40
- package/src/components/nodes/date-picker.vue +30 -13
- package/src/components/nodes/date-time-picker.vue +83 -3
- package/src/components/nodes/expansion-panels.vue +17 -9
- package/src/components/nodes/file-input.vue +15 -11
- package/src/components/nodes/list.vue +246 -112
- package/src/components/nodes/number-combobox.vue +18 -39
- package/src/components/nodes/number-field.vue +17 -11
- package/src/components/nodes/one-of-select.vue +53 -27
- package/src/components/nodes/radio-group.vue +58 -0
- package/src/components/nodes/section.vue +4 -1
- package/src/components/nodes/select.vue +15 -54
- package/src/components/nodes/slider.vue +32 -29
- package/src/components/nodes/stepper.vue +10 -2
- package/src/components/nodes/switch-group.vue +39 -0
- package/src/components/nodes/switch.vue +31 -26
- package/src/components/nodes/tabs.vue +20 -8
- package/src/components/nodes/text-field.vue +10 -7
- package/src/components/nodes/textarea.vue +20 -12
- package/src/components/nodes/time-picker.vue +41 -1
- package/src/components/nodes/vertical-tabs.vue +16 -6
- package/src/components/tree.vue +1 -1
- package/src/components/vjsf.vue +11 -1
- package/src/composables/use-comp-defaults.js +19 -0
- package/src/composables/use-dnd.js +2 -1
- package/src/composables/use-get-items.js +53 -0
- package/src/composables/use-node.js +136 -0
- package/src/composables/use-select-node.js +67 -0
- package/src/composables/use-vjsf.js +74 -42
- package/src/index.js +5 -2
- package/src/options.js +67 -0
- package/src/types.ts +64 -33
- package/src/utils/arrays.js +37 -6
- package/types/compat/v2.d.ts.map +1 -1
- package/types/compile/index.d.ts +2 -2
- package/types/compile/index.d.ts.map +1 -1
- package/types/compile/options.d.ts +3 -2
- package/types/compile/options.d.ts.map +1 -1
- package/types/components/fragments/child-subtitle.vue.d.ts +8 -0
- package/types/components/fragments/child-subtitle.vue.d.ts.map +1 -0
- package/types/components/fragments/help-message.vue.d.ts +2 -2
- package/types/components/fragments/node-slot.vue.d.ts +2 -44
- package/types/components/fragments/node-slot.vue.d.ts.map +1 -1
- package/types/components/fragments/section-header.vue.d.ts +4 -2
- package/types/components/fragments/select-item-icon.vue.d.ts +2 -12
- package/types/components/fragments/select-item.vue.d.ts +2 -2
- package/types/components/fragments/select-selection.vue.d.ts +2 -2
- package/types/components/fragments/selection-group.vue.d.ts +5 -0
- package/types/components/fragments/selection-group.vue.d.ts.map +1 -0
- package/types/components/fragments/text-field-menu.vue.d.ts +2 -2
- package/types/components/fragments/text-field-menu.vue.d.ts.map +1 -1
- package/types/components/node.vue.d.ts +2 -2
- package/types/components/nodes/autocomplete.vue.d.ts +2 -24
- package/types/components/nodes/autocomplete.vue.d.ts.map +1 -1
- package/types/components/nodes/card.vue.d.ts +10 -0
- package/types/components/nodes/card.vue.d.ts.map +1 -0
- package/types/components/nodes/checkbox-group.vue.d.ts +5 -0
- package/types/components/nodes/checkbox-group.vue.d.ts.map +1 -0
- package/types/components/nodes/checkbox.vue.d.ts +3 -8
- package/types/components/nodes/color-picker.vue.d.ts +2 -2
- package/types/components/nodes/combobox.vue.d.ts +2 -24
- package/types/components/nodes/combobox.vue.d.ts.map +1 -1
- package/types/components/nodes/date-picker.vue.d.ts +2 -2
- package/types/components/nodes/date-time-picker.vue.d.ts +4 -4
- package/types/components/nodes/expansion-panels.vue.d.ts +2 -2
- package/types/components/nodes/file-input.vue.d.ts +2 -24
- package/types/components/nodes/file-input.vue.d.ts.map +1 -1
- package/types/components/nodes/list.vue.d.ts +2 -2
- package/types/components/nodes/number-combobox.vue.d.ts +2 -24
- package/types/components/nodes/number-combobox.vue.d.ts.map +1 -1
- package/types/components/nodes/number-field.vue.d.ts +2 -24
- package/types/components/nodes/number-field.vue.d.ts.map +1 -1
- package/types/components/nodes/one-of-select.vue.d.ts +2 -2
- package/types/components/nodes/radio-group.vue.d.ts +5 -0
- package/types/components/nodes/radio-group.vue.d.ts.map +1 -0
- package/types/components/nodes/section.vue.d.ts +2 -2
- package/types/components/nodes/select.vue.d.ts +2 -24
- package/types/components/nodes/select.vue.d.ts.map +1 -1
- package/types/components/nodes/slider.vue.d.ts +3 -8
- package/types/components/nodes/stepper.vue.d.ts +2 -2
- package/types/components/nodes/switch-group.vue.d.ts +5 -0
- package/types/components/nodes/switch-group.vue.d.ts.map +1 -0
- package/types/components/nodes/switch.vue.d.ts +3 -8
- package/types/components/nodes/tabs.vue.d.ts +2 -2
- package/types/components/nodes/text-field.vue.d.ts +2 -24
- package/types/components/nodes/text-field.vue.d.ts.map +1 -1
- package/types/components/nodes/textarea.vue.d.ts +2 -24
- package/types/components/nodes/textarea.vue.d.ts.map +1 -1
- package/types/components/nodes/time-picker.vue.d.ts +8 -1
- package/types/components/nodes/vertical-tabs.vue.d.ts +2 -2
- package/types/components/options.d.ts +1 -1
- package/types/components/options.d.ts.map +1 -1
- package/types/components/tree.vue.d.ts +2 -2
- package/types/components/vjsf.vue.d.ts +4 -4
- package/types/composables/use-comp-defaults.d.ts +8 -0
- package/types/composables/use-comp-defaults.d.ts.map +1 -0
- package/types/composables/use-dnd.d.ts +3 -3
- package/types/composables/use-dnd.d.ts.map +1 -1
- package/types/composables/use-field-props.d.ts +30 -0
- package/types/composables/use-field-props.d.ts.map +1 -0
- package/types/composables/use-field.d.ts +31 -0
- package/types/composables/use-field.d.ts.map +1 -0
- package/types/composables/use-get-items.d.ts +12 -0
- package/types/composables/use-get-items.d.ts.map +1 -0
- package/types/composables/use-node.d.ts +32 -0
- package/types/composables/use-node.d.ts.map +1 -0
- package/types/composables/use-select-field.d.ts +21 -0
- package/types/composables/use-select-field.d.ts.map +1 -0
- package/types/composables/use-select-node.d.ts +27 -0
- package/types/composables/use-select-node.d.ts.map +1 -0
- package/types/composables/use-select-props.d.ts +21 -0
- package/types/composables/use-select-props.d.ts.map +1 -0
- package/types/composables/use-select.d.ts +21 -0
- package/types/composables/use-select.d.ts.map +1 -0
- package/types/composables/use-vjsf.d.ts +2 -2
- package/types/composables/use-vjsf.d.ts.map +1 -1
- package/types/iconsets/default-aliases.d.ts +10 -0
- package/types/iconsets/default-aliases.d.ts.map +1 -0
- package/types/iconsets/mdi-svg.d.ts +3 -0
- package/types/iconsets/mdi-svg.d.ts.map +1 -0
- package/types/iconsets/mdi.d.ts +3 -0
- package/types/iconsets/mdi.d.ts.map +1 -0
- package/types/index.d.ts +5 -2
- package/types/index.d.ts.map +1 -1
- package/types/options.d.ts +9 -0
- package/types/options.d.ts.map +1 -0
- package/types/types.d.ts +65 -33
- package/types/types.d.ts.map +1 -1
- package/types/utils/arrays.d.ts +17 -4
- package/types/utils/arrays.d.ts.map +1 -1
- package/types/utils/index.d.ts +0 -3
- package/types/utils/props.d.ts +7 -0
- package/types/utils/props.d.ts.map +1 -1
- package/types/utils/slots.d.ts +8 -0
- package/types/utils/slots.d.ts.map +1 -1
- package/src/compile/index.js +0 -65
- package/src/compile/options.js +0 -19
- package/src/compile/v-jsf-compiled.vue.ejs +0 -61
- package/src/components/options.js +0 -27
- package/src/utils/global-register.js +0 -13
- package/src/utils/index.js +0 -5
- package/src/utils/props.js +0 -107
- package/src/utils/slots.js +0 -18
- package/types/utils/global-register.d.ts +0 -8
- package/types/utils/global-register.d.ts.map +0 -1
|
@@ -1,7 +1,47 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import TextFieldMenu from '../fragments/text-field-menu.vue'
|
|
3
|
+
import { VTimePicker } from 'vuetify/labs/VTimePicker'
|
|
4
|
+
import { useDate, useDefaults } from 'vuetify'
|
|
5
|
+
import { computed, toRef } from 'vue'
|
|
6
|
+
import { getShortTime, getLongTime } from '../../utils/dates.js'
|
|
7
|
+
import useNode from '../../composables/use-node.js'
|
|
2
8
|
|
|
9
|
+
useDefaults({}, 'VjsfDatePicker')
|
|
10
|
+
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
modelValue: {
|
|
13
|
+
/** @type import('vue').PropType<import('../../types.js').VjsfDatePickerNode> */
|
|
14
|
+
type: Object,
|
|
15
|
+
required: true
|
|
16
|
+
},
|
|
17
|
+
statefulLayout: {
|
|
18
|
+
/** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
|
|
19
|
+
type: Object,
|
|
20
|
+
required: true
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const vDate = useDate()
|
|
25
|
+
|
|
26
|
+
const { compProps, localData } = useNode(toRef(props, 'modelValue'), props.statefulLayout)
|
|
27
|
+
|
|
28
|
+
const timePickerProps = computed(() => {
|
|
29
|
+
const timePickerProps = { ...compProps.value }
|
|
30
|
+
timePickerProps['ampm-in-title'] = true
|
|
31
|
+
if (localData.value) timePickerProps.modelValue = getShortTime(localData.value)
|
|
32
|
+
return timePickerProps
|
|
33
|
+
})
|
|
3
34
|
</script>
|
|
4
35
|
|
|
5
36
|
<template>
|
|
6
|
-
|
|
37
|
+
<text-field-menu
|
|
38
|
+
:model-value="props.modelValue"
|
|
39
|
+
:stateful-layout="statefulLayout"
|
|
40
|
+
:formatted-value="timePickerProps.modelValue && vDate.format('2010-04-13T' + timePickerProps.modelValue, 'fullTime')"
|
|
41
|
+
>
|
|
42
|
+
<v-time-picker
|
|
43
|
+
v-bind="timePickerProps"
|
|
44
|
+
@update:model-value="value => {statefulLayout.input(props.modelValue, value && getLongTime(value))}"
|
|
45
|
+
/>
|
|
46
|
+
</text-field-menu>
|
|
7
47
|
</template>
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { isSection } from '@json-layout/core'
|
|
3
|
-
import { VTabs, VTab
|
|
3
|
+
import { VTabs, VTab } from 'vuetify/components/VTabs'
|
|
4
|
+
import { VContainer, VRow } from 'vuetify/components/VGrid'
|
|
5
|
+
import { VIcon } from 'vuetify/components/VIcon'
|
|
6
|
+
import { VSheet } from 'vuetify/components/VSheet'
|
|
7
|
+
import { VWindow, VWindowItem } from 'vuetify/components/VWindow'
|
|
4
8
|
import { ref } from 'vue'
|
|
5
9
|
import Node from '../node.vue'
|
|
6
10
|
import SectionHeader from '../fragments/section-header.vue'
|
|
11
|
+
import ChildSubtitle from '../fragments/child-subtitle.vue'
|
|
12
|
+
import { useDefaults } from 'vuetify'
|
|
13
|
+
import useCompDefaults from '../../composables/use-comp-defaults.js'
|
|
14
|
+
|
|
15
|
+
useDefaults({}, 'VjsfVerticalTabs')
|
|
16
|
+
const vSheetProps = useCompDefaults('VjsfVerticalTabs-VSheet', { border: true })
|
|
7
17
|
|
|
8
18
|
defineProps({
|
|
9
19
|
modelValue: {
|
|
@@ -23,7 +33,7 @@ const tab = ref(0)
|
|
|
23
33
|
|
|
24
34
|
<template>
|
|
25
35
|
<section-header :node="modelValue" />
|
|
26
|
-
<v-sheet
|
|
36
|
+
<v-sheet v-bind="vSheetProps">
|
|
27
37
|
<div class="d-flex flex-row">
|
|
28
38
|
<v-tabs
|
|
29
39
|
v-model="tab"
|
|
@@ -38,9 +48,8 @@ const tab = ref(0)
|
|
|
38
48
|
<v-icon
|
|
39
49
|
v-if="child.validated && (child.error || child.childError)"
|
|
40
50
|
color="error"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
</v-icon>
|
|
51
|
+
:icon="statefulLayout.options.icons.alert"
|
|
52
|
+
/>
|
|
44
53
|
{{ child.layout.title ?? child.layout.label }}
|
|
45
54
|
</v-tab>
|
|
46
55
|
</v-tabs>
|
|
@@ -54,7 +63,8 @@ const tab = ref(0)
|
|
|
54
63
|
:value="i"
|
|
55
64
|
>
|
|
56
65
|
<v-container fluid>
|
|
57
|
-
<
|
|
66
|
+
<child-subtitle :model-value="child" />
|
|
67
|
+
<v-row :dense="modelValue.options?.density === 'compact' || modelValue.options?.density === 'comfortable'">
|
|
58
68
|
<node
|
|
59
69
|
v-for="grandChild of isSection(child) ? child.children : [child]"
|
|
60
70
|
:key="grandChild.fullKey"
|
package/src/components/tree.vue
CHANGED
package/src/components/vjsf.vue
CHANGED
|
@@ -14,10 +14,14 @@ import NodeSwitch from './nodes/switch.vue'
|
|
|
14
14
|
import NodeNumberField from './nodes/number-field.vue'
|
|
15
15
|
import NodeSlider from './nodes/slider.vue'
|
|
16
16
|
import NodeDatePicker from './nodes/date-picker.vue'
|
|
17
|
+
import NodeTimePicker from './nodes/time-picker.vue'
|
|
17
18
|
import NodeDateTimePicker from './nodes/date-time-picker.vue'
|
|
18
19
|
import NodeColorPicker from './nodes/color-picker.vue'
|
|
19
20
|
import NodeSelect from './nodes/select.vue'
|
|
20
21
|
import NodeAutocomplete from './nodes/autocomplete.vue'
|
|
22
|
+
import NodeRadioGroup from './nodes/radio-group.vue'
|
|
23
|
+
import NodeCheckboxGroup from './nodes/checkbox-group.vue'
|
|
24
|
+
import NodeSwitchGroup from './nodes/switch-group.vue'
|
|
21
25
|
import NodeOneOfSelect from './nodes/one-of-select.vue'
|
|
22
26
|
import NodeTabs from './nodes/tabs.vue'
|
|
23
27
|
import NodeVerticalTabs from './nodes/vertical-tabs.vue'
|
|
@@ -27,6 +31,7 @@ import NodeExpansionPanels from './nodes/expansion-panels.vue'
|
|
|
27
31
|
import NodeStepper from './nodes/stepper.vue'
|
|
28
32
|
import NodeList from './nodes/list.vue'
|
|
29
33
|
import NodeFileInput from './nodes/file-input.vue'
|
|
34
|
+
import NodeCard from './nodes/card.vue'
|
|
30
35
|
|
|
31
36
|
/** @type {Record<string, import('vue').Component>} */
|
|
32
37
|
const nodeComponents = {
|
|
@@ -38,10 +43,14 @@ const nodeComponents = {
|
|
|
38
43
|
'number-field': NodeNumberField,
|
|
39
44
|
slider: NodeSlider,
|
|
40
45
|
'date-picker': NodeDatePicker,
|
|
46
|
+
'time-picker': NodeTimePicker,
|
|
41
47
|
'date-time-picker': NodeDateTimePicker,
|
|
42
48
|
'color-picker': NodeColorPicker,
|
|
43
49
|
select: NodeSelect,
|
|
44
50
|
autocomplete: NodeAutocomplete,
|
|
51
|
+
'radio-group': NodeRadioGroup,
|
|
52
|
+
'checkbox-group': NodeCheckboxGroup,
|
|
53
|
+
'switch-group': NodeSwitchGroup,
|
|
45
54
|
'one-of-select': NodeOneOfSelect,
|
|
46
55
|
tabs: NodeTabs,
|
|
47
56
|
'vertical-tabs': NodeVerticalTabs,
|
|
@@ -50,7 +59,8 @@ const nodeComponents = {
|
|
|
50
59
|
list: NodeList,
|
|
51
60
|
combobox: NodeCombobox,
|
|
52
61
|
'number-combobox': NodeNumberCombobox,
|
|
53
|
-
'file-input': NodeFileInput
|
|
62
|
+
'file-input': NodeFileInput,
|
|
63
|
+
card: NodeCard
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
const props = defineProps({
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { inject, computed } from 'vue'
|
|
2
|
+
|
|
3
|
+
// inspired by https://github.com/vuetifyjs/vuetify/blob/27b4b1e52060b6bee13a290a4829f935f1bd9c05/packages/vuetify/src/composables/defaults.ts#L92
|
|
4
|
+
/**
|
|
5
|
+
*
|
|
6
|
+
* @param {string} name
|
|
7
|
+
* @param {Record<string, any> | null} [localDefaults]
|
|
8
|
+
* @returns {import('vue').ComputedRef<Record<string, any>>}
|
|
9
|
+
*/
|
|
10
|
+
export default function useCompDefaultProps (name, localDefaults = null) {
|
|
11
|
+
/** @type {import('vue').Ref<import('vuetify').DefaultsInstance> | undefined} */
|
|
12
|
+
const defaults = inject(Symbol.for('vuetify:defaults'))
|
|
13
|
+
if (!defaults) throw new Error('[vjsf] Could not find defaults instance')
|
|
14
|
+
return computed(() => {
|
|
15
|
+
const componentDefaults = defaults.value?.[name] ?? {}
|
|
16
|
+
if (!localDefaults) return componentDefaults
|
|
17
|
+
return { ...componentDefaults, ...localDefaults }
|
|
18
|
+
})
|
|
19
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { shallowRef, ref, computed } from 'vue'
|
|
2
|
-
import { moveArrayItem } from '../utils/
|
|
2
|
+
import { moveArrayItem } from '../utils/arrays.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @template T
|
|
@@ -42,6 +42,7 @@ export default function useDnd (array, callback) {
|
|
|
42
42
|
dragging.value = itemIndex
|
|
43
43
|
},
|
|
44
44
|
onDragend: () => {
|
|
45
|
+
hovered.value = itemIndex
|
|
45
46
|
dragging.value = -1
|
|
46
47
|
callback()
|
|
47
48
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { watch, shallowRef, ref, computed } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {import('vue').Ref<import('../types.js').VjsfNode>} nodeRef
|
|
5
|
+
* @param {import('../types.js').VjsfStatefulLayout} statefulLayout
|
|
6
|
+
*/
|
|
7
|
+
export default function (nodeRef, statefulLayout) {
|
|
8
|
+
/** @type import('vue').Ref<import('@json-layout/vocabulary').SelectItems> */
|
|
9
|
+
const items = shallowRef([])
|
|
10
|
+
/** @type import('vue').Ref<boolean> */
|
|
11
|
+
const loading = ref(false)
|
|
12
|
+
/** @type import('vue').Ref<string> */
|
|
13
|
+
const search = ref('')
|
|
14
|
+
|
|
15
|
+
const hasItems = computed(() => {
|
|
16
|
+
return !!(nodeRef.value.layout.items || nodeRef.value.layout.getItems)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const fetchItems = async () => {
|
|
20
|
+
loading.value = true
|
|
21
|
+
items.value = await statefulLayout.getItems(nodeRef.value, search.value)
|
|
22
|
+
loading.value = false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
watch(() => nodeRef.value.itemsCacheKey, (newValue, oldValue) => {
|
|
26
|
+
if (newValue === oldValue) return
|
|
27
|
+
fetchItems()
|
|
28
|
+
}, { immediate: true })
|
|
29
|
+
|
|
30
|
+
watch(search, () => {
|
|
31
|
+
fetchItems()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {any} selectedItem
|
|
36
|
+
* @param {any} itemValue
|
|
37
|
+
*/
|
|
38
|
+
const prepareSelectedItem = (selectedItem, itemValue) => {
|
|
39
|
+
let item = selectedItem
|
|
40
|
+
// item and value are the same when the selection is not found in items list
|
|
41
|
+
if (selectedItem === itemValue) {
|
|
42
|
+
try {
|
|
43
|
+
item = statefulLayout.prepareSelectItem(nodeRef.value, selectedItem)
|
|
44
|
+
if (item.value === undefined) item.value = itemValue
|
|
45
|
+
} catch (e) {
|
|
46
|
+
item = { value: itemValue }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return item
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { hasItems, items, loading, search, prepareSelectedItem }
|
|
53
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import NodeSlot from '../components/fragments/node-slot.vue'
|
|
2
|
+
import { computed, camelize, watch, ref, h } from 'vue'
|
|
3
|
+
|
|
4
|
+
// NOTE: in a previous draft we used to have this in options,
|
|
5
|
+
// but it was not very flexible and not very easy to use, user defined props should be managed
|
|
6
|
+
// by a combination of layout.props, layout.getProps and vuetify defaults provider (https://vuetifyjs.com/en/components/defaults-providers/#usage)
|
|
7
|
+
const defaultProps = {
|
|
8
|
+
fieldPropsCompact: {
|
|
9
|
+
hideDetails: 'auto'
|
|
10
|
+
},
|
|
11
|
+
fieldPropsReadOnly: { hideDetails: 'auto', variant: 'plain' },
|
|
12
|
+
fieldPropsSummary: { hideDetails: true }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {(Record<string, any> | undefined)[]} propsLevels
|
|
17
|
+
* @returns {Record<string, any> & {class: string[]}}
|
|
18
|
+
*/
|
|
19
|
+
export function mergePropsLevels (propsLevels) {
|
|
20
|
+
/** @type {Record<string, any> & {class: string[]}} */
|
|
21
|
+
const fullProps = { class: [] }
|
|
22
|
+
for (const propsLevel of propsLevels) {
|
|
23
|
+
if (propsLevel) {
|
|
24
|
+
for (const key of Object.keys(propsLevel)) {
|
|
25
|
+
if (key === 'class') {
|
|
26
|
+
// a small convention for merging/overwriting classes:
|
|
27
|
+
// a class defined as a simple string overwrites the previous ones
|
|
28
|
+
// a class defined as an array is merged with the previous ones
|
|
29
|
+
if (Array.isArray(propsLevel.class)) fullProps.class = fullProps.class.concat(propsLevel.class)
|
|
30
|
+
else fullProps.class = [propsLevel.class]
|
|
31
|
+
} else {
|
|
32
|
+
fullProps[camelize(key)] = propsLevel[key]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return fullProps
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {import('vue').Ref<import('../types.js').VjsfNode>} nodeRef
|
|
42
|
+
* @param {import('../types.js').VjsfStatefulLayout} statefulLayout
|
|
43
|
+
* @param {{isMainComp?: boolean, bindData?: boolean, layoutPropsMap?: (string | [string, string])[]}} [opts]
|
|
44
|
+
*/
|
|
45
|
+
export default function (nodeRef, statefulLayout, opts = {}) {
|
|
46
|
+
if (opts.bindData === undefined) opts.bindData = true
|
|
47
|
+
if (opts.isMainComp === undefined) opts.isMainComp = true
|
|
48
|
+
|
|
49
|
+
// we access vjsfNode properties through computeds so that the parts without mutations do not trigger reactivity
|
|
50
|
+
// this is to leverage the immutability provided by immer in json-layout
|
|
51
|
+
const options = computed(() => nodeRef.value.options)
|
|
52
|
+
const skeleton = computed(() => nodeRef.value.skeleton)
|
|
53
|
+
const layout = computed(() => nodeRef.value.layout)
|
|
54
|
+
const data = computed(() => nodeRef.value.data)
|
|
55
|
+
const error = computed(() => nodeRef.value.error)
|
|
56
|
+
const validated = computed(() => nodeRef.value.validated)
|
|
57
|
+
const nodeProps = computed(() => nodeRef.value.props)
|
|
58
|
+
const autofocus = computed(() => nodeRef.value.autofocus)
|
|
59
|
+
const children = computed(() => nodeRef.value.children)
|
|
60
|
+
|
|
61
|
+
const preparedData = computed(() => {
|
|
62
|
+
return (typeof data.value === 'string' && layout.value.separator) ? data.value.split(/** @type {string} */(layout.value.separator)) : data.value
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// modelValue is not a straight computed, but is separated in a ref because the change in json-layout can be delayed
|
|
66
|
+
// depending on the debounceInputMs option
|
|
67
|
+
const localData = ref()
|
|
68
|
+
watch(preparedData, (data) => { localData.value = data }, { immediate: true })
|
|
69
|
+
|
|
70
|
+
// calculate the props of a field/input type component (text fields, etc)
|
|
71
|
+
// isMainComp is used to determine if this input component is also the main rendered component or if is mostly a wrapper (date picker, etc.)
|
|
72
|
+
const inputProps = computed(() => {
|
|
73
|
+
/** @type {(Record<string, any> | undefined)[]} */
|
|
74
|
+
const propsLevels = []
|
|
75
|
+
if (options.value.density === 'compact') propsLevels.push(defaultProps.fieldPropsCompact)
|
|
76
|
+
if (options.value.readOnly) propsLevels.push(defaultProps.fieldPropsReadOnly)
|
|
77
|
+
if (opts.isMainComp && nodeProps.value) propsLevels.push(nodeProps.value)
|
|
78
|
+
|
|
79
|
+
const fullProps = mergePropsLevels(propsLevels)
|
|
80
|
+
|
|
81
|
+
fullProps.label = layout.value.label
|
|
82
|
+
if (error.value && validated.value) {
|
|
83
|
+
fullProps.errorMessages = error.value
|
|
84
|
+
}
|
|
85
|
+
if (options.value.readOnly) {
|
|
86
|
+
fullProps.disabled = true
|
|
87
|
+
fullProps.class.push('vjsf-input--readonly')
|
|
88
|
+
}
|
|
89
|
+
if (autofocus.value) {
|
|
90
|
+
fullProps.class.push('vjsf-input--autofocus')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (opts.layoutPropsMap) {
|
|
94
|
+
for (const propMap of opts.layoutPropsMap) {
|
|
95
|
+
if (typeof propMap === 'string') {
|
|
96
|
+
if (propMap in layout.value) fullProps[propMap] = layout.value[propMap]
|
|
97
|
+
} else {
|
|
98
|
+
if (propMap[1] in layout.value) fullProps[propMap[0]] = layout.value[propMap[1]]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (opts.bindData) {
|
|
104
|
+
fullProps['onUpdate:modelValue'] = (/** @type string */value) => {
|
|
105
|
+
const newData = (Array.isArray(value) && layout.value.separator) ? value.join(/** @type {string} */(layout.value.separator)) : value
|
|
106
|
+
localData.value = newData
|
|
107
|
+
return statefulLayout.input(nodeRef.value, newData)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
fullProps.onBlur = () => statefulLayout.blur(nodeRef.value)
|
|
111
|
+
|
|
112
|
+
return fullProps
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// calculate the props of components that are not of the field category
|
|
116
|
+
const compProps = computed(() => {
|
|
117
|
+
/** @type {(Record<string, any> | undefined)[]} */
|
|
118
|
+
const propsLevels = [{ density: options.value.density }]
|
|
119
|
+
if (opts.isMainComp) propsLevels.push(layout.value.props)
|
|
120
|
+
const fullProps = mergePropsLevels(propsLevels)
|
|
121
|
+
return fullProps
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// calculate the slots of components
|
|
125
|
+
const compSlots = computed(() => {
|
|
126
|
+
if (!layout.value.slots) return {}
|
|
127
|
+
/** @type {Record<string, any>} */
|
|
128
|
+
const slots = {}
|
|
129
|
+
for (const [key, layoutSlot] of Object.entries(layout.value.slots)) {
|
|
130
|
+
slots[key] = () => h(NodeSlot, { layoutSlot, node: nodeRef.value, statefulLayout })
|
|
131
|
+
}
|
|
132
|
+
return slots
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
return { localData, inputProps, compProps, compSlots, options, skeleton, layout, data, children }
|
|
136
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { computed, h } from 'vue'
|
|
2
|
+
import useField from './use-node.js'
|
|
3
|
+
import useGetItems from './use-get-items.js'
|
|
4
|
+
import SelectItem from '../components/fragments/select-item.vue'
|
|
5
|
+
import SelectSelection from '../components/fragments/select-selection.vue'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* specialized use of useFieldProps shared between select and autocomplete components
|
|
9
|
+
* @param {import('vue').Ref<import('../types.js').VjsfSelectNode>} nodeRef
|
|
10
|
+
* @param {import('../types.js').VjsfStatefulLayout} statefulLayout
|
|
11
|
+
*/
|
|
12
|
+
export default function (nodeRef, statefulLayout) {
|
|
13
|
+
const layout = computed(() => nodeRef.value.layout)
|
|
14
|
+
|
|
15
|
+
const { inputProps, options, skeleton, localData, compSlots } = useField(nodeRef, statefulLayout, { layoutPropsMap: ['multiple'], bindData: false })
|
|
16
|
+
const getItems = useGetItems(nodeRef, statefulLayout)
|
|
17
|
+
|
|
18
|
+
const selectProps = computed(() => {
|
|
19
|
+
const props = { ...inputProps.value }
|
|
20
|
+
|
|
21
|
+
if (options.value.readOnly) props.menuProps = { modelValue: false }
|
|
22
|
+
props.clearable = props.clearable ?? !skeleton.value.required
|
|
23
|
+
props.valueComparator = (/** @type {any} */a, /** @type {any} */b) => {
|
|
24
|
+
const aKey = typeof a === 'object' ? statefulLayout.prepareSelectItem(nodeRef.value, a).key : a
|
|
25
|
+
const bKey = typeof b === 'object' ? statefulLayout.prepareSelectItem(nodeRef.value, b).key : b
|
|
26
|
+
return aKey === bKey
|
|
27
|
+
}
|
|
28
|
+
props['onUpdate:modelValue'] = (/** @type string */value) => {
|
|
29
|
+
// fix some weird case where vuetify only keep the title property of an unknown item
|
|
30
|
+
if (Array.isArray(value) && Array.isArray(nodeRef.value.data)) {
|
|
31
|
+
for (let i = 0; i < nodeRef.value.data.length; i++) {
|
|
32
|
+
if (typeof nodeRef.value.data[i] === 'object' && typeof value[i] === 'string') {
|
|
33
|
+
value[i] = nodeRef.value.data[i]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const newData = (Array.isArray(value) && nodeRef.value.layout.separator) ? value.join(/** @type {string} */(nodeRef.value.layout.separator)) : value
|
|
39
|
+
localData.value = newData
|
|
40
|
+
return statefulLayout.input(nodeRef.value, newData)
|
|
41
|
+
}
|
|
42
|
+
props.onBlur = () => statefulLayout.blur(nodeRef.value)
|
|
43
|
+
return props
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// shared between select and autocomplete components
|
|
47
|
+
const selectSlots = computed(() => {
|
|
48
|
+
const slots = { ...compSlots.value }
|
|
49
|
+
if (!slots.item) {
|
|
50
|
+
slots.item = (/** @type {any} */ context) => h(SelectItem, {
|
|
51
|
+
multiple: layout.value.multiple,
|
|
52
|
+
itemProps: context.props,
|
|
53
|
+
item: context.item.raw
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
if (!slots.selection) {
|
|
57
|
+
slots.selection = (/** @type {any} */ context) => h(SelectSelection, {
|
|
58
|
+
multiple: layout.value.multiple,
|
|
59
|
+
last: layout.value.multiple && context.index === nodeRef.value.data.length - 1,
|
|
60
|
+
item: getItems.prepareSelectedItem(context.item.raw, context.item.value)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
return slots
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return { localData, inputProps, selectProps, compSlots, selectSlots, getItems }
|
|
67
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { useDefaults } from 'vuetify'
|
|
1
2
|
import { StatefulLayout, produceCompileOptions } from '@json-layout/core'
|
|
2
|
-
import { inject, toRaw, shallowRef, computed, ref, watch, useSlots } from 'vue'
|
|
3
|
+
import { inject, toRaw, shallowRef, computed, ref, watch, useSlots, getCurrentInstance } from 'vue'
|
|
3
4
|
import { useElementSize } from '@vueuse/core'
|
|
4
|
-
import { getFullOptions } from '../
|
|
5
|
-
import { registeredNodeComponents } from '../utils/index.js'
|
|
5
|
+
import { getFullOptions } from '../options.js'
|
|
6
6
|
import { setAutoFreeze } from 'immer'
|
|
7
|
+
import Debug from 'debug'
|
|
8
|
+
|
|
9
|
+
const debug = Debug('vjsf:use-vjsf')
|
|
7
10
|
|
|
8
11
|
// immer freezing is disabled because it is not compatible with Vue 3 reactivity
|
|
9
12
|
setAutoFreeze(false)
|
|
@@ -30,6 +33,13 @@ export const emits = {
|
|
|
30
33
|
*/
|
|
31
34
|
export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compile, precompiledLayout) => {
|
|
32
35
|
const el = ref(null)
|
|
36
|
+
|
|
37
|
+
useDefaults({}, 'Vjsf')
|
|
38
|
+
/** @type {import('vuetify').DefaultsInstance} */
|
|
39
|
+
const defaults = (inject(Symbol.for('vuetify:defaults')))?.value
|
|
40
|
+
debug('provided defaults', defaults)
|
|
41
|
+
|
|
42
|
+
// TODO: apply a debounce to width ?
|
|
33
43
|
const { width } = useElementSize(el)
|
|
34
44
|
|
|
35
45
|
/** @type import('vue').ShallowRef<import('../types.js').VjsfStatefulLayout | null> */
|
|
@@ -38,22 +48,60 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
|
|
|
38
48
|
const stateTree = shallowRef(null)
|
|
39
49
|
|
|
40
50
|
// cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/composables/form.ts
|
|
41
|
-
|
|
51
|
+
/** @type {any | null} */
|
|
52
|
+
const form = inject(Symbol.for('vuetify:form'), null)
|
|
42
53
|
if (form) {
|
|
43
54
|
form.register({
|
|
44
|
-
id:
|
|
55
|
+
id: `vjsf-${Math.random().toString(36).substring(2, 9)}`,
|
|
45
56
|
validate: () => {
|
|
46
57
|
statefulLayout.value?.validate()
|
|
47
|
-
return statefulLayout.value?.errors
|
|
58
|
+
return statefulLayout.value?.errors || []
|
|
48
59
|
},
|
|
49
60
|
reset: () => statefulLayout.value?.resetValidation(), // TODO: also empty the data ?
|
|
50
|
-
resetValidation: () => statefulLayout.value?.resetValidation()
|
|
61
|
+
resetValidation: () => statefulLayout.value?.resetValidation(),
|
|
62
|
+
vm: getCurrentInstance()
|
|
51
63
|
})
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
const slots = useSlots()
|
|
55
67
|
|
|
56
|
-
|
|
68
|
+
/* Callbacks from json layout stateful layout */
|
|
69
|
+
/**
|
|
70
|
+
* @param {import('@json-layout/core').StatefulLayout} statefulLayout
|
|
71
|
+
*/
|
|
72
|
+
const onStatefulLayoutUpdate = (statefulLayout) => {
|
|
73
|
+
debug('onStatefulLayoutUpdate', statefulLayout)
|
|
74
|
+
if (!statefulLayout) return
|
|
75
|
+
stateTree.value = statefulLayout.stateTree
|
|
76
|
+
debug(' -> emit update:state')
|
|
77
|
+
emit('update:state', statefulLayout)
|
|
78
|
+
if (form) {
|
|
79
|
+
// cf https://vuetifyjs.com/en/components/forms/#validation-state
|
|
80
|
+
if (statefulLayout.valid) form.update('vjsf', true, [])
|
|
81
|
+
else if (statefulLayout.hasHiddenError) form.update('vjsf', null, [])
|
|
82
|
+
else form.update('vjsf', false, [])
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* @param {any} data
|
|
87
|
+
*/
|
|
88
|
+
const onDataUpdate = (data) => {
|
|
89
|
+
debug('onDataUpdate', data)
|
|
90
|
+
debug(' -> emit update:modelValue')
|
|
91
|
+
emit('update:modelValue', data)
|
|
92
|
+
}
|
|
93
|
+
const onAutofocus = () => {
|
|
94
|
+
if (!el.value) return
|
|
95
|
+
// @ts-ignore
|
|
96
|
+
const autofocusNodeElement = el.value.querySelector('.vjsf-input--autofocus')
|
|
97
|
+
debug('onAutofocus', autofocusNodeElement)
|
|
98
|
+
if (autofocusNodeElement) {
|
|
99
|
+
const autofocusInputElement = autofocusNodeElement.querySelector('input') ?? autofocusNodeElement.querySelector('textarea:not([style*="display: none"]')
|
|
100
|
+
if (autofocusInputElement) autofocusInputElement.focus()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fullOptions = computed(() => getFullOptions(options.value, form, width.value, defaults?.global, slots, { ...nodeComponents }, onDataUpdate, onStatefulLayoutUpdate, onAutofocus))
|
|
57
105
|
|
|
58
106
|
// do not use a simple computed here as we want to prevent recompiling the layout when the options are the same
|
|
59
107
|
/** @type {import('vue').Ref<import('@json-layout/core').PartialCompileOptions>} */
|
|
@@ -61,7 +109,10 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
|
|
|
61
109
|
watch(fullOptions, (newOptions) => {
|
|
62
110
|
if (precompiledLayout?.value) return
|
|
63
111
|
const newCompileOptions = produceCompileOptions(compileOptions.value, newOptions)
|
|
64
|
-
if (newCompileOptions !== compileOptions.value)
|
|
112
|
+
if (newCompileOptions !== compileOptions.value) {
|
|
113
|
+
debug('new compileOptions', newCompileOptions)
|
|
114
|
+
compileOptions.value = newCompileOptions
|
|
115
|
+
}
|
|
65
116
|
}, { immediate: true })
|
|
66
117
|
|
|
67
118
|
const compiledLayout = computed(() => {
|
|
@@ -71,62 +122,43 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
|
|
|
71
122
|
return compiledLayout
|
|
72
123
|
})
|
|
73
124
|
|
|
74
|
-
const onStatefulLayoutUpdate = () => {
|
|
75
|
-
if (!statefulLayout.value) return
|
|
76
|
-
stateTree.value = statefulLayout.value.stateTree
|
|
77
|
-
emit('update:modelValue', statefulLayout.value.data)
|
|
78
|
-
emit('update:state', statefulLayout.value)
|
|
79
|
-
if (form) {
|
|
80
|
-
// cf https://vuetifyjs.com/en/components/forms/#validation-state
|
|
81
|
-
if (statefulLayout.value.valid) form.update('vjsf', true, [])
|
|
82
|
-
else if (statefulLayout.value.hasHiddenError) form.update('vjsf', null, [])
|
|
83
|
-
else form.update('vjsf', false, [])
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
125
|
const initStatefulLayout = () => {
|
|
88
126
|
if (!width.value) return
|
|
89
|
-
|
|
90
127
|
// @ts-ignore
|
|
91
|
-
|
|
128
|
+
statefulLayout.value = /** @type {import('../types.js').VjsfStatefulLayout} */(new StatefulLayout(
|
|
92
129
|
toRaw(compiledLayout.value),
|
|
93
|
-
toRaw(compiledLayout.value.
|
|
130
|
+
toRaw(compiledLayout.value.skeletonTrees[compiledLayout.value.mainTree]),
|
|
94
131
|
toRaw(fullOptions.value),
|
|
95
132
|
toRaw(modelValue.value)
|
|
96
133
|
))
|
|
97
|
-
statefulLayout.value = _statefulLayout
|
|
98
|
-
onStatefulLayoutUpdate()
|
|
99
|
-
_statefulLayout.events.on('update', () => {
|
|
100
|
-
onStatefulLayoutUpdate()
|
|
101
|
-
})
|
|
102
|
-
emit('update:state', _statefulLayout)
|
|
103
|
-
_statefulLayout.events.on('autofocus', () => {
|
|
104
|
-
if (!el.value) return
|
|
105
|
-
// @ts-ignore
|
|
106
|
-
const autofocusNodeElement = el.value.querySelector('.vjsf-input--autofocus')
|
|
107
|
-
if (autofocusNodeElement) {
|
|
108
|
-
const autofocusInputElement = autofocusNodeElement.querySelector('input') ?? autofocusNodeElement.querySelector('textarea:not([style*="display: none"]')
|
|
109
|
-
if (autofocusInputElement) autofocusInputElement.focus()
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
// case where options are updated from outside
|
|
115
137
|
watch(fullOptions, (newOptions) => {
|
|
138
|
+
debug('watch fullOptions', fullOptions)
|
|
116
139
|
if (statefulLayout.value) {
|
|
117
|
-
statefulLayout
|
|
140
|
+
debug(' -> update statefulLayout options')
|
|
141
|
+
statefulLayout.value.options = toRaw(newOptions)
|
|
118
142
|
} else {
|
|
143
|
+
debug(' -> init statefulLayout')
|
|
119
144
|
initStatefulLayout()
|
|
120
145
|
}
|
|
121
146
|
})
|
|
122
147
|
|
|
123
148
|
// case where data is updated from outside
|
|
124
149
|
watch(modelValue, (newData) => {
|
|
125
|
-
|
|
150
|
+
const rawData = toRaw(newData)
|
|
151
|
+
if (statefulLayout.value && statefulLayout.value.data !== rawData) {
|
|
152
|
+
debug('modelValue changed from outside', rawData)
|
|
153
|
+
debug(' -> update statefulLayout data')
|
|
154
|
+
statefulLayout.value.data = toRaw(rawData)
|
|
155
|
+
}
|
|
126
156
|
})
|
|
127
157
|
|
|
128
158
|
// case where schema or compile options are updated from outside
|
|
129
159
|
watch(compiledLayout, (newCompiledLayout) => {
|
|
160
|
+
debug('watch compiledLayout', newCompiledLayout)
|
|
161
|
+
debug(' -> init statefulLayout')
|
|
130
162
|
initStatefulLayout()
|
|
131
163
|
})
|
|
132
164
|
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import Vjsf from './components/vjsf.vue'
|
|
2
|
-
import { defaultOptions } from './
|
|
3
|
-
export { Vjsf, defaultOptions }
|
|
2
|
+
import { defaultOptions, defaultIcons } from './options.js'
|
|
3
|
+
export { Vjsf, defaultOptions, defaultIcons }
|
|
4
4
|
export default Vjsf
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('./types.js').PartialVjsfOptions} Options */
|
|
7
|
+
/** @typedef {import('./types.js').PartialVjsfCompileOptions} CompileOptions */
|