@koumoul/vjsf 3.0.0-alpha.0 → 3.0.0-alpha.10

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 (129) hide show
  1. package/package.json +37 -10
  2. package/src/compat/v2.js +45 -10
  3. package/src/compile/index.js +42 -9
  4. package/src/compile/options.js +19 -0
  5. package/src/compile/v-jsf-compiled.vue.ejs +35 -57
  6. package/src/components/fragments/help-message.vue +49 -0
  7. package/src/components/fragments/node-slot.vue +3 -3
  8. package/src/components/fragments/section-header.vue +6 -2
  9. package/src/components/fragments/select-item-icon.vue +28 -0
  10. package/src/components/fragments/select-item.vue +43 -0
  11. package/src/components/fragments/select-selection.vue +35 -0
  12. package/src/components/fragments/text-field-menu.vue +4 -4
  13. package/src/components/node.vue +35 -9
  14. package/src/components/nodes/autocomplete.vue +88 -0
  15. package/src/components/nodes/checkbox.vue +3 -3
  16. package/src/components/nodes/color-picker.vue +3 -3
  17. package/src/components/nodes/combobox.vue +72 -0
  18. package/src/components/nodes/date-picker.vue +5 -6
  19. package/src/components/nodes/date-time-picker.vue +2 -2
  20. package/src/components/nodes/expansion-panels.vue +4 -4
  21. package/src/components/nodes/file-input.vue +43 -0
  22. package/src/components/nodes/list.vue +177 -91
  23. package/src/components/nodes/number-combobox.vue +72 -0
  24. package/src/components/nodes/number-field.vue +4 -6
  25. package/src/components/nodes/one-of-select.vue +4 -4
  26. package/src/components/nodes/section.vue +4 -3
  27. package/src/components/nodes/select.vue +72 -53
  28. package/src/components/nodes/slider.vue +3 -3
  29. package/src/components/nodes/stepper.vue +96 -0
  30. package/src/components/nodes/switch.vue +3 -3
  31. package/src/components/nodes/tabs.vue +4 -4
  32. package/src/components/nodes/text-field.vue +10 -5
  33. package/src/components/nodes/textarea.vue +26 -6
  34. package/src/components/nodes/vertical-tabs.vue +4 -4
  35. package/src/components/options.js +27 -2
  36. package/src/components/tree.vue +4 -3
  37. package/src/components/vjsf.vue +41 -103
  38. package/src/composables/use-dnd.js +69 -0
  39. package/src/composables/use-vjsf.js +122 -0
  40. package/src/index.js +3 -1
  41. package/src/styles/vjsf.css +14 -0
  42. package/src/{components/types.ts → types.ts} +26 -1
  43. package/src/utils/arrays.js +15 -0
  44. package/src/utils/build.js +1 -0
  45. package/src/utils/global-register.js +13 -0
  46. package/src/utils/index.js +5 -0
  47. package/src/utils/props.js +29 -10
  48. package/src/utils/slots.js +2 -3
  49. package/types/compat/v2.d.ts.map +1 -1
  50. package/types/compile/index.d.ts +3 -2
  51. package/types/compile/index.d.ts.map +1 -1
  52. package/types/compile/options.d.ts +4 -0
  53. package/types/compile/options.d.ts.map +1 -0
  54. package/types/components/fragments/help-message.vue.d.ts +8 -0
  55. package/types/components/fragments/help-message.vue.d.ts.map +1 -0
  56. package/types/components/fragments/node-slot.vue.d.ts +9 -9
  57. package/types/components/fragments/node-slot.vue.d.ts.map +1 -1
  58. package/types/components/fragments/section-header.vue.d.ts +3 -3
  59. package/types/components/fragments/select-item-icon.vue.d.ts +15 -0
  60. package/types/components/fragments/select-item-icon.vue.d.ts.map +1 -0
  61. package/types/components/fragments/select-item.vue.d.ts +12 -0
  62. package/types/components/fragments/select-item.vue.d.ts.map +1 -0
  63. package/types/components/fragments/select-selection.vue.d.ts +12 -0
  64. package/types/components/fragments/select-selection.vue.d.ts.map +1 -0
  65. package/types/components/fragments/text-field-menu.vue.d.ts +5 -5
  66. package/types/components/fragments/text-field-menu.vue.d.ts.map +1 -1
  67. package/types/components/global-register.d.ts +8 -0
  68. package/types/components/global-register.d.ts.map +1 -0
  69. package/types/components/node.vue.d.ts +5 -5
  70. package/types/components/nodes/autocomplete.vue.d.ts +27 -0
  71. package/types/components/nodes/autocomplete.vue.d.ts.map +1 -0
  72. package/types/components/nodes/checkbox.vue.d.ts +5 -5
  73. package/types/components/nodes/color-picker.vue.d.ts +5 -5
  74. package/types/components/nodes/combobox.vue.d.ts +27 -0
  75. package/types/components/nodes/combobox.vue.d.ts.map +1 -0
  76. package/types/components/nodes/date-picker.vue.d.ts +5 -5
  77. package/types/components/nodes/date-time-picker.vue.d.ts +5 -5
  78. package/types/components/nodes/expansion-panels.vue.d.ts +5 -5
  79. package/types/components/nodes/file-input.vue.d.ts +27 -0
  80. package/types/components/nodes/file-input.vue.d.ts.map +1 -0
  81. package/types/components/nodes/list.vue.d.ts +5 -5
  82. package/types/components/nodes/markdown.vue.d.ts +27 -0
  83. package/types/components/nodes/markdown.vue.d.ts.map +1 -0
  84. package/types/components/nodes/number-combobox.vue.d.ts +27 -0
  85. package/types/components/nodes/number-combobox.vue.d.ts.map +1 -0
  86. package/types/components/nodes/number-field.vue.d.ts +9 -9
  87. package/types/components/nodes/number-field.vue.d.ts.map +1 -1
  88. package/types/components/nodes/one-of-select.vue.d.ts +5 -5
  89. package/types/components/nodes/section.vue.d.ts +5 -5
  90. package/types/components/nodes/select.vue.d.ts +25 -8
  91. package/types/components/nodes/select.vue.d.ts.map +1 -1
  92. package/types/components/nodes/slider.vue.d.ts +5 -5
  93. package/types/components/nodes/stepper.vue.d.ts +10 -0
  94. package/types/components/nodes/stepper.vue.d.ts.map +1 -0
  95. package/types/components/nodes/switch.vue.d.ts +5 -5
  96. package/types/components/nodes/tabs.vue.d.ts +5 -5
  97. package/types/components/nodes/text-field.vue.d.ts +9 -9
  98. package/types/components/nodes/text-field.vue.d.ts.map +1 -1
  99. package/types/components/nodes/textarea.vue.d.ts +9 -9
  100. package/types/components/nodes/textarea.vue.d.ts.map +1 -1
  101. package/types/components/nodes/time-picker.vue.d.ts +1 -1
  102. package/types/components/nodes/vertical-tabs.vue.d.ts +5 -5
  103. package/types/components/options.d.ts +3 -2
  104. package/types/components/options.d.ts.map +1 -1
  105. package/types/components/tree.vue.d.ts +3 -3
  106. package/types/components/types.d.ts +22 -2
  107. package/types/components/types.d.ts.map +1 -1
  108. package/types/components/vjsf.vue.d.ts +8 -6
  109. package/types/composables/use-dnd.d.ts +25 -0
  110. package/types/composables/use-dnd.d.ts.map +1 -0
  111. package/types/composables/use-vjsf.d.ts +16 -0
  112. package/types/composables/use-vjsf.d.ts.map +1 -0
  113. package/types/index.d.ts +3 -1
  114. package/types/index.d.ts.map +1 -1
  115. package/types/types.d.ts +96 -0
  116. package/types/types.d.ts.map +1 -0
  117. package/types/utils/arrays.d.ts +9 -0
  118. package/types/utils/arrays.d.ts.map +1 -0
  119. package/types/utils/build.d.ts +2 -0
  120. package/types/utils/build.d.ts.map +1 -0
  121. package/types/utils/global-register.d.ts +8 -0
  122. package/types/utils/global-register.d.ts.map +1 -0
  123. package/types/utils/index.d.ts +6 -0
  124. package/types/utils/index.d.ts.map +1 -0
  125. package/types/utils/props.d.ts +8 -5
  126. package/types/utils/props.d.ts.map +1 -1
  127. package/types/utils/slots.d.ts +3 -3
  128. package/types/utils/slots.d.ts.map +1 -1
  129. package/src/utils/clone.js +0 -3
@@ -0,0 +1,96 @@
1
+ <script setup>
2
+ import { ref, computed } from 'vue'
3
+ import { VStepper, VStepperHeader, VStepperItem, VStepperWindow, VStepperWindowItem, VStepperActions, VContainer, VRow, VSpacer, VBtn, VDivider } 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('../../types.js').VjsfStatefulLayout> */
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
+ const child = props.modelValue.children[step.value]
30
+ props.statefulLayout.validateNodeRecurse(child)
31
+ if (!(child.error || child.childError)) step.value++
32
+ }
33
+ </script>
34
+
35
+ <template>
36
+ <section-header :node="modelValue" />
37
+ <v-stepper v-model="step">
38
+ <v-stepper-header>
39
+ <template
40
+ v-for="(child, i) of modelValue.children"
41
+ :key="child.key"
42
+ >
43
+ <v-stepper-item
44
+ :value="i"
45
+ :title="/** @type {string | undefined} */(child.layout.title ?? child.layout.label)"
46
+ :error="child.validated && !!(child.error || child.childError)"
47
+ :complete="child.validated && !(child.error || child.childError)"
48
+ :editable="i <= firstErrorIndex"
49
+ />
50
+ <v-divider />
51
+ </template>
52
+ </v-stepper-header>
53
+ <v-stepper-window>
54
+ <v-stepper-window-item
55
+ v-for="(child) of modelValue.children"
56
+ :key="child.key"
57
+ >
58
+ <v-container
59
+ fluid
60
+ class="pa-0"
61
+ >
62
+ <v-row>
63
+ <node
64
+ v-for="grandChild of isSection(child) ? child.children : [child]"
65
+ :key="grandChild.fullKey"
66
+ :model-value="/** @type import('../../types.js').VjsfNode */(grandChild)"
67
+ :stateful-layout="statefulLayout"
68
+ />
69
+ </v-row>
70
+ </v-container>
71
+ </v-stepper-window-item>
72
+ </v-stepper-window>
73
+ <v-stepper-actions>
74
+ <template #prev>
75
+ <v-btn
76
+ v-if="step > 0"
77
+ variant="text"
78
+ @click="step--"
79
+ >
80
+ Back
81
+ </v-btn>
82
+ </template>
83
+ <template #next>
84
+ <v-spacer />
85
+ <v-btn
86
+ v-if="step < modelValue.children.length - 1"
87
+ variant="flat"
88
+ color="primary"
89
+ @click="goNext"
90
+ >
91
+ Next
92
+ </v-btn>
93
+ </template>
94
+ </v-stepper-actions>
95
+ </v-stepper>
96
+ </template>
@@ -1,16 +1,16 @@
1
1
  <script setup>
2
2
  import { VSwitch } from 'vuetify/components'
3
3
  import { computed } from 'vue'
4
- import { getInputProps } from '../../utils/props.js'
4
+ import { getInputProps } from '../../utils/index.js'
5
5
 
6
6
  const props = defineProps({
7
7
  modelValue: {
8
- /** @type import('vue').PropType<import('../types.js').VjsfSwitchNode> */
8
+ /** @type import('vue').PropType<import('../../types.js').VjsfSwitchNode> */
9
9
  type: Object,
10
10
  required: true
11
11
  },
12
12
  statefulLayout: {
13
- /** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
13
+ /** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
14
14
  type: Object,
15
15
  required: true
16
16
  }
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { VTabs, VTab, VContainer } from 'vuetify/components'
2
+ import { VTabs, VTab, VContainer, VSheet, VWindow, VWindowItem, VRow, VIcon } from 'vuetify/components'
3
3
  import { ref } from 'vue'
4
4
  import { isSection } from '@json-layout/core'
5
5
  import Node from '../node.vue'
@@ -7,12 +7,12 @@ import SectionHeader from '../fragments/section-header.vue'
7
7
 
8
8
  defineProps({
9
9
  modelValue: {
10
- /** @type import('vue').PropType<import('../types.js').VjsfTabsNode> */
10
+ /** @type import('vue').PropType<import('../../types.js').VjsfTabsNode> */
11
11
  type: Object,
12
12
  required: true
13
13
  },
14
14
  statefulLayout: {
15
- /** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
15
+ /** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
16
16
  type: Object,
17
17
  required: true
18
18
  }
@@ -52,7 +52,7 @@ const tab = ref(0)
52
52
  <node
53
53
  v-for="grandChild of isSection(child) ? child.children : [child]"
54
54
  :key="grandChild.fullKey"
55
- :model-value="/** @type import('../types.js').VjsfNode */(grandChild)"
55
+ :model-value="/** @type import('../../types.js').VjsfNode */(grandChild)"
56
56
  :stateful-layout="statefulLayout"
57
57
  />
58
58
  </v-row>
@@ -1,24 +1,23 @@
1
1
  <script>
2
2
  import { defineComponent, h, computed } from 'vue'
3
3
  import { VTextField } from 'vuetify/components'
4
- import { getInputProps } from '../../utils/props.js'
5
- import { getCompSlots } from '../../utils/slots.js'
4
+ import { getInputProps, getCompSlots } from '../../utils/index.js'
6
5
 
7
6
  export default defineComponent({
8
7
  props: {
9
8
  modelValue: {
10
- /** @type import('vue').PropType<import('../types.js').VjsfTextFieldNode> */
9
+ /** @type import('vue').PropType<import('../../types.js').VjsfTextFieldNode> */
11
10
  type: Object,
12
11
  required: true
13
12
  },
14
13
  statefulLayout: {
15
- /** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
14
+ /** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
16
15
  type: Object,
17
16
  required: true
18
17
  }
19
18
  },
20
19
  setup (props) {
21
- const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout))
20
+ const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout, ['placeholder']))
22
21
  const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
23
22
 
24
23
  // @ts-ignore
@@ -27,3 +26,9 @@ export default defineComponent({
27
26
  })
28
27
 
29
28
  </script>
29
+
30
+ <style>
31
+ .vjsf-node-text-field.vjsf-readonly.vjsf-summary input {
32
+ text-overflow: ellipsis;
33
+ }
34
+ </style>
@@ -1,29 +1,49 @@
1
1
  <script>
2
- import { defineComponent, h, computed } from 'vue'
2
+ import { defineComponent, h, computed, ref, watch } from 'vue'
3
3
  import { VTextarea } from 'vuetify/components'
4
- import { getInputProps } from '../../utils/props.js'
5
- import { getCompSlots } from '../../utils/slots.js'
4
+ import { getInputProps, getCompSlots } from '../../utils/index.js'
6
5
 
7
6
  export default defineComponent({
8
7
  props: {
9
8
  modelValue: {
10
- /** @type import('vue').PropType<import('../types.js').VjsfTextareaNode> */
9
+ /** @type import('vue').PropType<import('../../types.js').VjsfTextareaNode> */
11
10
  type: Object,
12
11
  required: true
13
12
  },
14
13
  statefulLayout: {
15
- /** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
14
+ /** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
16
15
  type: Object,
17
16
  required: true
18
17
  }
19
18
  },
20
19
  setup (props) {
21
- const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout))
20
+ /** @type {import('vue').Ref<null | HTMLElement>} */
21
+ const textarea = ref(null)
22
+
23
+ const fieldProps = computed(() => {
24
+ const inputProps = getInputProps(props.modelValue, props.statefulLayout, ['placeholder'])
25
+ inputProps.ref = textarea
26
+ if (props.modelValue.options.readOnly && props.modelValue.options.summary) inputProps.rows = 3
27
+ return inputProps
28
+ })
22
29
  const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
23
30
 
31
+ watch(() => props.modelValue.options.readOnly, (readOnly) => {
32
+ if (readOnly && textarea.value) {
33
+ textarea.value.scrollTop = 0
34
+ }
35
+ })
36
+
24
37
  // @ts-ignore
25
38
  return () => h(VTextarea, fieldProps.value, fieldSlots.value)
26
39
  }
27
40
  })
28
41
 
29
42
  </script>
43
+
44
+ <style>
45
+ .vjsf-node-textarea.vjsf-readonly.vjsf-summary textarea {
46
+ overflow: hidden;
47
+ mask-image: linear-gradient(180deg, #000 66%, transparent 90%);
48
+ }
49
+ </style>
@@ -1,18 +1,18 @@
1
1
  <script setup>
2
2
  import { isSection } from '@json-layout/core'
3
- import { VTabs, VTab, VContainer } from 'vuetify/components'
3
+ import { VTabs, VTab, VContainer, VSheet, VWindow, VWindowItem, VRow, VIcon } from 'vuetify/components'
4
4
  import { ref } from 'vue'
5
5
  import Node from '../node.vue'
6
6
  import SectionHeader from '../fragments/section-header.vue'
7
7
 
8
8
  defineProps({
9
9
  modelValue: {
10
- /** @type import('vue').PropType<import('../types.js').VjsfVerticalTabsNode> */
10
+ /** @type import('vue').PropType<import('../../types.js').VjsfVerticalTabsNode> */
11
11
  type: Object,
12
12
  required: true
13
13
  },
14
14
  statefulLayout: {
15
- /** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
15
+ /** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
16
16
  type: Object,
17
17
  required: true
18
18
  }
@@ -58,7 +58,7 @@ const tab = ref(0)
58
58
  <node
59
59
  v-for="grandChild of isSection(child) ? child.children : [child]"
60
60
  :key="grandChild.fullKey"
61
- :model-value="/** @type import('../types.js').VjsfNode */(grandChild)"
61
+ :model-value="/** @type import('../../types.js').VjsfNode */(grandChild)"
62
62
  :stateful-layout="statefulLayout"
63
63
  />
64
64
  </v-row>
@@ -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',
@@ -12,6 +12,7 @@ export const defaultOptions = {
12
12
  density: 'comfortable'
13
13
  },
14
14
  fieldPropsReadOnly: { hideDetails: 'auto', variant: 'plain' },
15
+ fieldPropsSummary: { hideDetails: true },
15
16
  textfieldProps: {},
16
17
  textfieldPropsReadOnly: {},
17
18
  textareaProps: {},
@@ -21,5 +22,29 @@ export const defaultOptions = {
21
22
  checkboxPropsReadOnly: {},
22
23
  switchProps: { hideDetails: 'auto' },
23
24
  switchPropsReadOnly: {},
24
- errorAlertProps: { type: 'error', variant: 'tonal' }
25
+ errorAlertProps: { type: 'error', variant: 'tonal' },
26
+ easyMDEOptions: {},
27
+ nodeComponents: {}
28
+ }
29
+
30
+ /**
31
+ *
32
+ * @param {Partial<import("../types.js").VjsfOptions> | null} options
33
+ * @param {any} form
34
+ * @param {number} width
35
+ * @param {import("vue").Slots} slots
36
+ * @param {Record<string, import('vue').Component>} nodeComponents
37
+ * @returns
38
+ */
39
+ export const getFullOptions = (options, form, width, slots, nodeComponents) => {
40
+ const fullOptions = {
41
+ ...defaultOptions,
42
+ readOnly: !!(form && (form.isDisabled.value || form.isReadonly.value)),
43
+ ...options,
44
+ context: options?.context ? JSON.parse(JSON.stringify(options.context)) : {},
45
+ width: Math.round(width ?? 0),
46
+ vjsfSlots: { ...slots },
47
+ nodeComponents: { ...nodeComponents, ...options?.nodeComponents }
48
+ }
49
+ return /** @type import('../types.js').VjsfOptions */ (fullOptions)
25
50
  }
@@ -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({
@@ -8,7 +9,7 @@ defineProps({
8
9
  required: true
9
10
  },
10
11
  statefulLayout: {
11
- /** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
12
+ /** @type import('vue').PropType<import('../types.js').VjsfStatefulLayout> */
12
13
  type: Object,
13
14
  required: true
14
15
  }
@@ -16,10 +17,10 @@ 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
- :model-value="/** @type import('./types.js').VjsfNode */(modelValue.root)"
23
+ :model-value="/** @type import('../types.js').VjsfNode */(modelValue.root)"
23
24
  />
24
25
  </v-row>
25
26
  </template>
@@ -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 } 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,14 +17,19 @@ 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 NodeFileInput from './nodes/file-input.vue'
24
30
 
25
- const comps = {
31
+ /** @type {Record<string, import('vue').Component>} */
32
+ const nodeComponents = {
26
33
  section: NodeSection,
27
34
  'text-field': NodeTextField,
28
35
  textarea: NodeTextarea,
@@ -34,18 +41,16 @@ const comps = {
34
41
  'date-time-picker': NodeDateTimePicker,
35
42
  'color-picker': NodeColorPicker,
36
43
  select: NodeSelect,
44
+ autocomplete: NodeAutocomplete,
37
45
  'one-of-select': NodeOneOfSelect,
38
46
  tabs: NodeTabs,
39
47
  'vertical-tabs': NodeVerticalTabs,
40
48
  'expansion-panels': NodeExpansionPanels,
41
- list: NodeList
42
- }
43
-
44
- const instance = getCurrentInstance()
45
- for (const [name, comp] of Object.entries(comps)) {
46
- if (!instance?.appContext.app.component(`vjsf-node-${name}`)) {
47
- instance?.appContext.app.component(`vjsf-node-${name}`, comp)
48
- }
49
+ stepper: NodeStepper,
50
+ list: NodeList,
51
+ combobox: NodeCombobox,
52
+ 'number-combobox': NodeNumberCombobox,
53
+ 'file-input': NodeFileInput
49
54
  }
50
55
 
51
56
  const props = defineProps({
@@ -53,99 +58,41 @@ const props = defineProps({
53
58
  type: Object,
54
59
  required: true
55
60
  },
61
+ precompiledLayout: {
62
+ /** @type import('vue').PropType<import('@json-layout/core').CompiledLayout> */
63
+ type: Object,
64
+ default: null
65
+ },
56
66
  modelValue: {
57
- type: [Object, String, Number, Boolean],
67
+ type: null,
58
68
  default: null
59
69
  },
60
70
  options: {
61
- /** @type import('vue').PropType<Partial<Omit<import('./types.js').VjsfOptions, 'width'>>> */
71
+ /** @type import('vue').PropType<import('../types.js').PartialVjsfOptions | null> */
62
72
  type: Object,
63
- required: true
64
- }
65
- })
66
-
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
- }
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()
73
+ default: null
134
74
  }
135
75
  })
136
76
 
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
- })
77
+ const emit = defineEmits(emits)
141
78
 
142
- // case where schema is updated from outside
143
- watch(compiledLayout, (newCompiledLayout) => initStatefulLayout())
79
+ const { el, statefulLayout, stateTree } = useVjsf(
80
+ computed(() => props.schema),
81
+ computed(() => props.modelValue),
82
+ computed(() => props.options),
83
+ nodeComponents,
84
+ emit,
85
+ compile,
86
+ computed(() => props.precompiledLayout)
87
+ )
144
88
 
145
89
  </script>
146
90
 
147
91
  <template>
148
- <div ref="el">
92
+ <div
93
+ ref="el"
94
+ class="vjsf"
95
+ >
149
96
  <tree
150
97
  v-if="statefulLayout && stateTree"
151
98
  :model-value="stateTree"
@@ -155,14 +102,5 @@ watch(compiledLayout, (newCompiledLayout) => initStatefulLayout())
155
102
  </template>
156
103
 
157
104
  <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
- }
105
+ /* nothing here, use ../styles/vjsf.css */
168
106
  </style>
@@ -0,0 +1,69 @@
1
+ import { shallowRef, ref, computed } from 'vue'
2
+ import { moveArrayItem } from '../utils/index.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 hovered = ref(-1)
22
+ const draggable = ref(-1)
23
+ const dragging = ref(-1)
24
+
25
+ hovered.value = 1
26
+
27
+ const itemBind = (/** @type {number} */itemIndex) => ({
28
+ // hover the item
29
+ onMouseenter: () => {
30
+ hovered.value = itemIndex
31
+ },
32
+ onMouseleave: () => {
33
+ hovered.value = -1
34
+ },
35
+
36
+ // drag the item
37
+ onDragstart: () => {
38
+ dragging.value = itemIndex
39
+ },
40
+ onDragover: () => {
41
+ sortableArray.value = moveArrayItem(sortableArray.value, dragging.value, itemIndex)
42
+ dragging.value = itemIndex
43
+ },
44
+ onDragend: () => {
45
+ dragging.value = -1
46
+ callback()
47
+ }
48
+ })
49
+
50
+ const handleBind = (/** @type {number} */itemIndex) => ({
51
+ // hover the handle
52
+ onMouseover () {
53
+ draggable.value = itemIndex
54
+ },
55
+ onMouseout () {
56
+ draggable.value = -1
57
+ }
58
+ })
59
+
60
+ return {
61
+ activeDnd,
62
+ sortableArray,
63
+ hovered,
64
+ draggable,
65
+ dragging,
66
+ itemBind,
67
+ handleBind
68
+ }
69
+ }