@koumoul/vjsf 3.0.0-alpha.0 → 3.0.0-alpha.2
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 +10 -4
- package/src/compat/v2.js +44 -6
- package/src/compile/index.js +25 -6
- package/src/compile/v-jsf-compiled.vue.ejs +14 -52
- package/src/components/fragments/help-message.vue +48 -0
- package/src/components/fragments/section-header.vue +4 -1
- package/src/components/fragments/select-item-icon.vue +28 -0
- package/src/components/fragments/select-item.vue +43 -0
- package/src/components/fragments/select-selection.vue +35 -0
- package/src/components/fragments/text-field-menu.vue +1 -1
- package/src/components/node.vue +10 -1
- package/src/components/nodes/autocomplete.vue +95 -0
- package/src/components/nodes/combobox.vue +73 -0
- package/src/components/nodes/date-picker.vue +2 -2
- package/src/components/nodes/list.vue +170 -86
- package/src/components/nodes/markdown.vue +233 -0
- package/src/components/nodes/number-combobox.vue +73 -0
- package/src/components/nodes/number-field.vue +1 -2
- package/src/components/nodes/select.vue +70 -50
- package/src/components/nodes/stepper.vue +98 -0
- package/src/components/nodes/text-field.vue +1 -1
- package/src/components/nodes/textarea.vue +1 -1
- package/src/components/options.js +23 -2
- package/src/components/tree.vue +2 -1
- package/src/components/types.ts +8 -0
- package/src/components/vjsf.vue +38 -94
- package/src/composables/use-dnd.js +54 -0
- package/src/composables/use-vjsf.js +115 -0
- package/src/index.js +2 -1
- package/src/styles/vjsf.css +10 -0
- package/src/utils/arrays.js +15 -0
- package/src/utils/props.js +25 -6
- package/types/compat/v2.d.ts.map +1 -1
- package/types/compile/index.d.ts.map +1 -1
- package/types/components/fragments/help-message.vue.d.ts +8 -0
- package/types/components/fragments/help-message.vue.d.ts.map +1 -0
- package/types/components/fragments/select-item-icon.vue.d.ts +15 -0
- package/types/components/fragments/select-item-icon.vue.d.ts.map +1 -0
- package/types/components/fragments/select-item.vue.d.ts +12 -0
- package/types/components/fragments/select-item.vue.d.ts.map +1 -0
- package/types/components/fragments/select-selection.vue.d.ts +12 -0
- package/types/components/fragments/select-selection.vue.d.ts.map +1 -0
- package/types/components/nodes/autocomplete.vue.d.ts +27 -0
- package/types/components/nodes/autocomplete.vue.d.ts.map +1 -0
- package/types/components/nodes/combobox.vue.d.ts +27 -0
- package/types/components/nodes/combobox.vue.d.ts.map +1 -0
- package/types/components/nodes/markdown.vue.d.ts +27 -0
- package/types/components/nodes/markdown.vue.d.ts.map +1 -0
- package/types/components/nodes/number-combobox.vue.d.ts +27 -0
- package/types/components/nodes/number-combobox.vue.d.ts.map +1 -0
- package/types/components/nodes/select.vue.d.ts +25 -8
- package/types/components/nodes/select.vue.d.ts.map +1 -1
- package/types/components/nodes/stepper.vue.d.ts +10 -0
- package/types/components/nodes/stepper.vue.d.ts.map +1 -0
- package/types/components/options.d.ts +3 -2
- package/types/components/options.d.ts.map +1 -1
- package/types/components/types.d.ts +9 -1
- package/types/components/types.d.ts.map +1 -1
- package/types/components/vjsf.vue.d.ts +5 -3
- package/types/composables/use-dnd.d.ts +21 -0
- package/types/composables/use-dnd.d.ts.map +1 -0
- package/types/composables/use-vjsf.d.ts +17 -0
- package/types/composables/use-vjsf.d.ts.map +1 -0
- package/types/index.d.ts +2 -1
- package/types/index.d.ts.map +1 -1
- package/types/utils/arrays.d.ts +9 -0
- package/types/utils/arrays.d.ts.map +1 -0
- package/types/utils/props.d.ts +6 -3
- package/types/utils/props.d.ts.map +1 -1
|
@@ -1,59 +1,79 @@
|
|
|
1
|
-
<script
|
|
1
|
+
<script>
|
|
2
2
|
import { VSelect } from 'vuetify/components'
|
|
3
|
-
import { computed, ref } from 'vue'
|
|
3
|
+
import { defineComponent, h, computed, ref, shallowRef } from 'vue'
|
|
4
4
|
import { getInputProps } from '../../utils/props.js'
|
|
5
|
+
import { getCompSlots } from '../../utils/slots.js'
|
|
6
|
+
import SelectItem from '../fragments/select-item.vue'
|
|
7
|
+
import SelectSelection from '../fragments/select-selection.vue'
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
export default defineComponent({
|
|
10
|
+
props: {
|
|
11
|
+
modelValue: {
|
|
8
12
|
/** @type import('vue').PropType<import('../types.js').VjsfSelectNode> */
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
type: Object,
|
|
14
|
+
required: true
|
|
15
|
+
},
|
|
16
|
+
statefulLayout: {
|
|
13
17
|
/** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
+
type: Object,
|
|
19
|
+
required: true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
setup (props) {
|
|
23
|
+
/** @type import('vue').Ref<import('@json-layout/vocabulary').SelectItems> */
|
|
24
|
+
const items = shallowRef([])
|
|
25
|
+
/** @type import('vue').Ref<boolean> */
|
|
26
|
+
const loading = ref(false)
|
|
18
27
|
|
|
19
|
-
const fieldProps = computed(() => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
const fieldProps = computed(() => {
|
|
29
|
+
const fieldProps = getInputProps(props.modelValue, props.statefulLayout, ['multiple'])
|
|
30
|
+
if (props.modelValue.options.readOnly) fieldProps.menuProps = { modelValue: false }
|
|
31
|
+
fieldProps.loading = loading.value
|
|
32
|
+
fieldProps.items = items.value
|
|
33
|
+
fieldProps['onUpdate:menu'] = refresh
|
|
34
|
+
return fieldProps
|
|
35
|
+
})
|
|
24
36
|
|
|
25
|
-
/** @type import('
|
|
26
|
-
|
|
27
|
-
/** @type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/** @type import('@json-layout/core').StateTree | null */
|
|
31
|
-
let lastStateTree = null
|
|
32
|
-
/** @type Record<string, any> | null */
|
|
33
|
-
let lastContext = null
|
|
34
|
-
|
|
35
|
-
const refresh = async () => {
|
|
36
|
-
if (props.modelValue.layout.items) return
|
|
37
|
-
if (props.statefulLayout.stateTree === lastStateTree && props.statefulLayout.options.context === lastContext) return
|
|
38
|
-
lastStateTree = props.statefulLayout.stateTree
|
|
39
|
-
lastContext = props.statefulLayout.options.context ?? null
|
|
40
|
-
loading.value = true
|
|
41
|
-
items.value = await props.statefulLayout.getSelectItems(props.modelValue)
|
|
42
|
-
loading.value = false
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!props.modelValue.layout.items) {
|
|
46
|
-
refresh()
|
|
47
|
-
}
|
|
37
|
+
/** @type import('@json-layout/core').StateTree | null */
|
|
38
|
+
let lastStateTree = null
|
|
39
|
+
/** @type Record<string, any> | null */
|
|
40
|
+
let lastContext = null
|
|
48
41
|
|
|
49
|
-
|
|
42
|
+
const refresh = async () => {
|
|
43
|
+
if (props.statefulLayout.stateTree === lastStateTree && props.statefulLayout.options.context === lastContext) return
|
|
44
|
+
lastStateTree = props.statefulLayout.stateTree
|
|
45
|
+
lastContext = props.statefulLayout.options.context ?? null
|
|
46
|
+
loading.value = true
|
|
47
|
+
items.value = await props.statefulLayout.getItems(props.modelValue)
|
|
48
|
+
loading.value = false
|
|
49
|
+
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
if (!props.modelValue.layout.items) {
|
|
52
|
+
refresh()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fieldSlots = computed(() => {
|
|
56
|
+
const slots = getCompSlots(props.modelValue, props.statefulLayout)
|
|
57
|
+
if (!slots.item) {
|
|
58
|
+
slots.item = (/** @type {any} */ context) => h(SelectItem, {
|
|
59
|
+
multiple: props.modelValue.layout.multiple,
|
|
60
|
+
itemProps: context.props,
|
|
61
|
+
item: context.item.raw
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
if (!slots.selection) {
|
|
65
|
+
slots.selection = (/** @type {any} */ context) => h(SelectSelection, {
|
|
66
|
+
multiple: props.modelValue.layout.multiple,
|
|
67
|
+
last: props.modelValue.layout.multiple && context.index === props.modelValue.data.length - 1,
|
|
68
|
+
item: context.item.raw
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
return slots
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
return () => h(VSelect, fieldProps.value, fieldSlots.value)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
</script>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { VStepper, VStepperHeader, VStepperItem, VContainer } from 'vuetify/components'
|
|
4
|
+
import { isSection } from '@json-layout/core'
|
|
5
|
+
import Node from '../node.vue'
|
|
6
|
+
import SectionHeader from '../fragments/section-header.vue'
|
|
7
|
+
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
modelValue: {
|
|
10
|
+
/** @type import('vue').PropType<import('../types.js').VjsfStepperNode> */
|
|
11
|
+
type: Object,
|
|
12
|
+
required: true
|
|
13
|
+
},
|
|
14
|
+
statefulLayout: {
|
|
15
|
+
/** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
|
|
16
|
+
type: Object,
|
|
17
|
+
required: true
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const step = ref(0)
|
|
22
|
+
|
|
23
|
+
const firstErrorIndex = computed(() => {
|
|
24
|
+
const index = props.modelValue.children.findIndex(child => child.validated && !!(child.error || child.childError))
|
|
25
|
+
return index === -1 ? props.modelValue.children.length : index
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const goNext = () => {
|
|
29
|
+
console.log(props.statefulLayout.validationState)
|
|
30
|
+
const child = props.modelValue.children[step.value]
|
|
31
|
+
props.statefulLayout.validateNodeRecurse(child)
|
|
32
|
+
console.log(props.statefulLayout.validationState)
|
|
33
|
+
if (!(child.error || child.childError)) step.value++
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<section-header :node="modelValue" />
|
|
39
|
+
<v-stepper v-model="step">
|
|
40
|
+
<v-stepper-header>
|
|
41
|
+
<template
|
|
42
|
+
v-for="(child, i) of modelValue.children"
|
|
43
|
+
:key="child.key"
|
|
44
|
+
>
|
|
45
|
+
<v-stepper-item
|
|
46
|
+
:value="i"
|
|
47
|
+
:title="/** @type {string | undefined} */(child.layout.title ?? child.layout.label)"
|
|
48
|
+
:error="child.validated && !!(child.error || child.childError)"
|
|
49
|
+
:complete="child.validated && !(child.error || child.childError)"
|
|
50
|
+
:editable="i <= firstErrorIndex"
|
|
51
|
+
/>
|
|
52
|
+
<v-divider />
|
|
53
|
+
</template>
|
|
54
|
+
</v-stepper-header>
|
|
55
|
+
<v-stepper-window>
|
|
56
|
+
<v-stepper-window-item
|
|
57
|
+
v-for="(child) of modelValue.children"
|
|
58
|
+
:key="child.key"
|
|
59
|
+
>
|
|
60
|
+
<v-container
|
|
61
|
+
fluid
|
|
62
|
+
class="pa-0"
|
|
63
|
+
>
|
|
64
|
+
<v-row>
|
|
65
|
+
<node
|
|
66
|
+
v-for="grandChild of isSection(child) ? child.children : [child]"
|
|
67
|
+
:key="grandChild.fullKey"
|
|
68
|
+
:model-value="/** @type import('../types.js').VjsfNode */(grandChild)"
|
|
69
|
+
:stateful-layout="statefulLayout"
|
|
70
|
+
/>
|
|
71
|
+
</v-row>
|
|
72
|
+
</v-container>
|
|
73
|
+
</v-stepper-window-item>
|
|
74
|
+
</v-stepper-window>
|
|
75
|
+
<v-stepper-actions>
|
|
76
|
+
<template #prev>
|
|
77
|
+
<v-btn
|
|
78
|
+
v-if="step > 0"
|
|
79
|
+
variant="text"
|
|
80
|
+
@click="step--"
|
|
81
|
+
>
|
|
82
|
+
Back
|
|
83
|
+
</v-btn>
|
|
84
|
+
</template>
|
|
85
|
+
<template #next>
|
|
86
|
+
<v-spacer />
|
|
87
|
+
<v-btn
|
|
88
|
+
v-if="step < modelValue.children.length - 1"
|
|
89
|
+
variant="flat"
|
|
90
|
+
color="primary"
|
|
91
|
+
@click="goNext"
|
|
92
|
+
>
|
|
93
|
+
Next
|
|
94
|
+
</v-btn>
|
|
95
|
+
</template>
|
|
96
|
+
</v-stepper-actions>
|
|
97
|
+
</v-stepper>
|
|
98
|
+
</template>
|
|
@@ -18,7 +18,7 @@ export default defineComponent({
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
setup (props) {
|
|
21
|
-
const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout))
|
|
21
|
+
const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout, ['placeholder']))
|
|
22
22
|
const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
|
|
23
23
|
|
|
24
24
|
// @ts-ignore
|
|
@@ -18,7 +18,7 @@ export default defineComponent({
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
setup (props) {
|
|
21
|
-
const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout))
|
|
21
|
+
const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout, ['placeholder']))
|
|
22
22
|
const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
|
|
23
23
|
|
|
24
24
|
// @ts-ignore
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @type
|
|
1
|
+
/** @type import("./types.js").PartialVjsfOptions */
|
|
2
2
|
export const defaultOptions = {
|
|
3
3
|
// matches the density prop found in many vuetify components
|
|
4
4
|
density: 'default',
|
|
@@ -21,5 +21,26 @@ export const defaultOptions = {
|
|
|
21
21
|
checkboxPropsReadOnly: {},
|
|
22
22
|
switchProps: { hideDetails: 'auto' },
|
|
23
23
|
switchPropsReadOnly: {},
|
|
24
|
-
errorAlertProps: { type: 'error', variant: 'tonal' }
|
|
24
|
+
errorAlertProps: { type: 'error', variant: 'tonal' },
|
|
25
|
+
easyMDEOptions: {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* @param {Partial<import("./types.js").VjsfOptions>} options
|
|
31
|
+
* @param {any} form
|
|
32
|
+
* @param {number} width
|
|
33
|
+
* @param {import("vue").Slots} slots
|
|
34
|
+
* @returns
|
|
35
|
+
*/
|
|
36
|
+
export const getFullOptions = (options, form, width, slots) => {
|
|
37
|
+
const fullOptions = {
|
|
38
|
+
...defaultOptions,
|
|
39
|
+
readOnly: !!(form && (form.isDisabled.value || form.isReadonly.value)),
|
|
40
|
+
...options,
|
|
41
|
+
context: options.context ? JSON.parse(JSON.stringify(options.context)) : {},
|
|
42
|
+
width: Math.round(width ?? 0),
|
|
43
|
+
vjsfSlots: { ...slots }
|
|
44
|
+
}
|
|
45
|
+
return /** @type import('./types.js').VjsfOptions */ (fullOptions)
|
|
25
46
|
}
|
package/src/components/tree.vue
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { VRow } from 'vuetify/components'
|
|
2
3
|
import Node from './node.vue'
|
|
3
4
|
|
|
4
5
|
defineProps({
|
|
@@ -16,7 +17,7 @@ defineProps({
|
|
|
16
17
|
</script>
|
|
17
18
|
|
|
18
19
|
<template>
|
|
19
|
-
<v-row>
|
|
20
|
+
<v-row class="vjsf-tree">
|
|
20
21
|
<node
|
|
21
22
|
:stateful-layout="statefulLayout"
|
|
22
23
|
:model-value="/** @type import('./types.js').VjsfNode */(modelValue.root)"
|
package/src/components/types.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
TextFieldNode,
|
|
18
18
|
TextareaNode,
|
|
19
19
|
VerticalTabsNode,
|
|
20
|
+
StepperNode,
|
|
21
|
+
ComboboxNode,
|
|
20
22
|
CompileOptions
|
|
21
23
|
} from '@json-layout/core'
|
|
22
24
|
|
|
@@ -38,8 +40,11 @@ export type VjsfOptions = StatefulLayoutOptions & CompileOptions & {
|
|
|
38
40
|
switchPropsReadOnly: Record<string, unknown>,
|
|
39
41
|
errorAlertProps: Record<string, unknown>,
|
|
40
42
|
vjsfSlots: Record<string, () => unknown>,
|
|
43
|
+
easyMDEOptions: Record<string, unknown>,
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
export type PartialVjsfOptions = Partial<Omit<VjsfOptions, 'width'>>
|
|
47
|
+
|
|
43
48
|
export type VjsfNode = Omit<StateNode, 'options'> & {options: VjsfOptions}
|
|
44
49
|
export type VjsfTabsNode = Omit<TabsNode, 'options'> & {options: VjsfOptions}
|
|
45
50
|
export type VjsfCheckboxNode = Omit<CheckboxNode, 'options'> & {options: VjsfOptions}
|
|
@@ -57,3 +62,6 @@ export type VjsfSwitchNode = Omit<SwitchNode, 'options'> & {options: VjsfOptions
|
|
|
57
62
|
export type VjsfTextFieldNode = Omit<TextFieldNode, 'options'> & {options: VjsfOptions}
|
|
58
63
|
export type VjsfTextareaNode = Omit<TextareaNode, 'options'> & {options: VjsfOptions}
|
|
59
64
|
export type VjsfVerticalTabsNode = Omit<VerticalTabsNode, 'options'> & {options: VjsfOptions}
|
|
65
|
+
export type VjsfStepperNode = Omit<StepperNode, 'options'> & {options: VjsfOptions}
|
|
66
|
+
|
|
67
|
+
export type VjsfComboboxNode = Omit<ComboboxNode, 'options'> & {options: VjsfOptions}
|
package/src/components/vjsf.vue
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
2
|
+
import { computed, getCurrentInstance } from 'vue'
|
|
3
|
+
|
|
4
|
+
import { compile } from '@json-layout/core'
|
|
5
5
|
import Tree from './tree.vue'
|
|
6
|
+
import { useVjsf, emits } from '../composables/use-vjsf.js'
|
|
7
|
+
import '../styles/vjsf.css'
|
|
6
8
|
|
|
7
9
|
import NodeSection from './nodes/section.vue'
|
|
8
10
|
import NodeTextField from './nodes/text-field.vue'
|
|
@@ -15,12 +17,16 @@ import NodeDatePicker from './nodes/date-picker.vue'
|
|
|
15
17
|
import NodeDateTimePicker from './nodes/date-time-picker.vue'
|
|
16
18
|
import NodeColorPicker from './nodes/color-picker.vue'
|
|
17
19
|
import NodeSelect from './nodes/select.vue'
|
|
20
|
+
import NodeAutocomplete from './nodes/autocomplete.vue'
|
|
18
21
|
import NodeOneOfSelect from './nodes/one-of-select.vue'
|
|
19
22
|
import NodeTabs from './nodes/tabs.vue'
|
|
20
23
|
import NodeVerticalTabs from './nodes/vertical-tabs.vue'
|
|
24
|
+
import NodeCombobox from './nodes/combobox.vue'
|
|
25
|
+
import NodeNumberCombobox from './nodes/number-combobox.vue'
|
|
21
26
|
import NodeExpansionPanels from './nodes/expansion-panels.vue'
|
|
27
|
+
import NodeStepper from './nodes/stepper.vue'
|
|
22
28
|
import NodeList from './nodes/list.vue'
|
|
23
|
-
import
|
|
29
|
+
import NodeMarkdown from './nodes/markdown.vue'
|
|
24
30
|
|
|
25
31
|
const comps = {
|
|
26
32
|
section: NodeSection,
|
|
@@ -34,11 +40,16 @@ const comps = {
|
|
|
34
40
|
'date-time-picker': NodeDateTimePicker,
|
|
35
41
|
'color-picker': NodeColorPicker,
|
|
36
42
|
select: NodeSelect,
|
|
43
|
+
autocomplete: NodeAutocomplete,
|
|
37
44
|
'one-of-select': NodeOneOfSelect,
|
|
38
45
|
tabs: NodeTabs,
|
|
39
46
|
'vertical-tabs': NodeVerticalTabs,
|
|
40
47
|
'expansion-panels': NodeExpansionPanels,
|
|
41
|
-
|
|
48
|
+
stepper: NodeStepper,
|
|
49
|
+
list: NodeList,
|
|
50
|
+
combobox: NodeCombobox,
|
|
51
|
+
'number-combobox': NodeNumberCombobox,
|
|
52
|
+
markdown: NodeMarkdown
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
const instance = getCurrentInstance()
|
|
@@ -53,101 +64,43 @@ const props = defineProps({
|
|
|
53
64
|
type: Object,
|
|
54
65
|
required: true
|
|
55
66
|
},
|
|
67
|
+
precompiledLayout: {
|
|
68
|
+
/** @type import('vue').PropType<import('@json-layout/core').CompiledLayout> */
|
|
69
|
+
type: Object,
|
|
70
|
+
default: null
|
|
71
|
+
},
|
|
56
72
|
modelValue: {
|
|
57
|
-
type:
|
|
73
|
+
type: null,
|
|
58
74
|
default: null
|
|
59
75
|
},
|
|
60
76
|
options: {
|
|
61
|
-
/** @type import('vue').PropType<
|
|
77
|
+
/** @type import('vue').PropType<import('./types.js').PartialVjsfOptions> */
|
|
62
78
|
type: Object,
|
|
63
79
|
required: true
|
|
64
80
|
}
|
|
65
81
|
})
|
|
66
82
|
|
|
67
|
-
const emit = defineEmits(
|
|
68
|
-
|
|
69
|
-
/** @type import('vue').ShallowRef<StatefulLayout | null> */
|
|
70
|
-
const statefulLayout = shallowRef(null)
|
|
71
|
-
/** @type import('vue').ShallowRef<import('@json-layout/core').StateTree | null> */
|
|
72
|
-
const stateTree = shallowRef(null)
|
|
73
|
-
|
|
74
|
-
// cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/composables/form.ts
|
|
75
|
-
const form = inject(Symbol.for('vuetify:form'))
|
|
76
|
-
if (form) {
|
|
77
|
-
form.register({
|
|
78
|
-
id: 'vjsf', // TODO: a unique random id ?
|
|
79
|
-
validate: () => statefulLayout.value?.validate(),
|
|
80
|
-
reset: () => statefulLayout.value?.resetValidation(), // TODO: also empty the data ?
|
|
81
|
-
resetValidation: () => statefulLayout.value?.resetValidation()
|
|
82
|
-
})
|
|
83
|
-
}
|
|
83
|
+
const emit = defineEmits(emits)
|
|
84
84
|
|
|
85
|
-
const el =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
...defaultOptions,
|
|
94
|
-
readOnly: form.isDisabled || form.isReadOnly,
|
|
95
|
-
...props.options,
|
|
96
|
-
context: props.options.context ? JSON.parse(JSON.stringify(props.options.context)) : {},
|
|
97
|
-
width: Math.round(width.value ?? 0),
|
|
98
|
-
vjsfSlots: { ...slots }
|
|
99
|
-
}
|
|
100
|
-
return /** @type import('./types.js').VjsfOptions */ (options)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
const compiledLayout = computed(() => compile(props.schema, fullOptions.value))
|
|
104
|
-
|
|
105
|
-
const onStatefulLayoutUpdate = () => {
|
|
106
|
-
if (!statefulLayout.value) return
|
|
107
|
-
stateTree.value = statefulLayout.value.stateTree
|
|
108
|
-
emit('update:modelValue', statefulLayout.value.data)
|
|
109
|
-
emit('update:state', statefulLayout.value)
|
|
110
|
-
if (form) {
|
|
111
|
-
// cf https://vuetifyjs.com/en/components/forms/#validation-state
|
|
112
|
-
if (statefulLayout.value.valid) form.update('vjsf', true, [])
|
|
113
|
-
else if (statefulLayout.value.hasHiddenError) form.update('vjsf', null, [])
|
|
114
|
-
else form.update('vjsf', false, [])
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const initStatefulLayout = () => {
|
|
119
|
-
if (!width.value) return
|
|
120
|
-
const _statefulLayout = new StatefulLayout(compiledLayout.value, compiledLayout.value.skeletonTree, fullOptions.value, props.modelValue)
|
|
121
|
-
statefulLayout.value = _statefulLayout
|
|
122
|
-
onStatefulLayoutUpdate()
|
|
123
|
-
_statefulLayout.events.on('update', () => {
|
|
124
|
-
onStatefulLayoutUpdate()
|
|
125
|
-
})
|
|
126
|
-
emit('update:state', _statefulLayout)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
watch(fullOptions, (newOptions) => {
|
|
130
|
-
if (statefulLayout.value) {
|
|
131
|
-
statefulLayout.value.options = newOptions
|
|
132
|
-
} else {
|
|
133
|
-
initStatefulLayout()
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
// case where data is updated from outside
|
|
138
|
-
watch(() => props.modelValue, (newData) => {
|
|
139
|
-
if (statefulLayout.value && statefulLayout.value.data !== newData) statefulLayout.value.data = newData
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
// case where schema is updated from outside
|
|
143
|
-
watch(compiledLayout, (newCompiledLayout) => initStatefulLayout())
|
|
85
|
+
const { el, statefulLayout, stateTree } = useVjsf(
|
|
86
|
+
computed(() => props.schema),
|
|
87
|
+
computed(() => props.modelValue),
|
|
88
|
+
computed(() => props.options),
|
|
89
|
+
emit,
|
|
90
|
+
compile,
|
|
91
|
+
props.precompiledLayout
|
|
92
|
+
)
|
|
144
93
|
|
|
145
94
|
</script>
|
|
146
95
|
|
|
147
96
|
<template>
|
|
148
|
-
<div
|
|
97
|
+
<div
|
|
98
|
+
ref="el"
|
|
99
|
+
class="vjsf"
|
|
100
|
+
>
|
|
149
101
|
<tree
|
|
150
102
|
v-if="statefulLayout && stateTree"
|
|
103
|
+
ref="tree"
|
|
151
104
|
:model-value="stateTree"
|
|
152
105
|
:stateful-layout="statefulLayout"
|
|
153
106
|
/>
|
|
@@ -155,14 +108,5 @@ watch(compiledLayout, (newCompiledLayout) => initStatefulLayout())
|
|
|
155
108
|
</template>
|
|
156
109
|
|
|
157
110
|
<style lang="css">
|
|
158
|
-
/*
|
|
159
|
-
.vjsf-input--readonly.v-input--disabled.v-text-field .v-field--disabled input {
|
|
160
|
-
pointer-events: auto;
|
|
161
|
-
}
|
|
162
|
-
.vjsf-input--readonly.v-input--disabled .v-field--disabled,
|
|
163
|
-
.vjsf-input--readonly.v-input--disabled .v-input__details,
|
|
164
|
-
.vjsf-input--readonly.v-input--disabled .v-input__append,
|
|
165
|
-
.vjsf-input--readonly.v-input--disabled .v-input__prepend {
|
|
166
|
-
opacity: inherit;
|
|
167
|
-
}
|
|
111
|
+
/* nothing here, use ../styles/vjsf.css */
|
|
168
112
|
</style>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { shallowRef, ref, computed } from 'vue'
|
|
2
|
+
import { moveArrayItem } from '../utils/arrays.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @template T
|
|
6
|
+
* @param {T[]} array
|
|
7
|
+
* @param {() => void} callback
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export default function useDnd (array, callback) {
|
|
11
|
+
const activeDnd = computed(() => {
|
|
12
|
+
// cf https://ultimatecourses.com/blog/feature-detect-javascript-drag-drop-api
|
|
13
|
+
if (!('draggable' in document.createElement('div'))) return false
|
|
14
|
+
// cf https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
|
|
15
|
+
if (window.matchMedia('(pointer: coarse)').matches) return false
|
|
16
|
+
return true
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const sortableArray = shallowRef(array)
|
|
20
|
+
|
|
21
|
+
const draggable = ref(-1)
|
|
22
|
+
const dragging = ref(-1)
|
|
23
|
+
|
|
24
|
+
const itemBind = (/** @type {number} */itemIndex) => ({
|
|
25
|
+
onDragstart: () => {
|
|
26
|
+
dragging.value = itemIndex
|
|
27
|
+
},
|
|
28
|
+
onDragover: () => {
|
|
29
|
+
sortableArray.value = moveArrayItem(sortableArray.value, dragging.value, itemIndex)
|
|
30
|
+
dragging.value = itemIndex
|
|
31
|
+
},
|
|
32
|
+
onDragend: () => {
|
|
33
|
+
dragging.value = -1
|
|
34
|
+
callback()
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const handleBind = (/** @type {number} */itemIndex) => ({
|
|
39
|
+
onMouseover () {
|
|
40
|
+
draggable.value = itemIndex
|
|
41
|
+
},
|
|
42
|
+
onMouseout () {
|
|
43
|
+
draggable.value = -1
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
activeDnd,
|
|
49
|
+
sortableArray,
|
|
50
|
+
draggable,
|
|
51
|
+
itemBind,
|
|
52
|
+
handleBind
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { StatefulLayout } from '@json-layout/core'
|
|
2
|
+
import { inject, toRaw, shallowRef, computed, ref, watch, useSlots } from 'vue'
|
|
3
|
+
import { useElementSize } from '@vueuse/core'
|
|
4
|
+
import { getFullOptions } from '../components/options.js'
|
|
5
|
+
|
|
6
|
+
export const emits = {
|
|
7
|
+
/**
|
|
8
|
+
* @arg {any} data
|
|
9
|
+
*/
|
|
10
|
+
'update:modelValue': (data) => true,
|
|
11
|
+
/**
|
|
12
|
+
* @arg {StatefulLayout} state
|
|
13
|
+
*/
|
|
14
|
+
'update:state': (state) => true
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {import('vue').Ref<Object>} schema
|
|
19
|
+
* @param {import('vue').Ref<any>} modelValue
|
|
20
|
+
* @param {import('vue').Ref<import("../components/types.js").PartialVjsfOptions>} options
|
|
21
|
+
* @param {any} emit
|
|
22
|
+
* @param {typeof import('@json-layout/core').compile} compile
|
|
23
|
+
* @param {import('@json-layout/core').CompiledLayout} [precompiledLayout]
|
|
24
|
+
*/
|
|
25
|
+
export const useVjsf = (schema, modelValue, options, emit, compile, precompiledLayout) => {
|
|
26
|
+
const el = ref(null)
|
|
27
|
+
const { width } = useElementSize(el)
|
|
28
|
+
|
|
29
|
+
/** @type import('vue').ShallowRef<StatefulLayout | null> */
|
|
30
|
+
const statefulLayout = shallowRef(null)
|
|
31
|
+
/** @type import('vue').ShallowRef<import('@json-layout/core').StateTree | null> */
|
|
32
|
+
const stateTree = shallowRef(null)
|
|
33
|
+
|
|
34
|
+
// cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/composables/form.ts
|
|
35
|
+
const form = inject(Symbol.for('vuetify:form'))
|
|
36
|
+
if (form) {
|
|
37
|
+
form.register({
|
|
38
|
+
id: 'vjsf', // TODO: a unique random id ?
|
|
39
|
+
validate: () => {
|
|
40
|
+
statefulLayout.value?.validate()
|
|
41
|
+
return statefulLayout.value?.errors
|
|
42
|
+
},
|
|
43
|
+
reset: () => statefulLayout.value?.resetValidation(), // TODO: also empty the data ?
|
|
44
|
+
resetValidation: () => statefulLayout.value?.resetValidation()
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const slots = useSlots()
|
|
49
|
+
|
|
50
|
+
const fullOptions = computed(() => getFullOptions(options.value, form, width.value, slots))
|
|
51
|
+
|
|
52
|
+
const compiledLayout = computed(() => {
|
|
53
|
+
if (precompiledLayout) return precompiledLayout
|
|
54
|
+
const compiledLayout = compile(schema.value, fullOptions.value)
|
|
55
|
+
return compiledLayout
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const onStatefulLayoutUpdate = () => {
|
|
59
|
+
if (!statefulLayout.value) return
|
|
60
|
+
stateTree.value = statefulLayout.value.stateTree
|
|
61
|
+
emit('update:modelValue', statefulLayout.value.data)
|
|
62
|
+
emit('update:state', statefulLayout.value)
|
|
63
|
+
if (form) {
|
|
64
|
+
// cf https://vuetifyjs.com/en/components/forms/#validation-state
|
|
65
|
+
if (statefulLayout.value.valid) form.update('vjsf', true, [])
|
|
66
|
+
else if (statefulLayout.value.hasHiddenError) form.update('vjsf', null, [])
|
|
67
|
+
else form.update('vjsf', false, [])
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const initStatefulLayout = () => {
|
|
72
|
+
if (!width.value) return
|
|
73
|
+
const _statefulLayout = new StatefulLayout(
|
|
74
|
+
toRaw(compiledLayout.value),
|
|
75
|
+
toRaw(compiledLayout.value.skeletonTree),
|
|
76
|
+
toRaw(fullOptions.value),
|
|
77
|
+
toRaw(modelValue.value)
|
|
78
|
+
)
|
|
79
|
+
statefulLayout.value = _statefulLayout
|
|
80
|
+
onStatefulLayoutUpdate()
|
|
81
|
+
_statefulLayout.events.on('update', () => {
|
|
82
|
+
onStatefulLayoutUpdate()
|
|
83
|
+
})
|
|
84
|
+
emit('update:state', _statefulLayout)
|
|
85
|
+
_statefulLayout.events.on('autofocus', () => {
|
|
86
|
+
if (!el.value) return
|
|
87
|
+
// @ts-ignore
|
|
88
|
+
const autofocusNodeElement = el.value.querySelector('.vjsf-input--autofocus')
|
|
89
|
+
if (autofocusNodeElement) {
|
|
90
|
+
const autofocusInputElement = autofocusNodeElement.querySelector('input')
|
|
91
|
+
if (autofocusInputElement) autofocusInputElement.focus()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
watch(fullOptions, (newOptions) => {
|
|
97
|
+
if (statefulLayout.value) {
|
|
98
|
+
statefulLayout.value.options = newOptions
|
|
99
|
+
} else {
|
|
100
|
+
initStatefulLayout()
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// case where data is updated from outside
|
|
105
|
+
watch(modelValue, (newData) => {
|
|
106
|
+
if (statefulLayout.value && statefulLayout.value.data !== newData) statefulLayout.value.data = toRaw(newData)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// case where schema is updated from outside
|
|
110
|
+
watch(compiledLayout, (newCompiledLayout) => {
|
|
111
|
+
initStatefulLayout()
|
|
112
|
+
}, { immediate: true })
|
|
113
|
+
|
|
114
|
+
return { el, statefulLayout, stateTree }
|
|
115
|
+
}
|