@koumoul/vjsf 3.0.0-alpha.1 → 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 +8 -8
- package/src/compile/index.js +1 -1
- package/src/compile/v-jsf-compiled.vue.ejs +14 -52
- package/src/components/nodes/combobox.vue +1 -1
- package/src/components/nodes/list.vue +170 -86
- package/src/components/nodes/markdown.vue +209 -5
- package/src/components/nodes/number-field.vue +1 -1
- 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 +22 -1
- package/src/components/types.ts +4 -0
- package/src/components/vjsf.vue +21 -104
- package/src/composables/use-dnd.js +54 -0
- package/src/composables/use-vjsf.js +115 -0
- package/src/styles/vjsf.css +10 -0
- package/src/utils/arrays.js +15 -0
- package/src/utils/props.js +16 -5
- 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/nodes/markdown.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 +1 -0
- package/types/components/options.d.ts.map +1 -1
- package/types/components/tree.vue.d.ts +2 -2
- package/types/components/types.d.ts +5 -1
- package/types/components/types.d.ts.map +1 -1
- package/types/components/vjsf.vue.d.ts +5 -6
- package/types/components/vjsf.vue.d.ts.map +1 -1
- 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/utils/arrays.d.ts +9 -0
- package/types/utils/arrays.d.ts.map +1 -0
- package/types/utils/props.d.ts +4 -2
- package/types/utils/props.d.ts.map +1 -1
|
@@ -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
|
|
@@ -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/types.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
TextFieldNode,
|
|
18
18
|
TextareaNode,
|
|
19
19
|
VerticalTabsNode,
|
|
20
|
+
StepperNode,
|
|
20
21
|
ComboboxNode,
|
|
21
22
|
CompileOptions
|
|
22
23
|
} from '@json-layout/core'
|
|
@@ -39,6 +40,7 @@ export type VjsfOptions = StatefulLayoutOptions & CompileOptions & {
|
|
|
39
40
|
switchPropsReadOnly: Record<string, unknown>,
|
|
40
41
|
errorAlertProps: Record<string, unknown>,
|
|
41
42
|
vjsfSlots: Record<string, () => unknown>,
|
|
43
|
+
easyMDEOptions: Record<string, unknown>,
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export type PartialVjsfOptions = Partial<Omit<VjsfOptions, 'width'>>
|
|
@@ -60,4 +62,6 @@ export type VjsfSwitchNode = Omit<SwitchNode, 'options'> & {options: VjsfOptions
|
|
|
60
62
|
export type VjsfTextFieldNode = Omit<TextFieldNode, 'options'> & {options: VjsfOptions}
|
|
61
63
|
export type VjsfTextareaNode = Omit<TextareaNode, 'options'> & {options: VjsfOptions}
|
|
62
64
|
export type VjsfVerticalTabsNode = Omit<VerticalTabsNode, 'options'> & {options: VjsfOptions}
|
|
65
|
+
export type VjsfStepperNode = Omit<StepperNode, 'options'> & {options: VjsfOptions}
|
|
66
|
+
|
|
63
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'
|
|
@@ -22,8 +24,9 @@ import NodeVerticalTabs from './nodes/vertical-tabs.vue'
|
|
|
22
24
|
import NodeCombobox from './nodes/combobox.vue'
|
|
23
25
|
import NodeNumberCombobox from './nodes/number-combobox.vue'
|
|
24
26
|
import NodeExpansionPanels from './nodes/expansion-panels.vue'
|
|
27
|
+
import NodeStepper from './nodes/stepper.vue'
|
|
25
28
|
import NodeList from './nodes/list.vue'
|
|
26
|
-
import
|
|
29
|
+
import NodeMarkdown from './nodes/markdown.vue'
|
|
27
30
|
|
|
28
31
|
const comps = {
|
|
29
32
|
section: NodeSection,
|
|
@@ -42,9 +45,11 @@ const comps = {
|
|
|
42
45
|
tabs: NodeTabs,
|
|
43
46
|
'vertical-tabs': NodeVerticalTabs,
|
|
44
47
|
'expansion-panels': NodeExpansionPanels,
|
|
48
|
+
stepper: NodeStepper,
|
|
45
49
|
list: NodeList,
|
|
46
50
|
combobox: NodeCombobox,
|
|
47
|
-
'number-combobox': NodeNumberCombobox
|
|
51
|
+
'number-combobox': NodeNumberCombobox,
|
|
52
|
+
markdown: NodeMarkdown
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
const instance = getCurrentInstance()
|
|
@@ -65,7 +70,7 @@ const props = defineProps({
|
|
|
65
70
|
default: null
|
|
66
71
|
},
|
|
67
72
|
modelValue: {
|
|
68
|
-
type:
|
|
73
|
+
type: null,
|
|
69
74
|
default: null
|
|
70
75
|
},
|
|
71
76
|
options: {
|
|
@@ -75,95 +80,16 @@ const props = defineProps({
|
|
|
75
80
|
}
|
|
76
81
|
})
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
const emit = defineEmits({
|
|
80
|
-
/**
|
|
81
|
-
* @arg {any} data
|
|
82
|
-
*/
|
|
83
|
-
'update:modelValue': (data) => true,
|
|
84
|
-
/**
|
|
85
|
-
* @arg {StatefulLayout} state
|
|
86
|
-
*/
|
|
87
|
-
'update:state': (state) => true
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
/** @type import('vue').ShallowRef<StatefulLayout | null> */
|
|
91
|
-
const statefulLayout = shallowRef(null)
|
|
92
|
-
/** @type import('vue').ShallowRef<import('@json-layout/core').StateTree | null> */
|
|
93
|
-
const stateTree = shallowRef(null)
|
|
94
|
-
|
|
95
|
-
// cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/composables/form.ts
|
|
96
|
-
const form = inject(Symbol.for('vuetify:form'))
|
|
97
|
-
if (form) {
|
|
98
|
-
form.register({
|
|
99
|
-
id: 'vjsf', // TODO: a unique random id ?
|
|
100
|
-
validate: () => statefulLayout.value?.validate(),
|
|
101
|
-
reset: () => statefulLayout.value?.resetValidation(), // TODO: also empty the data ?
|
|
102
|
-
resetValidation: () => statefulLayout.value?.resetValidation()
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const el = ref(null)
|
|
107
|
-
const { width } = useElementSize(el)
|
|
108
|
-
|
|
109
|
-
const slots = useSlots()
|
|
110
|
-
|
|
111
|
-
const fullOptions = computed(() => {
|
|
112
|
-
const options = {
|
|
113
|
-
...defaultOptions,
|
|
114
|
-
readOnly: !!(form && (form.isDisabled.value || form.isReadonly.value)),
|
|
115
|
-
...props.options,
|
|
116
|
-
context: props.options.context ? JSON.parse(JSON.stringify(props.options.context)) : {},
|
|
117
|
-
width: Math.round(width.value ?? 0),
|
|
118
|
-
vjsfSlots: { ...slots }
|
|
119
|
-
}
|
|
120
|
-
return /** @type import('./types.js').VjsfOptions */ (options)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
const compiledLayout = computed(() => {
|
|
124
|
-
if (props.precompiledLayout) return props.precompiledLayout
|
|
125
|
-
return compile(props.schema, fullOptions.value)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
const onStatefulLayoutUpdate = () => {
|
|
129
|
-
if (!statefulLayout.value) return
|
|
130
|
-
stateTree.value = statefulLayout.value.stateTree
|
|
131
|
-
emit('update:modelValue', statefulLayout.value.data)
|
|
132
|
-
emit('update:state', statefulLayout.value)
|
|
133
|
-
if (form) {
|
|
134
|
-
// cf https://vuetifyjs.com/en/components/forms/#validation-state
|
|
135
|
-
if (statefulLayout.value.valid) form.update('vjsf', true, [])
|
|
136
|
-
else if (statefulLayout.value.hasHiddenError) form.update('vjsf', null, [])
|
|
137
|
-
else form.update('vjsf', false, [])
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const initStatefulLayout = () => {
|
|
142
|
-
if (!width.value) return
|
|
143
|
-
const _statefulLayout = new StatefulLayout(toRaw(compiledLayout.value), toRaw(compiledLayout.value.skeletonTree), toRaw(fullOptions.value), toRaw(props.modelValue))
|
|
144
|
-
statefulLayout.value = _statefulLayout
|
|
145
|
-
onStatefulLayoutUpdate()
|
|
146
|
-
_statefulLayout.events.on('update', () => {
|
|
147
|
-
onStatefulLayoutUpdate()
|
|
148
|
-
})
|
|
149
|
-
emit('update:state', _statefulLayout)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
watch(fullOptions, (newOptions) => {
|
|
153
|
-
if (statefulLayout.value) {
|
|
154
|
-
statefulLayout.value.options = newOptions
|
|
155
|
-
} else {
|
|
156
|
-
initStatefulLayout()
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
// case where data is updated from outside
|
|
161
|
-
watch(() => props.modelValue, (newData) => {
|
|
162
|
-
if (statefulLayout.value && statefulLayout.value.data !== newData) statefulLayout.value.data = newData
|
|
163
|
-
})
|
|
83
|
+
const emit = defineEmits(emits)
|
|
164
84
|
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
)
|
|
167
93
|
|
|
168
94
|
</script>
|
|
169
95
|
|
|
@@ -182,14 +108,5 @@ watch(compiledLayout, (newCompiledLayout) => initStatefulLayout())
|
|
|
182
108
|
</template>
|
|
183
109
|
|
|
184
110
|
<style lang="css">
|
|
185
|
-
/*
|
|
186
|
-
.vjsf-input--readonly.v-input--disabled.v-text-field .v-field--disabled input {
|
|
187
|
-
pointer-events: auto;
|
|
188
|
-
}
|
|
189
|
-
.vjsf-input--readonly.v-input--disabled .v-field--disabled,
|
|
190
|
-
.vjsf-input--readonly.v-input--disabled .v-input__details,
|
|
191
|
-
.vjsf-input--readonly.v-input--disabled .v-input__append,
|
|
192
|
-
.vjsf-input--readonly.v-input--disabled .v-input__prepend {
|
|
193
|
-
opacity: inherit;
|
|
194
|
-
}
|
|
111
|
+
/* nothing here, use ../styles/vjsf.css */
|
|
195
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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* override vuetify styles to manage readOnly fields more usable than the default disabled fields */
|
|
2
|
+
.vjsf-input--readonly.v-input--disabled.v-text-field .v-field--disabled input {
|
|
3
|
+
pointer-events: auto;
|
|
4
|
+
}
|
|
5
|
+
.vjsf-input--readonly.v-input--disabled .v-field--disabled,
|
|
6
|
+
.vjsf-input--readonly.v-input--disabled .v-input__details,
|
|
7
|
+
.vjsf-input--readonly.v-input--disabled .v-input__append,
|
|
8
|
+
.vjsf-input--readonly.v-input--disabled .v-input__prepend {
|
|
9
|
+
opacity: inherit;
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template T
|
|
3
|
+
* @param {T[]} array
|
|
4
|
+
* @param {number} fromIndex
|
|
5
|
+
* @param {number} toIndex
|
|
6
|
+
* @return {T[]}
|
|
7
|
+
*/
|
|
8
|
+
export function moveArrayItem (array, fromIndex, toIndex) {
|
|
9
|
+
if (fromIndex === toIndex || fromIndex === -1 || toIndex === -1) return array
|
|
10
|
+
const newArray = [...array]
|
|
11
|
+
const element = newArray[fromIndex]
|
|
12
|
+
newArray.splice(fromIndex, 1)
|
|
13
|
+
newArray.splice(toIndex, 0, element)
|
|
14
|
+
return newArray
|
|
15
|
+
}
|
package/src/utils/props.js
CHANGED
|
@@ -3,15 +3,23 @@ import { camelize } from 'vue'
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @param {(Record<string, any> | undefined)[]} propsLevels
|
|
6
|
-
* @returns Record<string, any>
|
|
6
|
+
* @returns {Record<string, any> & {class: string[]}}
|
|
7
7
|
*/
|
|
8
8
|
export function mergePropsLevels (propsLevels) {
|
|
9
|
-
/** @type Record<string, any> */
|
|
10
|
-
const fullProps = {}
|
|
9
|
+
/** @type {Record<string, any> & {class: string[]}} */
|
|
10
|
+
const fullProps = { class: [] }
|
|
11
11
|
for (const propsLevel of propsLevels) {
|
|
12
12
|
if (propsLevel) {
|
|
13
13
|
for (const key of Object.keys(propsLevel)) {
|
|
14
|
-
|
|
14
|
+
if (key === 'class') {
|
|
15
|
+
// a small convention for merging/overwriting classes:
|
|
16
|
+
// a class defined as a simple string overwrites the previous ones
|
|
17
|
+
// a class defined as an array is merged with the previous ones
|
|
18
|
+
if (Array.isArray(propsLevel.class)) fullProps.class = fullProps.class.concat(propsLevel.class)
|
|
19
|
+
else fullProps.class = [propsLevel.class]
|
|
20
|
+
} else {
|
|
21
|
+
fullProps[camelize(key)] = propsLevel[key]
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
24
|
}
|
|
17
25
|
}
|
|
@@ -49,7 +57,10 @@ export function getInputProps (node, statefulLayout, layoutPropsMap, isMainComp
|
|
|
49
57
|
fullProps.modelValue = node.data
|
|
50
58
|
if (node.options.readOnly) {
|
|
51
59
|
fullProps.disabled = true
|
|
52
|
-
fullProps.class
|
|
60
|
+
fullProps.class.push('vjsf-input--readonly')
|
|
61
|
+
}
|
|
62
|
+
if (node.autofocus) {
|
|
63
|
+
fullProps.class.push('vjsf-input--autofocus')
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
if (layoutPropsMap) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
declare const _default: import("vue").DefineComponent<{}, {
|
|
2
2
|
multiple: boolean;
|
|
3
3
|
itemProps: Record<string, any>;
|
|
4
|
-
item: import("
|
|
4
|
+
item: import("../../../../node_modules/@json-layout/vocabulary/types/normalized-layout/types.js").SelectItem;
|
|
5
5
|
$props: {
|
|
6
6
|
readonly multiple?: boolean | undefined;
|
|
7
7
|
readonly itemProps?: Record<string, any> | undefined;
|
|
8
|
-
readonly item?: import("
|
|
8
|
+
readonly item?: import("../../../../node_modules/@json-layout/vocabulary/types/normalized-layout/types.js").SelectItem | undefined;
|
|
9
9
|
};
|
|
10
10
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}, {}>;
|
|
11
11
|
export default _default;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
declare const _default: import("vue").DefineComponent<{}, {
|
|
2
2
|
multiple: boolean;
|
|
3
|
-
item: import("
|
|
3
|
+
item: import("../../../../node_modules/@json-layout/vocabulary/types/normalized-layout/types.js").SelectItem;
|
|
4
4
|
last: boolean;
|
|
5
5
|
$props: {
|
|
6
6
|
readonly multiple?: boolean | undefined;
|
|
7
|
-
readonly item?: import("
|
|
7
|
+
readonly item?: import("../../../../node_modules/@json-layout/vocabulary/types/normalized-layout/types.js").SelectItem | undefined;
|
|
8
8
|
readonly last?: boolean | undefined;
|
|
9
9
|
};
|
|
10
10
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}, {}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.vue.d.ts","sourceRoot":"","sources":["../../../src/components/nodes/markdown.vue.js"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"markdown.vue.d.ts","sourceRoot":"","sources":["../../../src/components/nodes/markdown.vue.js"],"names":[],"mappings":";;QAYM,2EAA2E;cAAjE,OAAO,KAAK,EAAE,QAAQ,CAAC,OAAO,aAAa,EAAE,gBAAgB,CAAC;;;;QAKxE,+EAA+E;cAArE,OAAO,KAAK,EAAE,QAAQ,CAAC,OAAO,mBAAmB,EAAE,cAAc,CAAC;;;;;;;QAL5E,2EAA2E;cAAjE,OAAO,KAAK,EAAE,QAAQ,CAAC,OAAO,aAAa,EAAE,gBAAgB,CAAC;;;;QAKxE,+EAA+E;cAArE,OAAO,KAAK,EAAE,QAAQ,CAAC,OAAO,mBAAmB,EAAE,cAAc,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {
|
|
2
|
+
modelValue: import("../types.js").VjsfStepperNode;
|
|
3
|
+
statefulLayout: import("@json-layout/core").StatefulLayout;
|
|
4
|
+
$props: {
|
|
5
|
+
readonly modelValue?: import("../types.js").VjsfStepperNode | undefined;
|
|
6
|
+
readonly statefulLayout?: import("@json-layout/core").StatefulLayout | undefined;
|
|
7
|
+
};
|
|
8
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}, {}>;
|
|
9
|
+
export default _default;
|
|
10
|
+
//# sourceMappingURL=stepper.vue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stepper.vue.d.ts","sourceRoot":"","sources":["../../../src/components/nodes/stepper.vue.js"],"names":[],"mappings":""}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
/** @type import("./types.js").PartialVjsfOptions */
|
|
2
2
|
export const defaultOptions: import("./types.js").PartialVjsfOptions;
|
|
3
|
+
export function getFullOptions(options: Partial<import("./types.js").VjsfOptions>, form: any, width: number, slots: import("vue").Slots): import("./types.js").VjsfOptions;
|
|
3
4
|
//# sourceMappingURL=options.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/components/options.js"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,6BADU,OAAO,YAAY,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/components/options.js"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,6BADU,OAAO,YAAY,EAAE,kBAAkB,CAyBhD;AAUM,wCANI,QAAQ,OAAO,YAAY,EAAE,WAAW,CAAC,QACzC,GAAG,SACH,MAAM,SACN,OAAO,KAAK,EAAE,KAAK,oCAa7B"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
declare const _default: import("vue").DefineComponent<{}, {
|
|
2
|
-
modelValue: import("
|
|
2
|
+
modelValue: import("../../../node_modules/@json-layout/core/types/state/types.js").StateTree;
|
|
3
3
|
statefulLayout: import("@json-layout/core").StatefulLayout;
|
|
4
4
|
$props: {
|
|
5
|
-
readonly modelValue?: import("
|
|
5
|
+
readonly modelValue?: import("../../../node_modules/@json-layout/core/types/state/types.js").StateTree | undefined;
|
|
6
6
|
readonly statefulLayout?: import("@json-layout/core").StatefulLayout | undefined;
|
|
7
7
|
};
|
|
8
8
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}, {}>;
|