@koumoul/vjsf 3.0.0-beta.8 → 3.0.0

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 +30 -13
  20. package/src/components/nodes/date-time-picker.vue +83 -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 +41 -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 +74 -42
  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 +4 -4
  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,47 @@
1
1
  <script setup>
2
+ import TextFieldMenu from '../fragments/text-field-menu.vue'
3
+ import { VTimePicker } from 'vuetify/labs/VTimePicker'
4
+ import { useDate, useDefaults } from 'vuetify'
5
+ import { computed, toRef } from 'vue'
6
+ import { getShortTime, getLongTime } from '../../utils/dates.js'
7
+ import useNode from '../../composables/use-node.js'
2
8
 
9
+ useDefaults({}, 'VjsfDatePicker')
10
+
11
+ const props = defineProps({
12
+ modelValue: {
13
+ /** @type import('vue').PropType<import('../../types.js').VjsfDatePickerNode> */
14
+ type: Object,
15
+ required: true
16
+ },
17
+ statefulLayout: {
18
+ /** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
19
+ type: Object,
20
+ required: true
21
+ }
22
+ })
23
+
24
+ const vDate = useDate()
25
+
26
+ const { compProps, localData } = useNode(toRef(props, 'modelValue'), props.statefulLayout)
27
+
28
+ const timePickerProps = computed(() => {
29
+ const timePickerProps = { ...compProps.value }
30
+ timePickerProps['ampm-in-title'] = true
31
+ if (localData.value) timePickerProps.modelValue = getShortTime(localData.value)
32
+ return timePickerProps
33
+ })
3
34
  </script>
4
35
 
5
36
  <template>
6
- TODO time
37
+ <text-field-menu
38
+ :model-value="props.modelValue"
39
+ :stateful-layout="statefulLayout"
40
+ :formatted-value="timePickerProps.modelValue && vDate.format('2010-04-13T' + timePickerProps.modelValue, 'fullTime')"
41
+ >
42
+ <v-time-picker
43
+ v-bind="timePickerProps"
44
+ @update:model-value="value => {statefulLayout.input(props.modelValue, value && getLongTime(value))}"
45
+ />
46
+ </text-field-menu>
7
47
  </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)
@@ -30,6 +33,13 @@ export const emits = {
30
33
  */
31
34
  export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compile, precompiledLayout) => {
32
35
  const el = ref(null)
36
+
37
+ useDefaults({}, 'Vjsf')
38
+ /** @type {import('vuetify').DefaultsInstance} */
39
+ const defaults = (inject(Symbol.for('vuetify:defaults')))?.value
40
+ debug('provided defaults', defaults)
41
+
42
+ // TODO: apply a debounce to width ?
33
43
  const { width } = useElementSize(el)
34
44
 
35
45
  /** @type import('vue').ShallowRef<import('../types.js').VjsfStatefulLayout | null> */
@@ -38,22 +48,60 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
38
48
  const stateTree = shallowRef(null)
39
49
 
40
50
  // cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/composables/form.ts
41
- const form = inject(Symbol.for('vuetify:form'))
51
+ /** @type {any | null} */
52
+ const form = inject(Symbol.for('vuetify:form'), null)
42
53
  if (form) {
43
54
  form.register({
44
- id: 'vjsf', // TODO: a unique random id ?
55
+ id: `vjsf-${Math.random().toString(36).substring(2, 9)}`,
45
56
  validate: () => {
46
57
  statefulLayout.value?.validate()
47
- return statefulLayout.value?.errors
58
+ return statefulLayout.value?.errors || []
48
59
  },
49
60
  reset: () => statefulLayout.value?.resetValidation(), // TODO: also empty the data ?
50
- resetValidation: () => statefulLayout.value?.resetValidation()
61
+ resetValidation: () => statefulLayout.value?.resetValidation(),
62
+ vm: getCurrentInstance()
51
63
  })
52
64
  }
53
65
 
54
66
  const slots = useSlots()
55
67
 
56
- 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))
57
105
 
58
106
  // do not use a simple computed here as we want to prevent recompiling the layout when the options are the same
59
107
  /** @type {import('vue').Ref<import('@json-layout/core').PartialCompileOptions>} */
@@ -61,7 +109,10 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
61
109
  watch(fullOptions, (newOptions) => {
62
110
  if (precompiledLayout?.value) return
63
111
  const newCompileOptions = produceCompileOptions(compileOptions.value, newOptions)
64
- if (newCompileOptions !== compileOptions.value) compileOptions.value = newCompileOptions
112
+ if (newCompileOptions !== compileOptions.value) {
113
+ debug('new compileOptions', newCompileOptions)
114
+ compileOptions.value = newCompileOptions
115
+ }
65
116
  }, { immediate: true })
66
117
 
67
118
  const compiledLayout = computed(() => {
@@ -71,62 +122,43 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
71
122
  return compiledLayout
72
123
  })
73
124
 
74
- const onStatefulLayoutUpdate = () => {
75
- if (!statefulLayout.value) return
76
- stateTree.value = statefulLayout.value.stateTree
77
- emit('update:modelValue', statefulLayout.value.data)
78
- emit('update:state', statefulLayout.value)
79
- if (form) {
80
- // cf https://vuetifyjs.com/en/components/forms/#validation-state
81
- if (statefulLayout.value.valid) form.update('vjsf', true, [])
82
- else if (statefulLayout.value.hasHiddenError) form.update('vjsf', null, [])
83
- else form.update('vjsf', false, [])
84
- }
85
- }
86
-
87
125
  const initStatefulLayout = () => {
88
126
  if (!width.value) return
89
-
90
127
  // @ts-ignore
91
- const _statefulLayout = /** @type {import('../types.js').VjsfStatefulLayout} */(new StatefulLayout(
128
+ statefulLayout.value = /** @type {import('../types.js').VjsfStatefulLayout} */(new StatefulLayout(
92
129
  toRaw(compiledLayout.value),
93
- toRaw(compiledLayout.value.skeletonTree),
130
+ toRaw(compiledLayout.value.skeletonTrees[compiledLayout.value.mainTree]),
94
131
  toRaw(fullOptions.value),
95
132
  toRaw(modelValue.value)
96
133
  ))
97
- statefulLayout.value = _statefulLayout
98
- onStatefulLayoutUpdate()
99
- _statefulLayout.events.on('update', () => {
100
- onStatefulLayoutUpdate()
101
- })
102
- emit('update:state', _statefulLayout)
103
- _statefulLayout.events.on('autofocus', () => {
104
- if (!el.value) return
105
- // @ts-ignore
106
- const autofocusNodeElement = el.value.querySelector('.vjsf-input--autofocus')
107
- if (autofocusNodeElement) {
108
- const autofocusInputElement = autofocusNodeElement.querySelector('input') ?? autofocusNodeElement.querySelector('textarea:not([style*="display: none"]')
109
- if (autofocusInputElement) autofocusInputElement.focus()
110
- }
111
- })
112
134
  }
113
135
 
114
136
  // case where options are updated from outside
115
137
  watch(fullOptions, (newOptions) => {
138
+ debug('watch fullOptions', fullOptions)
116
139
  if (statefulLayout.value) {
117
- statefulLayout.value.options = newOptions
140
+ debug(' -> update statefulLayout options')
141
+ statefulLayout.value.options = toRaw(newOptions)
118
142
  } else {
143
+ debug(' -> init statefulLayout')
119
144
  initStatefulLayout()
120
145
  }
121
146
  })
122
147
 
123
148
  // case where data is updated from outside
124
149
  watch(modelValue, (newData) => {
125
- 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
+ }
126
156
  })
127
157
 
128
158
  // case where schema or compile options are updated from outside
129
159
  watch(compiledLayout, (newCompiledLayout) => {
160
+ debug('watch compiledLayout', newCompiledLayout)
161
+ debug(' -> init statefulLayout')
130
162
  initStatefulLayout()
131
163
  })
132
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 */