@koumoul/vjsf 3.0.0-alpha.1 → 3.0.0-alpha.3
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/compat/v2.js +1 -4
- 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 +171 -88
- package/src/components/nodes/markdown.vue +218 -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 +119 -0
- package/src/styles/vjsf.css +10 -0
- package/src/utils/arrays.js +15 -0
- package/src/utils/props.js +16 -5
- package/types/compat/v2.d.ts.map +1 -1
- 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
- package/src/utils/clone.js +0 -3
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { defineComponent, h, computed } from 'vue'
|
|
3
|
-
import {
|
|
2
|
+
import { defineComponent, h, computed, onMounted, ref, onUnmounted, watch } from 'vue'
|
|
3
|
+
import { VInput, VLabel } from 'vuetify/components'
|
|
4
4
|
import { getInputProps } from '../../utils/props.js'
|
|
5
5
|
import { getCompSlots } from '../../utils/slots.js'
|
|
6
|
+
import 'easymde/dist/easymde.min.css'
|
|
6
7
|
|
|
7
8
|
export default defineComponent({
|
|
8
9
|
props: {
|
|
@@ -18,12 +19,224 @@ export default defineComponent({
|
|
|
18
19
|
}
|
|
19
20
|
},
|
|
20
21
|
setup (props) {
|
|
22
|
+
/** @type {import('vue').Ref<null | HTMLElement>} */
|
|
23
|
+
const element = ref(null)
|
|
24
|
+
|
|
21
25
|
const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout))
|
|
22
|
-
const fieldSlots = computed(() =>
|
|
26
|
+
const fieldSlots = computed(() => {
|
|
27
|
+
const fieldSlots = getCompSlots(props.modelValue, props.statefulLayout)
|
|
28
|
+
fieldSlots.default = () => [
|
|
29
|
+
h('div', { style: 'width:100%' }, [
|
|
30
|
+
h(VLabel, { text: fieldProps.value.label }),
|
|
31
|
+
h('textarea', { ref: element, style: 'display:none' })
|
|
32
|
+
])
|
|
33
|
+
]
|
|
34
|
+
return fieldSlots
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/** @type {EasyMDE | null} */
|
|
38
|
+
let easymde = null
|
|
39
|
+
|
|
40
|
+
const initEasyMDE = async () => {
|
|
41
|
+
if (!element.value) throw new Error('component was not mounted for markdown editor')
|
|
42
|
+
|
|
43
|
+
const EasyMDE = (await import('easymde')).default
|
|
44
|
+
|
|
45
|
+
const messages = props.modelValue.messages
|
|
46
|
+
|
|
47
|
+
const config = {
|
|
48
|
+
element: element.value,
|
|
49
|
+
initialValue: props.modelValue.data ?? '',
|
|
50
|
+
renderingConfig: {},
|
|
51
|
+
status: false,
|
|
52
|
+
autoDownloadFontAwesome: false,
|
|
53
|
+
spellChecker: false,
|
|
54
|
+
minHeight: '300px',
|
|
55
|
+
insertTexts: {
|
|
56
|
+
link: [messages.mdeLink1, messages.mdeLink2],
|
|
57
|
+
image: [messages.mdeImg1, messages.mdeImg2],
|
|
58
|
+
table: [messages.mdeTable1, messages.mdeTable2],
|
|
59
|
+
horizontalRule: ['', '\n\n-----\n\n']
|
|
60
|
+
},
|
|
61
|
+
// cf https://github.com/Ionaru/easy-markdown-editor/blob/master/src/js/easymde.js#L1380
|
|
62
|
+
toolbar: [{
|
|
63
|
+
name: 'bold',
|
|
64
|
+
action: EasyMDE.toggleBold,
|
|
65
|
+
className: 'mdi mdi-format-bold',
|
|
66
|
+
title: messages.bold
|
|
67
|
+
}, {
|
|
68
|
+
name: 'italic',
|
|
69
|
+
action: EasyMDE.toggleItalic,
|
|
70
|
+
className: 'mdi mdi-format-italic',
|
|
71
|
+
title: messages.italic
|
|
72
|
+
}, {
|
|
73
|
+
name: 'heading',
|
|
74
|
+
action: EasyMDE.toggleHeadingSmaller,
|
|
75
|
+
className: 'mdi mdi-format-title',
|
|
76
|
+
title: messages.heading
|
|
77
|
+
}, /* {
|
|
78
|
+
name: 'heading-1',
|
|
79
|
+
action: EasyMDE.toggleHeading1,
|
|
80
|
+
className: 'mdi mdi-format-title',
|
|
81
|
+
title: 'Titre 1'
|
|
82
|
+
}, {
|
|
83
|
+
name: 'heading-2',
|
|
84
|
+
action: EasyMDE.toggleHeading2,
|
|
85
|
+
className: 'mdi mdi-format-title',
|
|
86
|
+
title: 'Titre 2'
|
|
87
|
+
}, {
|
|
88
|
+
name: 'heading-3',
|
|
89
|
+
action: EasyMDE.toggleHeading3,
|
|
90
|
+
className: 'mdi mdi-format-title',
|
|
91
|
+
title: 'Titre 3'
|
|
92
|
+
}, */
|
|
93
|
+
'|',
|
|
94
|
+
{
|
|
95
|
+
name: 'quote',
|
|
96
|
+
action: EasyMDE.toggleBlockquote,
|
|
97
|
+
className: 'mdi mdi-format-quote-open',
|
|
98
|
+
title: messages.quote
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'unordered-list',
|
|
102
|
+
action: EasyMDE.toggleUnorderedList,
|
|
103
|
+
className: 'mdi mdi-format-list-bulleted',
|
|
104
|
+
title: messages.unorderedList
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'ordered-list',
|
|
108
|
+
action: EasyMDE.toggleOrderedList,
|
|
109
|
+
className: 'mdi mdi-format-list-numbered',
|
|
110
|
+
title: messages.orderedList
|
|
111
|
+
},
|
|
112
|
+
'|',
|
|
113
|
+
{
|
|
114
|
+
name: 'link',
|
|
115
|
+
action: EasyMDE.drawLink,
|
|
116
|
+
className: 'mdi mdi-link',
|
|
117
|
+
title: messages.createLink
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'image',
|
|
121
|
+
action: EasyMDE.drawImage,
|
|
122
|
+
className: 'mdi mdi-image',
|
|
123
|
+
title: messages.insertImage
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'table',
|
|
127
|
+
action: EasyMDE.drawTable,
|
|
128
|
+
className: 'mdi mdi-table',
|
|
129
|
+
title: messages.createTable
|
|
130
|
+
},
|
|
131
|
+
'|',
|
|
132
|
+
{
|
|
133
|
+
name: 'preview',
|
|
134
|
+
action: EasyMDE.togglePreview,
|
|
135
|
+
className: 'mdi mdi-eye text-accent',
|
|
136
|
+
title: messages.preview,
|
|
137
|
+
noDisable: true
|
|
138
|
+
},
|
|
139
|
+
'|',
|
|
140
|
+
{
|
|
141
|
+
name: 'undo',
|
|
142
|
+
action: EasyMDE.undo,
|
|
143
|
+
className: 'mdi mdi-undo',
|
|
144
|
+
title: messages.undo,
|
|
145
|
+
noDisable: true
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'redo',
|
|
149
|
+
action: EasyMDE.redo,
|
|
150
|
+
className: 'mdi mdi-redo',
|
|
151
|
+
title: messages.redo,
|
|
152
|
+
noDisable: true
|
|
153
|
+
},
|
|
154
|
+
'|',
|
|
155
|
+
{
|
|
156
|
+
name: 'guide',
|
|
157
|
+
action: 'https://simplemde.com/markdown-guide',
|
|
158
|
+
className: 'mdi mdi-help-circle text-success',
|
|
159
|
+
title: messages.mdeGuide,
|
|
160
|
+
noDisable: true
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
...props.modelValue.options.easyMDEOptions
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (easymde) easymde.toTextArea()
|
|
167
|
+
// @ts-ignore
|
|
168
|
+
easymde = new EasyMDE(config)
|
|
169
|
+
|
|
170
|
+
let changed = false
|
|
171
|
+
easymde.codemirror.on('change', () => {
|
|
172
|
+
changed = true
|
|
173
|
+
if (easymde) props.statefulLayout.input(props.modelValue, easymde.value())
|
|
174
|
+
})
|
|
175
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
176
|
+
let blurTimeout = null
|
|
177
|
+
easymde.codemirror.on('blur', () => {
|
|
178
|
+
// timeout to prevent triggering save when clicking on a menu button
|
|
179
|
+
blurTimeout = setTimeout(() => {
|
|
180
|
+
if (changed) props.statefulLayout.blur(props.modelValue)
|
|
181
|
+
changed = false
|
|
182
|
+
}, 500)
|
|
183
|
+
})
|
|
184
|
+
easymde.codemirror.on('focus', () => {
|
|
185
|
+
if (blurTimeout) clearTimeout(blurTimeout)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
if (props.modelValue.autofocus) {
|
|
189
|
+
easymde.codemirror.focus()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
onMounted(initEasyMDE)
|
|
194
|
+
|
|
195
|
+
onUnmounted(() => {
|
|
196
|
+
if (easymde) easymde.toTextArea()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// update data from outside
|
|
200
|
+
watch(() => props.modelValue, () => {
|
|
201
|
+
if (easymde && (easymde.value() !== props.modelValue.data ?? '')) {
|
|
202
|
+
easymde.value(props.modelValue.data ?? '')
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// update easymde config from outside
|
|
207
|
+
watch(() => [props.modelValue.messages, props.modelValue.options.easyMDEOptions], (newValues, oldValues) => {
|
|
208
|
+
if (newValues[0] !== oldValues[0] || newValues[1] !== oldValues[1]) {
|
|
209
|
+
initEasyMDE()
|
|
210
|
+
}
|
|
211
|
+
})
|
|
23
212
|
|
|
24
|
-
|
|
25
|
-
|
|
213
|
+
props.statefulLayout.events.on('autofocus', () => {
|
|
214
|
+
if (props.modelValue.autofocus && easymde) {
|
|
215
|
+
easymde.codemirror.focus()
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
return () => h(VInput, fieldProps.value, fieldSlots.value)
|
|
26
220
|
}
|
|
27
221
|
})
|
|
28
222
|
|
|
29
223
|
</script>
|
|
224
|
+
|
|
225
|
+
<style>
|
|
226
|
+
.vjsf-node-markdown .v-input--density-compact .editor-toolbar {
|
|
227
|
+
padding: 0;
|
|
228
|
+
}
|
|
229
|
+
.vjsf-node-markdown .v-input--density-comfortable .editor-toolbar {
|
|
230
|
+
padding: 4px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.vjsf-node-markdown .v-input--density-compact .CodeMirror-wrap {
|
|
234
|
+
padding-top: 2px;
|
|
235
|
+
padding-bottom: 2px;
|
|
236
|
+
}
|
|
237
|
+
.vjsf-node-markdown .v-input--density-comfortable .CodeMirror-wrap {
|
|
238
|
+
padding-top: 6px;
|
|
239
|
+
padding-bottom: 6px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
</style>
|
|
@@ -19,7 +19,7 @@ export default defineComponent({
|
|
|
19
19
|
},
|
|
20
20
|
setup (props) {
|
|
21
21
|
const fieldProps = computed(() => {
|
|
22
|
-
const fieldProps = getInputProps(props.modelValue, props.statefulLayout, ['step', 'min', 'max'])
|
|
22
|
+
const fieldProps = getInputProps(props.modelValue, props.statefulLayout, ['step', 'min', 'max', 'placeholder'])
|
|
23
23
|
fieldProps.type = 'number'
|
|
24
24
|
fieldProps['onUpdate:modelValue'] = (/** @type string */value) => props.statefulLayout.input(props.modelValue, value && Number(value))
|
|
25
25
|
return fieldProps
|
|
@@ -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
|
+
computed(() => 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
|
+
}
|