@koumoul/vjsf 3.0.0-beta.9 → 3.0.1

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 (162) hide show
  1. package/README.md +21 -0
  2. package/package.json +13 -20
  3. package/src/compat/v2.js +126 -27
  4. package/src/components/fragments/child-subtitle.vue +25 -0
  5. package/src/components/fragments/help-message.vue +33 -8
  6. package/src/components/fragments/section-header.vue +9 -7
  7. package/src/components/fragments/select-item-icon.vue +2 -2
  8. package/src/components/fragments/select-item.vue +2 -1
  9. package/src/components/fragments/select-selection.vue +2 -1
  10. package/src/components/fragments/selection-group.vue +105 -0
  11. package/src/components/fragments/text-field-menu.vue +16 -7
  12. package/src/components/node.vue +58 -41
  13. package/src/components/nodes/autocomplete.vue +14 -60
  14. package/src/components/nodes/card.vue +39 -0
  15. package/src/components/nodes/checkbox-group.vue +39 -0
  16. package/src/components/nodes/checkbox.vue +31 -26
  17. package/src/components/nodes/color-picker.vue +10 -5
  18. package/src/components/nodes/combobox.vue +17 -40
  19. package/src/components/nodes/date-picker.vue +33 -12
  20. package/src/components/nodes/date-time-picker.vue +86 -3
  21. package/src/components/nodes/expansion-panels.vue +17 -9
  22. package/src/components/nodes/file-input.vue +15 -11
  23. package/src/components/nodes/list.vue +246 -112
  24. package/src/components/nodes/number-combobox.vue +18 -39
  25. package/src/components/nodes/number-field.vue +17 -11
  26. package/src/components/nodes/one-of-select.vue +53 -27
  27. package/src/components/nodes/radio-group.vue +58 -0
  28. package/src/components/nodes/section.vue +4 -1
  29. package/src/components/nodes/select.vue +15 -54
  30. package/src/components/nodes/slider.vue +32 -29
  31. package/src/components/nodes/stepper.vue +10 -2
  32. package/src/components/nodes/switch-group.vue +39 -0
  33. package/src/components/nodes/switch.vue +31 -26
  34. package/src/components/nodes/tabs.vue +20 -8
  35. package/src/components/nodes/text-field.vue +10 -7
  36. package/src/components/nodes/textarea.vue +20 -12
  37. package/src/components/nodes/time-picker.vue +45 -1
  38. package/src/components/nodes/vertical-tabs.vue +16 -6
  39. package/src/components/tree.vue +1 -1
  40. package/src/components/vjsf.vue +11 -1
  41. package/src/composables/use-comp-defaults.js +19 -0
  42. package/src/composables/use-dnd.js +2 -1
  43. package/src/composables/use-get-items.js +53 -0
  44. package/src/composables/use-node.js +136 -0
  45. package/src/composables/use-select-node.js +67 -0
  46. package/src/composables/use-vjsf.js +72 -51
  47. package/src/index.js +5 -2
  48. package/src/options.js +67 -0
  49. package/src/types.ts +64 -33
  50. package/src/utils/arrays.js +37 -6
  51. package/types/compat/v2.d.ts.map +1 -1
  52. package/types/compile/index.d.ts +2 -2
  53. package/types/compile/index.d.ts.map +1 -1
  54. package/types/compile/options.d.ts +3 -2
  55. package/types/compile/options.d.ts.map +1 -1
  56. package/types/components/fragments/child-subtitle.vue.d.ts +8 -0
  57. package/types/components/fragments/child-subtitle.vue.d.ts.map +1 -0
  58. package/types/components/fragments/help-message.vue.d.ts +2 -2
  59. package/types/components/fragments/node-slot.vue.d.ts +2 -44
  60. package/types/components/fragments/node-slot.vue.d.ts.map +1 -1
  61. package/types/components/fragments/section-header.vue.d.ts +4 -2
  62. package/types/components/fragments/select-item-icon.vue.d.ts +2 -12
  63. package/types/components/fragments/select-item.vue.d.ts +2 -2
  64. package/types/components/fragments/select-selection.vue.d.ts +2 -2
  65. package/types/components/fragments/selection-group.vue.d.ts +5 -0
  66. package/types/components/fragments/selection-group.vue.d.ts.map +1 -0
  67. package/types/components/fragments/text-field-menu.vue.d.ts +2 -2
  68. package/types/components/fragments/text-field-menu.vue.d.ts.map +1 -1
  69. package/types/components/node.vue.d.ts +2 -2
  70. package/types/components/nodes/autocomplete.vue.d.ts +2 -24
  71. package/types/components/nodes/autocomplete.vue.d.ts.map +1 -1
  72. package/types/components/nodes/card.vue.d.ts +10 -0
  73. package/types/components/nodes/card.vue.d.ts.map +1 -0
  74. package/types/components/nodes/checkbox-group.vue.d.ts +5 -0
  75. package/types/components/nodes/checkbox-group.vue.d.ts.map +1 -0
  76. package/types/components/nodes/checkbox.vue.d.ts +3 -8
  77. package/types/components/nodes/color-picker.vue.d.ts +2 -2
  78. package/types/components/nodes/combobox.vue.d.ts +2 -24
  79. package/types/components/nodes/combobox.vue.d.ts.map +1 -1
  80. package/types/components/nodes/date-picker.vue.d.ts +2 -2
  81. package/types/components/nodes/date-time-picker.vue.d.ts +4 -4
  82. package/types/components/nodes/expansion-panels.vue.d.ts +2 -2
  83. package/types/components/nodes/file-input.vue.d.ts +2 -24
  84. package/types/components/nodes/file-input.vue.d.ts.map +1 -1
  85. package/types/components/nodes/list.vue.d.ts +2 -2
  86. package/types/components/nodes/number-combobox.vue.d.ts +2 -24
  87. package/types/components/nodes/number-combobox.vue.d.ts.map +1 -1
  88. package/types/components/nodes/number-field.vue.d.ts +2 -24
  89. package/types/components/nodes/number-field.vue.d.ts.map +1 -1
  90. package/types/components/nodes/one-of-select.vue.d.ts +2 -2
  91. package/types/components/nodes/radio-group.vue.d.ts +5 -0
  92. package/types/components/nodes/radio-group.vue.d.ts.map +1 -0
  93. package/types/components/nodes/section.vue.d.ts +2 -2
  94. package/types/components/nodes/select.vue.d.ts +2 -24
  95. package/types/components/nodes/select.vue.d.ts.map +1 -1
  96. package/types/components/nodes/slider.vue.d.ts +3 -8
  97. package/types/components/nodes/stepper.vue.d.ts +2 -2
  98. package/types/components/nodes/switch-group.vue.d.ts +5 -0
  99. package/types/components/nodes/switch-group.vue.d.ts.map +1 -0
  100. package/types/components/nodes/switch.vue.d.ts +3 -8
  101. package/types/components/nodes/tabs.vue.d.ts +2 -2
  102. package/types/components/nodes/text-field.vue.d.ts +2 -24
  103. package/types/components/nodes/text-field.vue.d.ts.map +1 -1
  104. package/types/components/nodes/textarea.vue.d.ts +2 -24
  105. package/types/components/nodes/textarea.vue.d.ts.map +1 -1
  106. package/types/components/nodes/time-picker.vue.d.ts +8 -1
  107. package/types/components/nodes/vertical-tabs.vue.d.ts +2 -2
  108. package/types/components/options.d.ts +1 -1
  109. package/types/components/options.d.ts.map +1 -1
  110. package/types/components/tree.vue.d.ts +2 -2
  111. package/types/components/vjsf.vue.d.ts +5 -5
  112. package/types/composables/use-comp-defaults.d.ts +8 -0
  113. package/types/composables/use-comp-defaults.d.ts.map +1 -0
  114. package/types/composables/use-dnd.d.ts +3 -3
  115. package/types/composables/use-dnd.d.ts.map +1 -1
  116. package/types/composables/use-field-props.d.ts +30 -0
  117. package/types/composables/use-field-props.d.ts.map +1 -0
  118. package/types/composables/use-field.d.ts +31 -0
  119. package/types/composables/use-field.d.ts.map +1 -0
  120. package/types/composables/use-get-items.d.ts +12 -0
  121. package/types/composables/use-get-items.d.ts.map +1 -0
  122. package/types/composables/use-node.d.ts +32 -0
  123. package/types/composables/use-node.d.ts.map +1 -0
  124. package/types/composables/use-select-field.d.ts +21 -0
  125. package/types/composables/use-select-field.d.ts.map +1 -0
  126. package/types/composables/use-select-node.d.ts +27 -0
  127. package/types/composables/use-select-node.d.ts.map +1 -0
  128. package/types/composables/use-select-props.d.ts +21 -0
  129. package/types/composables/use-select-props.d.ts.map +1 -0
  130. package/types/composables/use-select.d.ts +21 -0
  131. package/types/composables/use-select.d.ts.map +1 -0
  132. package/types/composables/use-vjsf.d.ts +2 -2
  133. package/types/composables/use-vjsf.d.ts.map +1 -1
  134. package/types/iconsets/default-aliases.d.ts +10 -0
  135. package/types/iconsets/default-aliases.d.ts.map +1 -0
  136. package/types/iconsets/mdi-svg.d.ts +3 -0
  137. package/types/iconsets/mdi-svg.d.ts.map +1 -0
  138. package/types/iconsets/mdi.d.ts +3 -0
  139. package/types/iconsets/mdi.d.ts.map +1 -0
  140. package/types/index.d.ts +5 -2
  141. package/types/index.d.ts.map +1 -1
  142. package/types/options.d.ts +9 -0
  143. package/types/options.d.ts.map +1 -0
  144. package/types/types.d.ts +65 -33
  145. package/types/types.d.ts.map +1 -1
  146. package/types/utils/arrays.d.ts +17 -4
  147. package/types/utils/arrays.d.ts.map +1 -1
  148. package/types/utils/index.d.ts +0 -3
  149. package/types/utils/props.d.ts +7 -0
  150. package/types/utils/props.d.ts.map +1 -1
  151. package/types/utils/slots.d.ts +8 -0
  152. package/types/utils/slots.d.ts.map +1 -1
  153. package/src/compile/index.js +0 -65
  154. package/src/compile/options.js +0 -19
  155. package/src/compile/v-jsf-compiled.vue.ejs +0 -61
  156. package/src/components/options.js +0 -27
  157. package/src/utils/global-register.js +0 -13
  158. package/src/utils/index.js +0 -5
  159. package/src/utils/props.js +0 -107
  160. package/src/utils/slots.js +0 -18
  161. package/types/utils/global-register.d.ts +0 -8
  162. package/types/utils/global-register.d.ts.map +0 -1
@@ -1,7 +1,51 @@
1
1
  <script setup>
2
+ import TextFieldMenu from '../fragments/text-field-menu.vue'
3
+ import { VTimePicker } from 'vuetify/labs/VTimePicker'
4
+ import { VIcon } from 'vuetify/components/VIcon'
5
+ import { useDate, useDefaults } from 'vuetify'
6
+ import { computed, toRef } from 'vue'
7
+ import { getShortTime, getLongTime } from '../../utils/dates.js'
8
+ import useNode from '../../composables/use-node.js'
2
9
 
10
+ useDefaults({}, 'VjsfDatePicker')
11
+
12
+ const props = defineProps({
13
+ modelValue: {
14
+ /** @type import('vue').PropType<import('../../types.js').VjsfDatePickerNode> */
15
+ type: Object,
16
+ required: true
17
+ },
18
+ statefulLayout: {
19
+ /** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
20
+ type: Object,
21
+ required: true
22
+ }
23
+ })
24
+
25
+ const vDate = useDate()
26
+
27
+ const { compProps, localData } = useNode(toRef(props, 'modelValue'), props.statefulLayout)
28
+
29
+ const timePickerProps = computed(() => {
30
+ const timePickerProps = { ...compProps.value }
31
+ timePickerProps['ampm-in-title'] = true
32
+ if (localData.value) timePickerProps.modelValue = getShortTime(localData.value)
33
+ return timePickerProps
34
+ })
3
35
  </script>
4
36
 
5
37
  <template>
6
- TODO time
38
+ <text-field-menu
39
+ :model-value="props.modelValue"
40
+ :stateful-layout="statefulLayout"
41
+ :formatted-value="timePickerProps.modelValue && vDate.format('2010-04-13T' + timePickerProps.modelValue, 'fullTime')"
42
+ >
43
+ <template #prepend-inner>
44
+ <v-icon :icon="statefulLayout.options.icons.clock" />
45
+ </template>
46
+ <v-time-picker
47
+ v-bind="timePickerProps"
48
+ @update:model-value="value => {statefulLayout.input(props.modelValue, value && getLongTime(value))}"
49
+ />
50
+ </text-field-menu>
7
51
  </template>
@@ -1,9 +1,19 @@
1
1
  <script setup>
2
2
  import { isSection } from '@json-layout/core'
3
- import { VTabs, VTab, VContainer, VSheet, VWindow, VWindowItem, VRow, VIcon } from 'vuetify/components'
3
+ import { VTabs, VTab } from 'vuetify/components/VTabs'
4
+ import { VContainer, VRow } from 'vuetify/components/VGrid'
5
+ import { VIcon } from 'vuetify/components/VIcon'
6
+ import { VSheet } from 'vuetify/components/VSheet'
7
+ import { VWindow, VWindowItem } from 'vuetify/components/VWindow'
4
8
  import { ref } from 'vue'
5
9
  import Node from '../node.vue'
6
10
  import SectionHeader from '../fragments/section-header.vue'
11
+ import ChildSubtitle from '../fragments/child-subtitle.vue'
12
+ import { useDefaults } from 'vuetify'
13
+ import useCompDefaults from '../../composables/use-comp-defaults.js'
14
+
15
+ useDefaults({}, 'VjsfVerticalTabs')
16
+ const vSheetProps = useCompDefaults('VjsfVerticalTabs-VSheet', { border: true })
7
17
 
8
18
  defineProps({
9
19
  modelValue: {
@@ -23,7 +33,7 @@ const tab = ref(0)
23
33
 
24
34
  <template>
25
35
  <section-header :node="modelValue" />
26
- <v-sheet border>
36
+ <v-sheet v-bind="vSheetProps">
27
37
  <div class="d-flex flex-row">
28
38
  <v-tabs
29
39
  v-model="tab"
@@ -38,9 +48,8 @@ const tab = ref(0)
38
48
  <v-icon
39
49
  v-if="child.validated && (child.error || child.childError)"
40
50
  color="error"
41
- >
42
- mdi-alert
43
- </v-icon>
51
+ :icon="statefulLayout.options.icons.alert"
52
+ />
44
53
  {{ child.layout.title ?? child.layout.label }}
45
54
  </v-tab>
46
55
  </v-tabs>
@@ -54,7 +63,8 @@ const tab = ref(0)
54
63
  :value="i"
55
64
  >
56
65
  <v-container fluid>
57
- <v-row>
66
+ <child-subtitle :model-value="child" />
67
+ <v-row :dense="modelValue.options?.density === 'compact' || modelValue.options?.density === 'comfortable'">
58
68
  <node
59
69
  v-for="grandChild of isSection(child) ? child.children : [child]"
60
70
  :key="grandChild.fullKey"
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { VRow } from 'vuetify/components'
2
+ import { VRow } from 'vuetify/components/VGrid'
3
3
  import Node from './node.vue'
4
4
 
5
5
  defineProps({
@@ -14,10 +14,14 @@ import NodeSwitch from './nodes/switch.vue'
14
14
  import NodeNumberField from './nodes/number-field.vue'
15
15
  import NodeSlider from './nodes/slider.vue'
16
16
  import NodeDatePicker from './nodes/date-picker.vue'
17
+ import NodeTimePicker from './nodes/time-picker.vue'
17
18
  import NodeDateTimePicker from './nodes/date-time-picker.vue'
18
19
  import NodeColorPicker from './nodes/color-picker.vue'
19
20
  import NodeSelect from './nodes/select.vue'
20
21
  import NodeAutocomplete from './nodes/autocomplete.vue'
22
+ import NodeRadioGroup from './nodes/radio-group.vue'
23
+ import NodeCheckboxGroup from './nodes/checkbox-group.vue'
24
+ import NodeSwitchGroup from './nodes/switch-group.vue'
21
25
  import NodeOneOfSelect from './nodes/one-of-select.vue'
22
26
  import NodeTabs from './nodes/tabs.vue'
23
27
  import NodeVerticalTabs from './nodes/vertical-tabs.vue'
@@ -27,6 +31,7 @@ import NodeExpansionPanels from './nodes/expansion-panels.vue'
27
31
  import NodeStepper from './nodes/stepper.vue'
28
32
  import NodeList from './nodes/list.vue'
29
33
  import NodeFileInput from './nodes/file-input.vue'
34
+ import NodeCard from './nodes/card.vue'
30
35
 
31
36
  /** @type {Record<string, import('vue').Component>} */
32
37
  const nodeComponents = {
@@ -38,10 +43,14 @@ const nodeComponents = {
38
43
  'number-field': NodeNumberField,
39
44
  slider: NodeSlider,
40
45
  'date-picker': NodeDatePicker,
46
+ 'time-picker': NodeTimePicker,
41
47
  'date-time-picker': NodeDateTimePicker,
42
48
  'color-picker': NodeColorPicker,
43
49
  select: NodeSelect,
44
50
  autocomplete: NodeAutocomplete,
51
+ 'radio-group': NodeRadioGroup,
52
+ 'checkbox-group': NodeCheckboxGroup,
53
+ 'switch-group': NodeSwitchGroup,
45
54
  'one-of-select': NodeOneOfSelect,
46
55
  tabs: NodeTabs,
47
56
  'vertical-tabs': NodeVerticalTabs,
@@ -50,7 +59,8 @@ const nodeComponents = {
50
59
  list: NodeList,
51
60
  combobox: NodeCombobox,
52
61
  'number-combobox': NodeNumberCombobox,
53
- 'file-input': NodeFileInput
62
+ 'file-input': NodeFileInput,
63
+ card: NodeCard
54
64
  }
55
65
 
56
66
  const props = defineProps({
@@ -0,0 +1,19 @@
1
+ import { inject, computed } from 'vue'
2
+
3
+ // inspired by https://github.com/vuetifyjs/vuetify/blob/27b4b1e52060b6bee13a290a4829f935f1bd9c05/packages/vuetify/src/composables/defaults.ts#L92
4
+ /**
5
+ *
6
+ * @param {string} name
7
+ * @param {Record<string, any> | null} [localDefaults]
8
+ * @returns {import('vue').ComputedRef<Record<string, any>>}
9
+ */
10
+ export default function useCompDefaultProps (name, localDefaults = null) {
11
+ /** @type {import('vue').Ref<import('vuetify').DefaultsInstance> | undefined} */
12
+ const defaults = inject(Symbol.for('vuetify:defaults'))
13
+ if (!defaults) throw new Error('[vjsf] Could not find defaults instance')
14
+ return computed(() => {
15
+ const componentDefaults = defaults.value?.[name] ?? {}
16
+ if (!localDefaults) return componentDefaults
17
+ return { ...componentDefaults, ...localDefaults }
18
+ })
19
+ }
@@ -1,5 +1,5 @@
1
1
  import { shallowRef, ref, computed } from 'vue'
2
- import { moveArrayItem } from '../utils/index.js'
2
+ import { moveArrayItem } from '../utils/arrays.js'
3
3
 
4
4
  /**
5
5
  * @template T
@@ -42,6 +42,7 @@ export default function useDnd (array, callback) {
42
42
  dragging.value = itemIndex
43
43
  },
44
44
  onDragend: () => {
45
+ hovered.value = itemIndex
45
46
  dragging.value = -1
46
47
  callback()
47
48
  }
@@ -0,0 +1,53 @@
1
+ import { watch, shallowRef, ref, computed } from 'vue'
2
+
3
+ /**
4
+ * @param {import('vue').Ref<import('../types.js').VjsfNode>} nodeRef
5
+ * @param {import('../types.js').VjsfStatefulLayout} statefulLayout
6
+ */
7
+ export default function (nodeRef, statefulLayout) {
8
+ /** @type import('vue').Ref<import('@json-layout/vocabulary').SelectItems> */
9
+ const items = shallowRef([])
10
+ /** @type import('vue').Ref<boolean> */
11
+ const loading = ref(false)
12
+ /** @type import('vue').Ref<string> */
13
+ const search = ref('')
14
+
15
+ const hasItems = computed(() => {
16
+ return !!(nodeRef.value.layout.items || nodeRef.value.layout.getItems)
17
+ })
18
+
19
+ const fetchItems = async () => {
20
+ loading.value = true
21
+ items.value = await statefulLayout.getItems(nodeRef.value, search.value)
22
+ loading.value = false
23
+ }
24
+
25
+ watch(() => nodeRef.value.itemsCacheKey, (newValue, oldValue) => {
26
+ if (newValue === oldValue) return
27
+ fetchItems()
28
+ }, { immediate: true })
29
+
30
+ watch(search, () => {
31
+ fetchItems()
32
+ })
33
+
34
+ /**
35
+ * @param {any} selectedItem
36
+ * @param {any} itemValue
37
+ */
38
+ const prepareSelectedItem = (selectedItem, itemValue) => {
39
+ let item = selectedItem
40
+ // item and value are the same when the selection is not found in items list
41
+ if (selectedItem === itemValue) {
42
+ try {
43
+ item = statefulLayout.prepareSelectItem(nodeRef.value, selectedItem)
44
+ if (item.value === undefined) item.value = itemValue
45
+ } catch (e) {
46
+ item = { value: itemValue }
47
+ }
48
+ }
49
+ return item
50
+ }
51
+
52
+ return { hasItems, items, loading, search, prepareSelectedItem }
53
+ }
@@ -0,0 +1,136 @@
1
+ import NodeSlot from '../components/fragments/node-slot.vue'
2
+ import { computed, camelize, watch, ref, h } from 'vue'
3
+
4
+ // NOTE: in a previous draft we used to have this in options,
5
+ // but it was not very flexible and not very easy to use, user defined props should be managed
6
+ // by a combination of layout.props, layout.getProps and vuetify defaults provider (https://vuetifyjs.com/en/components/defaults-providers/#usage)
7
+ const defaultProps = {
8
+ fieldPropsCompact: {
9
+ hideDetails: 'auto'
10
+ },
11
+ fieldPropsReadOnly: { hideDetails: 'auto', variant: 'plain' },
12
+ fieldPropsSummary: { hideDetails: true }
13
+ }
14
+
15
+ /**
16
+ * @param {(Record<string, any> | undefined)[]} propsLevels
17
+ * @returns {Record<string, any> & {class: string[]}}
18
+ */
19
+ export function mergePropsLevels (propsLevels) {
20
+ /** @type {Record<string, any> & {class: string[]}} */
21
+ const fullProps = { class: [] }
22
+ for (const propsLevel of propsLevels) {
23
+ if (propsLevel) {
24
+ for (const key of Object.keys(propsLevel)) {
25
+ if (key === 'class') {
26
+ // a small convention for merging/overwriting classes:
27
+ // a class defined as a simple string overwrites the previous ones
28
+ // a class defined as an array is merged with the previous ones
29
+ if (Array.isArray(propsLevel.class)) fullProps.class = fullProps.class.concat(propsLevel.class)
30
+ else fullProps.class = [propsLevel.class]
31
+ } else {
32
+ fullProps[camelize(key)] = propsLevel[key]
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return fullProps
38
+ }
39
+
40
+ /**
41
+ * @param {import('vue').Ref<import('../types.js').VjsfNode>} nodeRef
42
+ * @param {import('../types.js').VjsfStatefulLayout} statefulLayout
43
+ * @param {{isMainComp?: boolean, bindData?: boolean, layoutPropsMap?: (string | [string, string])[]}} [opts]
44
+ */
45
+ export default function (nodeRef, statefulLayout, opts = {}) {
46
+ if (opts.bindData === undefined) opts.bindData = true
47
+ if (opts.isMainComp === undefined) opts.isMainComp = true
48
+
49
+ // we access vjsfNode properties through computeds so that the parts without mutations do not trigger reactivity
50
+ // this is to leverage the immutability provided by immer in json-layout
51
+ const options = computed(() => nodeRef.value.options)
52
+ const skeleton = computed(() => nodeRef.value.skeleton)
53
+ const layout = computed(() => nodeRef.value.layout)
54
+ const data = computed(() => nodeRef.value.data)
55
+ const error = computed(() => nodeRef.value.error)
56
+ const validated = computed(() => nodeRef.value.validated)
57
+ const nodeProps = computed(() => nodeRef.value.props)
58
+ const autofocus = computed(() => nodeRef.value.autofocus)
59
+ const children = computed(() => nodeRef.value.children)
60
+
61
+ const preparedData = computed(() => {
62
+ return (typeof data.value === 'string' && layout.value.separator) ? data.value.split(/** @type {string} */(layout.value.separator)) : data.value
63
+ })
64
+
65
+ // modelValue is not a straight computed, but is separated in a ref because the change in json-layout can be delayed
66
+ // depending on the debounceInputMs option
67
+ const localData = ref()
68
+ watch(preparedData, (data) => { localData.value = data }, { immediate: true })
69
+
70
+ // calculate the props of a field/input type component (text fields, etc)
71
+ // isMainComp is used to determine if this input component is also the main rendered component or if is mostly a wrapper (date picker, etc.)
72
+ const inputProps = computed(() => {
73
+ /** @type {(Record<string, any> | undefined)[]} */
74
+ const propsLevels = []
75
+ if (options.value.density === 'compact') propsLevels.push(defaultProps.fieldPropsCompact)
76
+ if (options.value.readOnly) propsLevels.push(defaultProps.fieldPropsReadOnly)
77
+ if (opts.isMainComp && nodeProps.value) propsLevels.push(nodeProps.value)
78
+
79
+ const fullProps = mergePropsLevels(propsLevels)
80
+
81
+ fullProps.label = layout.value.label
82
+ if (error.value && validated.value) {
83
+ fullProps.errorMessages = error.value
84
+ }
85
+ if (options.value.readOnly) {
86
+ fullProps.disabled = true
87
+ fullProps.class.push('vjsf-input--readonly')
88
+ }
89
+ if (autofocus.value) {
90
+ fullProps.class.push('vjsf-input--autofocus')
91
+ }
92
+
93
+ if (opts.layoutPropsMap) {
94
+ for (const propMap of opts.layoutPropsMap) {
95
+ if (typeof propMap === 'string') {
96
+ if (propMap in layout.value) fullProps[propMap] = layout.value[propMap]
97
+ } else {
98
+ if (propMap[1] in layout.value) fullProps[propMap[0]] = layout.value[propMap[1]]
99
+ }
100
+ }
101
+ }
102
+
103
+ if (opts.bindData) {
104
+ fullProps['onUpdate:modelValue'] = (/** @type string */value) => {
105
+ const newData = (Array.isArray(value) && layout.value.separator) ? value.join(/** @type {string} */(layout.value.separator)) : value
106
+ localData.value = newData
107
+ return statefulLayout.input(nodeRef.value, newData)
108
+ }
109
+ }
110
+ fullProps.onBlur = () => statefulLayout.blur(nodeRef.value)
111
+
112
+ return fullProps
113
+ })
114
+
115
+ // calculate the props of components that are not of the field category
116
+ const compProps = computed(() => {
117
+ /** @type {(Record<string, any> | undefined)[]} */
118
+ const propsLevels = [{ density: options.value.density }]
119
+ if (opts.isMainComp) propsLevels.push(layout.value.props)
120
+ const fullProps = mergePropsLevels(propsLevels)
121
+ return fullProps
122
+ })
123
+
124
+ // calculate the slots of components
125
+ const compSlots = computed(() => {
126
+ if (!layout.value.slots) return {}
127
+ /** @type {Record<string, any>} */
128
+ const slots = {}
129
+ for (const [key, layoutSlot] of Object.entries(layout.value.slots)) {
130
+ slots[key] = () => h(NodeSlot, { layoutSlot, node: nodeRef.value, statefulLayout })
131
+ }
132
+ return slots
133
+ })
134
+
135
+ return { localData, inputProps, compProps, compSlots, options, skeleton, layout, data, children }
136
+ }
@@ -0,0 +1,67 @@
1
+ import { computed, h } from 'vue'
2
+ import useField from './use-node.js'
3
+ import useGetItems from './use-get-items.js'
4
+ import SelectItem from '../components/fragments/select-item.vue'
5
+ import SelectSelection from '../components/fragments/select-selection.vue'
6
+
7
+ /**
8
+ * specialized use of useFieldProps shared between select and autocomplete components
9
+ * @param {import('vue').Ref<import('../types.js').VjsfSelectNode>} nodeRef
10
+ * @param {import('../types.js').VjsfStatefulLayout} statefulLayout
11
+ */
12
+ export default function (nodeRef, statefulLayout) {
13
+ const layout = computed(() => nodeRef.value.layout)
14
+
15
+ const { inputProps, options, skeleton, localData, compSlots } = useField(nodeRef, statefulLayout, { layoutPropsMap: ['multiple'], bindData: false })
16
+ const getItems = useGetItems(nodeRef, statefulLayout)
17
+
18
+ const selectProps = computed(() => {
19
+ const props = { ...inputProps.value }
20
+
21
+ if (options.value.readOnly) props.menuProps = { modelValue: false }
22
+ props.clearable = props.clearable ?? !skeleton.value.required
23
+ props.valueComparator = (/** @type {any} */a, /** @type {any} */b) => {
24
+ const aKey = typeof a === 'object' ? statefulLayout.prepareSelectItem(nodeRef.value, a).key : a
25
+ const bKey = typeof b === 'object' ? statefulLayout.prepareSelectItem(nodeRef.value, b).key : b
26
+ return aKey === bKey
27
+ }
28
+ props['onUpdate:modelValue'] = (/** @type string */value) => {
29
+ // fix some weird case where vuetify only keep the title property of an unknown item
30
+ if (Array.isArray(value) && Array.isArray(nodeRef.value.data)) {
31
+ for (let i = 0; i < nodeRef.value.data.length; i++) {
32
+ if (typeof nodeRef.value.data[i] === 'object' && typeof value[i] === 'string') {
33
+ value[i] = nodeRef.value.data[i]
34
+ }
35
+ }
36
+ }
37
+
38
+ const newData = (Array.isArray(value) && nodeRef.value.layout.separator) ? value.join(/** @type {string} */(nodeRef.value.layout.separator)) : value
39
+ localData.value = newData
40
+ return statefulLayout.input(nodeRef.value, newData)
41
+ }
42
+ props.onBlur = () => statefulLayout.blur(nodeRef.value)
43
+ return props
44
+ })
45
+
46
+ // shared between select and autocomplete components
47
+ const selectSlots = computed(() => {
48
+ const slots = { ...compSlots.value }
49
+ if (!slots.item) {
50
+ slots.item = (/** @type {any} */ context) => h(SelectItem, {
51
+ multiple: layout.value.multiple,
52
+ itemProps: context.props,
53
+ item: context.item.raw
54
+ })
55
+ }
56
+ if (!slots.selection) {
57
+ slots.selection = (/** @type {any} */ context) => h(SelectSelection, {
58
+ multiple: layout.value.multiple,
59
+ last: layout.value.multiple && context.index === nodeRef.value.data.length - 1,
60
+ item: getItems.prepareSelectedItem(context.item.raw, context.item.value)
61
+ })
62
+ }
63
+ return slots
64
+ })
65
+
66
+ return { localData, inputProps, selectProps, compSlots, selectSlots, getItems }
67
+ }
@@ -1,9 +1,12 @@
1
+ import { useDefaults } from 'vuetify'
1
2
  import { StatefulLayout, produceCompileOptions } from '@json-layout/core'
2
- import { inject, toRaw, shallowRef, computed, ref, watch, useSlots } from 'vue'
3
+ import { inject, toRaw, shallowRef, computed, ref, watch, useSlots, getCurrentInstance } from 'vue'
3
4
  import { useElementSize } from '@vueuse/core'
4
- import { getFullOptions } from '../components/options.js'
5
- import { registeredNodeComponents } from '../utils/index.js'
5
+ import { getFullOptions } from '../options.js'
6
6
  import { setAutoFreeze } from 'immer'
7
+ import Debug from 'debug'
8
+
9
+ const debug = Debug('vjsf:use-vjsf')
7
10
 
8
11
  // immer freezing is disabled because it is not compatible with Vue 3 reactivity
9
12
  setAutoFreeze(false)
@@ -31,6 +34,11 @@ export const emits = {
31
34
  export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compile, precompiledLayout) => {
32
35
  const el = ref(null)
33
36
 
37
+ useDefaults({}, 'Vjsf')
38
+ /** @type {import('vuetify').DefaultsInstance} */
39
+ const defaults = (inject(Symbol.for('vuetify:defaults')))?.value
40
+ debug('provided defaults', defaults)
41
+
34
42
  // TODO: apply a debounce to width ?
35
43
  const { width } = useElementSize(el)
36
44
 
@@ -40,22 +48,60 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
40
48
  const stateTree = shallowRef(null)
41
49
 
42
50
  // cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/composables/form.ts
43
- const form = inject(Symbol.for('vuetify:form'))
51
+ /** @type {any | null} */
52
+ const form = inject(Symbol.for('vuetify:form'), null)
44
53
  if (form) {
45
54
  form.register({
46
- id: 'vjsf', // TODO: a unique random id ?
55
+ id: `vjsf-${Math.random().toString(36).substring(2, 9)}`,
47
56
  validate: () => {
48
57
  statefulLayout.value?.validate()
49
- return statefulLayout.value?.errors
58
+ return statefulLayout.value?.errors || []
50
59
  },
51
60
  reset: () => statefulLayout.value?.resetValidation(), // TODO: also empty the data ?
52
- resetValidation: () => statefulLayout.value?.resetValidation()
61
+ resetValidation: () => statefulLayout.value?.resetValidation(),
62
+ vm: getCurrentInstance()
53
63
  })
54
64
  }
55
65
 
56
66
  const slots = useSlots()
57
67
 
58
- const fullOptions = computed(() => getFullOptions(options.value, form, width.value, slots, { ...nodeComponents, ...toRaw(registeredNodeComponents.value) }))
68
+ /* Callbacks from json layout stateful layout */
69
+ /**
70
+ * @param {import('@json-layout/core').StatefulLayout} statefulLayout
71
+ */
72
+ const onStatefulLayoutUpdate = (statefulLayout) => {
73
+ debug('onStatefulLayoutUpdate', statefulLayout)
74
+ if (!statefulLayout) return
75
+ stateTree.value = statefulLayout.stateTree
76
+ debug(' -> emit update:state')
77
+ emit('update:state', statefulLayout)
78
+ if (form) {
79
+ // cf https://vuetifyjs.com/en/components/forms/#validation-state
80
+ if (statefulLayout.valid) form.update('vjsf', true, [])
81
+ else if (statefulLayout.hasHiddenError) form.update('vjsf', null, [])
82
+ else form.update('vjsf', false, [])
83
+ }
84
+ }
85
+ /**
86
+ * @param {any} data
87
+ */
88
+ const onDataUpdate = (data) => {
89
+ debug('onDataUpdate', data)
90
+ debug(' -> emit update:modelValue')
91
+ emit('update:modelValue', data)
92
+ }
93
+ const onAutofocus = () => {
94
+ if (!el.value) return
95
+ // @ts-ignore
96
+ const autofocusNodeElement = el.value.querySelector('.vjsf-input--autofocus')
97
+ debug('onAutofocus', autofocusNodeElement)
98
+ if (autofocusNodeElement) {
99
+ const autofocusInputElement = autofocusNodeElement.querySelector('input') ?? autofocusNodeElement.querySelector('textarea:not([style*="display: none"]')
100
+ if (autofocusInputElement) autofocusInputElement.focus()
101
+ }
102
+ }
103
+
104
+ const fullOptions = computed(() => getFullOptions(options.value, form, width.value, defaults?.global, slots, { ...nodeComponents }, onDataUpdate, onStatefulLayoutUpdate, onAutofocus))
59
105
 
60
106
  // do not use a simple computed here as we want to prevent recompiling the layout when the options are the same
61
107
  /** @type {import('vue').Ref<import('@json-layout/core').PartialCompileOptions>} */
@@ -63,7 +109,10 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
63
109
  watch(fullOptions, (newOptions) => {
64
110
  if (precompiledLayout?.value) return
65
111
  const newCompileOptions = produceCompileOptions(compileOptions.value, newOptions)
66
- if (newCompileOptions !== compileOptions.value) compileOptions.value = newCompileOptions
112
+ if (newCompileOptions !== compileOptions.value) {
113
+ debug('new compileOptions', newCompileOptions)
114
+ compileOptions.value = newCompileOptions
115
+ }
67
116
  }, { immediate: true })
68
117
 
69
118
  const compiledLayout = computed(() => {
@@ -73,71 +122,43 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
73
122
  return compiledLayout
74
123
  })
75
124
 
76
- const onStatefulLayoutUpdate = () => {
77
- if (!statefulLayout.value) return
78
- stateTree.value = statefulLayout.value.stateTree
79
- emit('update:state', statefulLayout.value)
80
- if (form) {
81
- // cf https://vuetifyjs.com/en/components/forms/#validation-state
82
- if (statefulLayout.value.valid) form.update('vjsf', true, [])
83
- else if (statefulLayout.value.hasHiddenError) form.update('vjsf', null, [])
84
- else form.update('vjsf', false, [])
85
- }
86
- }
87
-
88
- const onDataUpdate = () => {
89
- if (statefulLayout.value && modelValue !== statefulLayout.value.data) {
90
- emit('update:modelValue', statefulLayout.value.data)
91
- }
92
- }
93
-
94
125
  const initStatefulLayout = () => {
95
126
  if (!width.value) return
96
-
97
127
  // @ts-ignore
98
- const _statefulLayout = /** @type {import('../types.js').VjsfStatefulLayout} */(new StatefulLayout(
128
+ statefulLayout.value = /** @type {import('../types.js').VjsfStatefulLayout} */(new StatefulLayout(
99
129
  toRaw(compiledLayout.value),
100
- toRaw(compiledLayout.value.skeletonTree),
130
+ toRaw(compiledLayout.value.skeletonTrees[compiledLayout.value.mainTree]),
101
131
  toRaw(fullOptions.value),
102
132
  toRaw(modelValue.value)
103
133
  ))
104
- statefulLayout.value = _statefulLayout
105
- onStatefulLayoutUpdate()
106
- onDataUpdate()
107
- _statefulLayout.events.on('update', () => {
108
- onStatefulLayoutUpdate()
109
- })
110
- _statefulLayout.events.on('data', () => {
111
- onDataUpdate()
112
- })
113
- emit('update:state', _statefulLayout)
114
- _statefulLayout.events.on('autofocus', () => {
115
- if (!el.value) return
116
- // @ts-ignore
117
- const autofocusNodeElement = el.value.querySelector('.vjsf-input--autofocus')
118
- if (autofocusNodeElement) {
119
- const autofocusInputElement = autofocusNodeElement.querySelector('input') ?? autofocusNodeElement.querySelector('textarea:not([style*="display: none"]')
120
- if (autofocusInputElement) autofocusInputElement.focus()
121
- }
122
- })
123
134
  }
124
135
 
125
136
  // case where options are updated from outside
126
137
  watch(fullOptions, (newOptions) => {
138
+ debug('watch fullOptions', fullOptions)
127
139
  if (statefulLayout.value) {
128
- statefulLayout.value.options = newOptions
140
+ debug(' -> update statefulLayout options')
141
+ statefulLayout.value.options = toRaw(newOptions)
129
142
  } else {
143
+ debug(' -> init statefulLayout')
130
144
  initStatefulLayout()
131
145
  }
132
146
  })
133
147
 
134
148
  // case where data is updated from outside
135
149
  watch(modelValue, (newData) => {
136
- if (statefulLayout.value && statefulLayout.value.data !== newData) statefulLayout.value.data = toRaw(newData)
150
+ const rawData = toRaw(newData)
151
+ if (statefulLayout.value && statefulLayout.value.data !== rawData) {
152
+ debug('modelValue changed from outside', rawData)
153
+ debug(' -> update statefulLayout data')
154
+ statefulLayout.value.data = toRaw(rawData)
155
+ }
137
156
  })
138
157
 
139
158
  // case where schema or compile options are updated from outside
140
159
  watch(compiledLayout, (newCompiledLayout) => {
160
+ debug('watch compiledLayout', newCompiledLayout)
161
+ debug(' -> init statefulLayout')
141
162
  initStatefulLayout()
142
163
  })
143
164
 
package/src/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import Vjsf from './components/vjsf.vue'
2
- import { defaultOptions } from './components/options.js'
3
- export { Vjsf, defaultOptions }
2
+ import { defaultOptions, defaultIcons } from './options.js'
3
+ export { Vjsf, defaultOptions, defaultIcons }
4
4
  export default Vjsf
5
+
6
+ /** @typedef {import('./types.js').PartialVjsfOptions} Options */
7
+ /** @typedef {import('./types.js').PartialVjsfCompileOptions} CompileOptions */