@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.
Files changed (69) hide show
  1. package/package.json +10 -4
  2. package/src/compat/v2.js +44 -6
  3. package/src/compile/index.js +25 -6
  4. package/src/compile/v-jsf-compiled.vue.ejs +14 -52
  5. package/src/components/fragments/help-message.vue +48 -0
  6. package/src/components/fragments/section-header.vue +4 -1
  7. package/src/components/fragments/select-item-icon.vue +28 -0
  8. package/src/components/fragments/select-item.vue +43 -0
  9. package/src/components/fragments/select-selection.vue +35 -0
  10. package/src/components/fragments/text-field-menu.vue +1 -1
  11. package/src/components/node.vue +10 -1
  12. package/src/components/nodes/autocomplete.vue +95 -0
  13. package/src/components/nodes/combobox.vue +73 -0
  14. package/src/components/nodes/date-picker.vue +2 -2
  15. package/src/components/nodes/list.vue +170 -86
  16. package/src/components/nodes/markdown.vue +233 -0
  17. package/src/components/nodes/number-combobox.vue +73 -0
  18. package/src/components/nodes/number-field.vue +1 -2
  19. package/src/components/nodes/select.vue +70 -50
  20. package/src/components/nodes/stepper.vue +98 -0
  21. package/src/components/nodes/text-field.vue +1 -1
  22. package/src/components/nodes/textarea.vue +1 -1
  23. package/src/components/options.js +23 -2
  24. package/src/components/tree.vue +2 -1
  25. package/src/components/types.ts +8 -0
  26. package/src/components/vjsf.vue +38 -94
  27. package/src/composables/use-dnd.js +54 -0
  28. package/src/composables/use-vjsf.js +115 -0
  29. package/src/index.js +2 -1
  30. package/src/styles/vjsf.css +10 -0
  31. package/src/utils/arrays.js +15 -0
  32. package/src/utils/props.js +25 -6
  33. package/types/compat/v2.d.ts.map +1 -1
  34. package/types/compile/index.d.ts.map +1 -1
  35. package/types/components/fragments/help-message.vue.d.ts +8 -0
  36. package/types/components/fragments/help-message.vue.d.ts.map +1 -0
  37. package/types/components/fragments/select-item-icon.vue.d.ts +15 -0
  38. package/types/components/fragments/select-item-icon.vue.d.ts.map +1 -0
  39. package/types/components/fragments/select-item.vue.d.ts +12 -0
  40. package/types/components/fragments/select-item.vue.d.ts.map +1 -0
  41. package/types/components/fragments/select-selection.vue.d.ts +12 -0
  42. package/types/components/fragments/select-selection.vue.d.ts.map +1 -0
  43. package/types/components/nodes/autocomplete.vue.d.ts +27 -0
  44. package/types/components/nodes/autocomplete.vue.d.ts.map +1 -0
  45. package/types/components/nodes/combobox.vue.d.ts +27 -0
  46. package/types/components/nodes/combobox.vue.d.ts.map +1 -0
  47. package/types/components/nodes/markdown.vue.d.ts +27 -0
  48. package/types/components/nodes/markdown.vue.d.ts.map +1 -0
  49. package/types/components/nodes/number-combobox.vue.d.ts +27 -0
  50. package/types/components/nodes/number-combobox.vue.d.ts.map +1 -0
  51. package/types/components/nodes/select.vue.d.ts +25 -8
  52. package/types/components/nodes/select.vue.d.ts.map +1 -1
  53. package/types/components/nodes/stepper.vue.d.ts +10 -0
  54. package/types/components/nodes/stepper.vue.d.ts.map +1 -0
  55. package/types/components/options.d.ts +3 -2
  56. package/types/components/options.d.ts.map +1 -1
  57. package/types/components/types.d.ts +9 -1
  58. package/types/components/types.d.ts.map +1 -1
  59. package/types/components/vjsf.vue.d.ts +5 -3
  60. package/types/composables/use-dnd.d.ts +21 -0
  61. package/types/composables/use-dnd.d.ts.map +1 -0
  62. package/types/composables/use-vjsf.d.ts +17 -0
  63. package/types/composables/use-vjsf.d.ts.map +1 -0
  64. package/types/index.d.ts +2 -1
  65. package/types/index.d.ts.map +1 -1
  66. package/types/utils/arrays.d.ts +9 -0
  67. package/types/utils/arrays.d.ts.map +1 -0
  68. package/types/utils/props.d.ts +6 -3
  69. package/types/utils/props.d.ts.map +1 -1
@@ -1,59 +1,79 @@
1
- <script setup>
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
- const props = defineProps({
7
- modelValue: {
9
+ export default defineComponent({
10
+ props: {
11
+ modelValue: {
8
12
  /** @type import('vue').PropType<import('../types.js').VjsfSelectNode> */
9
- type: Object,
10
- required: true
11
- },
12
- statefulLayout: {
13
+ type: Object,
14
+ required: true
15
+ },
16
+ statefulLayout: {
13
17
  /** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
14
- type: Object,
15
- required: true
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
- const fieldProps = getInputProps(props.modelValue, props.statefulLayout)
21
- if (props.modelValue.options.readOnly) fieldProps.menuProps = { modelValue: false }
22
- return fieldProps
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('vue').Ref<import('@json-layout/vocabulary').SelectItems> */
26
- const items = ref(props.modelValue.layout.items ?? [])
27
- /** @type import('vue').Ref<boolean> */
28
- const loading = ref(false)
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
- </script>
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
- <template>
52
- <v-select
53
- v-bind="fieldProps"
54
- :loading="loading"
55
- :items="items"
56
- @update:model-value="value => statefulLayout.input(modelValue, value)"
57
- @update:menu="refresh"
58
- />
59
- </template>
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 Partial<import("./types.js").VjsfOptions> */
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
  }
@@ -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)"
@@ -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}
@@ -1,8 +1,10 @@
1
1
  <script setup>
2
- import { ref, shallowRef, computed, getCurrentInstance, watch, useSlots, inject } from 'vue'
3
- import { useElementSize } from '@vueuse/core'
4
- import { StatefulLayout, compile } from '@json-layout/core'
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 { defaultOptions } from './options.js'
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
- list: NodeList
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: [Object, String, Number, Boolean],
73
+ type: null,
58
74
  default: null
59
75
  },
60
76
  options: {
61
- /** @type import('vue').PropType<Partial<Omit<import('./types.js').VjsfOptions, 'width'>>> */
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(['update:modelValue', 'update:state'])
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 = ref(null)
86
- const { width } = useElementSize(el)
87
-
88
- const slots = useSlots()
89
-
90
- /** @type import('vue').ComputedRef<import('./types.js').VjsfOptions> */
91
- const fullOptions = computed(() => {
92
- const options = {
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 ref="el">
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
- /* override vuetify styles to manage readOnly fields more usable than the default disabled fields */
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
+ }