@koumoul/vjsf 3.0.0-beta.5 → 3.0.0-beta.51

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 (164) hide show
  1. package/README.md +21 -0
  2. package/package.json +12 -17
  3. package/src/compat/v2.js +132 -27
  4. package/src/compile/index.js +19 -4
  5. package/src/compile/options.js +4 -9
  6. package/src/compile/v-jsf-compiled.vue.ejs +1 -2
  7. package/src/components/fragments/child-subtitle.vue +25 -0
  8. package/src/components/fragments/help-message.vue +33 -8
  9. package/src/components/fragments/section-header.vue +9 -7
  10. package/src/components/fragments/select-item-icon.vue +2 -2
  11. package/src/components/fragments/select-item.vue +2 -1
  12. package/src/components/fragments/select-selection.vue +2 -1
  13. package/src/components/fragments/selection-group.vue +105 -0
  14. package/src/components/fragments/text-field-menu.vue +16 -7
  15. package/src/components/node.vue +58 -41
  16. package/src/components/nodes/autocomplete.vue +14 -60
  17. package/src/components/nodes/card.vue +39 -0
  18. package/src/components/nodes/checkbox-group.vue +39 -0
  19. package/src/components/nodes/checkbox.vue +31 -26
  20. package/src/components/nodes/color-picker.vue +10 -4
  21. package/src/components/nodes/combobox.vue +17 -40
  22. package/src/components/nodes/date-picker.vue +30 -13
  23. package/src/components/nodes/date-time-picker.vue +83 -3
  24. package/src/components/nodes/expansion-panels.vue +34 -16
  25. package/src/components/nodes/file-input.vue +15 -11
  26. package/src/components/nodes/list.vue +251 -111
  27. package/src/components/nodes/number-combobox.vue +18 -39
  28. package/src/components/nodes/number-field.vue +17 -11
  29. package/src/components/nodes/one-of-select.vue +53 -27
  30. package/src/components/nodes/radio-group.vue +58 -0
  31. package/src/components/nodes/section.vue +4 -1
  32. package/src/components/nodes/select.vue +15 -54
  33. package/src/components/nodes/slider.vue +32 -29
  34. package/src/components/nodes/stepper.vue +10 -2
  35. package/src/components/nodes/switch-group.vue +39 -0
  36. package/src/components/nodes/switch.vue +31 -26
  37. package/src/components/nodes/tabs.vue +20 -8
  38. package/src/components/nodes/text-field.vue +10 -7
  39. package/src/components/nodes/textarea.vue +20 -12
  40. package/src/components/nodes/time-picker.vue +41 -1
  41. package/src/components/nodes/vertical-tabs.vue +16 -6
  42. package/src/components/tree.vue +1 -1
  43. package/src/components/vjsf.vue +11 -1
  44. package/src/composables/use-comp-defaults.js +19 -0
  45. package/src/composables/use-dnd.js +2 -1
  46. package/src/composables/use-get-items.js +53 -0
  47. package/src/composables/use-node.js +136 -0
  48. package/src/composables/use-select-node.js +67 -0
  49. package/src/composables/use-vjsf.js +70 -40
  50. package/src/index.js +5 -2
  51. package/src/options.js +65 -0
  52. package/src/types.ts +64 -33
  53. package/src/utils/arrays.js +37 -6
  54. package/src/utils/build.js +1 -1
  55. package/types/compat/v2.d.ts.map +1 -1
  56. package/types/compile/index.d.ts +2 -2
  57. package/types/compile/index.d.ts.map +1 -1
  58. package/types/compile/options.d.ts +3 -2
  59. package/types/compile/options.d.ts.map +1 -1
  60. package/types/components/fragments/child-subtitle.vue.d.ts +8 -0
  61. package/types/components/fragments/child-subtitle.vue.d.ts.map +1 -0
  62. package/types/components/fragments/help-message.vue.d.ts +2 -2
  63. package/types/components/fragments/node-slot.vue.d.ts +2 -44
  64. package/types/components/fragments/node-slot.vue.d.ts.map +1 -1
  65. package/types/components/fragments/section-header.vue.d.ts +4 -2
  66. package/types/components/fragments/select-item-icon.vue.d.ts +2 -12
  67. package/types/components/fragments/select-item.vue.d.ts +2 -2
  68. package/types/components/fragments/select-selection.vue.d.ts +2 -2
  69. package/types/components/fragments/selection-group.vue.d.ts +5 -0
  70. package/types/components/fragments/selection-group.vue.d.ts.map +1 -0
  71. package/types/components/fragments/text-field-menu.vue.d.ts +2 -2
  72. package/types/components/fragments/text-field-menu.vue.d.ts.map +1 -1
  73. package/types/components/node.vue.d.ts +2 -2
  74. package/types/components/nodes/autocomplete.vue.d.ts +2 -24
  75. package/types/components/nodes/autocomplete.vue.d.ts.map +1 -1
  76. package/types/components/nodes/card.vue.d.ts +10 -0
  77. package/types/components/nodes/card.vue.d.ts.map +1 -0
  78. package/types/components/nodes/checkbox-group.vue.d.ts +5 -0
  79. package/types/components/nodes/checkbox-group.vue.d.ts.map +1 -0
  80. package/types/components/nodes/checkbox.vue.d.ts +3 -8
  81. package/types/components/nodes/color-picker.vue.d.ts +2 -2
  82. package/types/components/nodes/combobox.vue.d.ts +2 -24
  83. package/types/components/nodes/combobox.vue.d.ts.map +1 -1
  84. package/types/components/nodes/date-picker.vue.d.ts +2 -2
  85. package/types/components/nodes/date-time-picker.vue.d.ts +4 -4
  86. package/types/components/nodes/expansion-panels.vue.d.ts +2 -2
  87. package/types/components/nodes/file-input.vue.d.ts +2 -24
  88. package/types/components/nodes/file-input.vue.d.ts.map +1 -1
  89. package/types/components/nodes/list.vue.d.ts +2 -2
  90. package/types/components/nodes/number-combobox.vue.d.ts +2 -24
  91. package/types/components/nodes/number-combobox.vue.d.ts.map +1 -1
  92. package/types/components/nodes/number-field.vue.d.ts +2 -24
  93. package/types/components/nodes/number-field.vue.d.ts.map +1 -1
  94. package/types/components/nodes/one-of-select.vue.d.ts +2 -2
  95. package/types/components/nodes/radio-group.vue.d.ts +5 -0
  96. package/types/components/nodes/radio-group.vue.d.ts.map +1 -0
  97. package/types/components/nodes/section.vue.d.ts +2 -2
  98. package/types/components/nodes/select.vue.d.ts +2 -24
  99. package/types/components/nodes/select.vue.d.ts.map +1 -1
  100. package/types/components/nodes/slider.vue.d.ts +3 -8
  101. package/types/components/nodes/stepper.vue.d.ts +2 -2
  102. package/types/components/nodes/switch-group.vue.d.ts +5 -0
  103. package/types/components/nodes/switch-group.vue.d.ts.map +1 -0
  104. package/types/components/nodes/switch.vue.d.ts +3 -8
  105. package/types/components/nodes/tabs.vue.d.ts +2 -2
  106. package/types/components/nodes/text-field.vue.d.ts +2 -24
  107. package/types/components/nodes/text-field.vue.d.ts.map +1 -1
  108. package/types/components/nodes/textarea.vue.d.ts +2 -24
  109. package/types/components/nodes/textarea.vue.d.ts.map +1 -1
  110. package/types/components/nodes/time-picker.vue.d.ts +8 -1
  111. package/types/components/nodes/vertical-tabs.vue.d.ts +2 -2
  112. package/types/components/options.d.ts +1 -1
  113. package/types/components/options.d.ts.map +1 -1
  114. package/types/components/tree.vue.d.ts +2 -2
  115. package/types/components/vjsf.vue.d.ts +4 -4
  116. package/types/composables/use-comp-defaults.d.ts +8 -0
  117. package/types/composables/use-comp-defaults.d.ts.map +1 -0
  118. package/types/composables/use-dnd.d.ts +3 -3
  119. package/types/composables/use-dnd.d.ts.map +1 -1
  120. package/types/composables/use-field-props.d.ts +30 -0
  121. package/types/composables/use-field-props.d.ts.map +1 -0
  122. package/types/composables/use-field.d.ts +31 -0
  123. package/types/composables/use-field.d.ts.map +1 -0
  124. package/types/composables/use-get-items.d.ts +12 -0
  125. package/types/composables/use-get-items.d.ts.map +1 -0
  126. package/types/composables/use-node.d.ts +32 -0
  127. package/types/composables/use-node.d.ts.map +1 -0
  128. package/types/composables/use-select-field.d.ts +21 -0
  129. package/types/composables/use-select-field.d.ts.map +1 -0
  130. package/types/composables/use-select-node.d.ts +27 -0
  131. package/types/composables/use-select-node.d.ts.map +1 -0
  132. package/types/composables/use-select-props.d.ts +21 -0
  133. package/types/composables/use-select-props.d.ts.map +1 -0
  134. package/types/composables/use-select.d.ts +21 -0
  135. package/types/composables/use-select.d.ts.map +1 -0
  136. package/types/composables/use-vjsf.d.ts +2 -2
  137. package/types/composables/use-vjsf.d.ts.map +1 -1
  138. package/types/iconsets/default-aliases.d.ts +10 -0
  139. package/types/iconsets/default-aliases.d.ts.map +1 -0
  140. package/types/iconsets/mdi-svg.d.ts +3 -0
  141. package/types/iconsets/mdi-svg.d.ts.map +1 -0
  142. package/types/iconsets/mdi.d.ts +3 -0
  143. package/types/iconsets/mdi.d.ts.map +1 -0
  144. package/types/index.d.ts +5 -2
  145. package/types/index.d.ts.map +1 -1
  146. package/types/options.d.ts +9 -0
  147. package/types/options.d.ts.map +1 -0
  148. package/types/types.d.ts +65 -33
  149. package/types/types.d.ts.map +1 -1
  150. package/types/utils/arrays.d.ts +17 -4
  151. package/types/utils/arrays.d.ts.map +1 -1
  152. package/types/utils/build.d.ts +1 -1
  153. package/types/utils/index.d.ts +0 -3
  154. package/types/utils/props.d.ts +8 -2
  155. package/types/utils/props.d.ts.map +1 -1
  156. package/types/utils/slots.d.ts +8 -0
  157. package/types/utils/slots.d.ts.map +1 -1
  158. package/src/components/options.js +0 -27
  159. package/src/utils/global-register.js +0 -13
  160. package/src/utils/index.js +0 -5
  161. package/src/utils/props.js +0 -109
  162. package/src/utils/slots.js +0 -18
  163. package/types/utils/global-register.d.ts +0 -8
  164. package/types/utils/global-register.d.ts.map +0 -1
@@ -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
+ fullProps.onBlur = () => statefulLayout.blur(nodeRef.value)
110
+ }
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,8 +1,14 @@
1
1
  import { StatefulLayout, produceCompileOptions } from '@json-layout/core'
2
- import { inject, toRaw, shallowRef, computed, ref, watch, useSlots } from 'vue'
2
+ import { inject, toRaw, shallowRef, computed, ref, watch, useSlots, getCurrentInstance } from 'vue'
3
3
  import { useElementSize } from '@vueuse/core'
4
- import { getFullOptions } from '../components/options.js'
5
- import { registeredNodeComponents } from '../utils/index.js'
4
+ import { getFullOptions } from '../options.js'
5
+ import { setAutoFreeze } from 'immer'
6
+ import Debug from 'debug'
7
+
8
+ const debug = Debug('vjsf:use-vjsf')
9
+
10
+ // immer freezing is disabled because it is not compatible with Vue 3 reactivity
11
+ setAutoFreeze(false)
6
12
 
7
13
  export const emits = {
8
14
  /**
@@ -26,6 +32,8 @@ export const emits = {
26
32
  */
27
33
  export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compile, precompiledLayout) => {
28
34
  const el = ref(null)
35
+
36
+ // TODO: apply a debounce to width ?
29
37
  const { width } = useElementSize(el)
30
38
 
31
39
  /** @type import('vue').ShallowRef<import('../types.js').VjsfStatefulLayout | null> */
@@ -34,7 +42,8 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
34
42
  const stateTree = shallowRef(null)
35
43
 
36
44
  // cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/composables/form.ts
37
- const form = inject(Symbol.for('vuetify:form'))
45
+ /** @type {any | null} */
46
+ const form = inject(Symbol.for('vuetify:form'), null)
38
47
  if (form) {
39
48
  form.register({
40
49
  id: 'vjsf', // TODO: a unique random id ?
@@ -43,13 +52,50 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
43
52
  return statefulLayout.value?.errors
44
53
  },
45
54
  reset: () => statefulLayout.value?.resetValidation(), // TODO: also empty the data ?
46
- resetValidation: () => statefulLayout.value?.resetValidation()
55
+ resetValidation: () => statefulLayout.value?.resetValidation(),
56
+ vm: getCurrentInstance()
47
57
  })
48
58
  }
49
59
 
50
60
  const slots = useSlots()
51
61
 
52
- const fullOptions = computed(() => getFullOptions(options.value, form, width.value, slots, { ...nodeComponents, ...toRaw(registeredNodeComponents.value) }))
62
+ /* Callbacks from json layout stateful layout */
63
+ /**
64
+ * @param {import('@json-layout/core').StatefulLayout} statefulLayout
65
+ */
66
+ const onStatefulLayoutUpdate = (statefulLayout) => {
67
+ debug('onStatefulLayoutUpdate', statefulLayout)
68
+ if (!statefulLayout) return
69
+ stateTree.value = statefulLayout.stateTree
70
+ debug(' -> emit update:state')
71
+ emit('update:state', statefulLayout)
72
+ if (form) {
73
+ // cf https://vuetifyjs.com/en/components/forms/#validation-state
74
+ if (statefulLayout.valid) form.update('vjsf', true, [])
75
+ else if (statefulLayout.hasHiddenError) form.update('vjsf', null, [])
76
+ else form.update('vjsf', false, [])
77
+ }
78
+ }
79
+ /**
80
+ * @param {any} data
81
+ */
82
+ const onDataUpdate = (data) => {
83
+ debug('onDataUpdate', data)
84
+ debug(' -> emit update:modelValue')
85
+ emit('update:modelValue', data)
86
+ }
87
+ const onAutofocus = () => {
88
+ if (!el.value) return
89
+ // @ts-ignore
90
+ const autofocusNodeElement = el.value.querySelector('.vjsf-input--autofocus')
91
+ debug('onAutofocus', autofocusNodeElement)
92
+ if (autofocusNodeElement) {
93
+ const autofocusInputElement = autofocusNodeElement.querySelector('input') ?? autofocusNodeElement.querySelector('textarea:not([style*="display: none"]')
94
+ if (autofocusInputElement) autofocusInputElement.focus()
95
+ }
96
+ }
97
+
98
+ const fullOptions = computed(() => getFullOptions(options.value, form, width.value, slots, { ...nodeComponents }, onDataUpdate, onStatefulLayoutUpdate, onAutofocus))
53
99
 
54
100
  // do not use a simple computed here as we want to prevent recompiling the layout when the options are the same
55
101
  /** @type {import('vue').Ref<import('@json-layout/core').PartialCompileOptions>} */
@@ -57,7 +103,10 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
57
103
  watch(fullOptions, (newOptions) => {
58
104
  if (precompiledLayout?.value) return
59
105
  const newCompileOptions = produceCompileOptions(compileOptions.value, newOptions)
60
- if (newCompileOptions !== compileOptions.value) compileOptions.value = newCompileOptions
106
+ if (newCompileOptions !== compileOptions.value) {
107
+ debug('new compileOptions', newCompileOptions)
108
+ compileOptions.value = newCompileOptions
109
+ }
61
110
  }, { immediate: true })
62
111
 
63
112
  const compiledLayout = computed(() => {
@@ -67,62 +116,43 @@ export const useVjsf = (schema, modelValue, options, nodeComponents, emit, compi
67
116
  return compiledLayout
68
117
  })
69
118
 
70
- const onStatefulLayoutUpdate = () => {
71
- if (!statefulLayout.value) return
72
- stateTree.value = statefulLayout.value.stateTree
73
- emit('update:modelValue', statefulLayout.value.data)
74
- emit('update:state', statefulLayout.value)
75
- if (form) {
76
- // cf https://vuetifyjs.com/en/components/forms/#validation-state
77
- if (statefulLayout.value.valid) form.update('vjsf', true, [])
78
- else if (statefulLayout.value.hasHiddenError) form.update('vjsf', null, [])
79
- else form.update('vjsf', false, [])
80
- }
81
- }
82
-
83
119
  const initStatefulLayout = () => {
84
120
  if (!width.value) return
85
-
86
121
  // @ts-ignore
87
- const _statefulLayout = /** @type {import('../types.js').VjsfStatefulLayout} */(new StatefulLayout(
122
+ statefulLayout.value = /** @type {import('../types.js').VjsfStatefulLayout} */(new StatefulLayout(
88
123
  toRaw(compiledLayout.value),
89
- toRaw(compiledLayout.value.skeletonTree),
124
+ toRaw(compiledLayout.value.skeletonTrees[compiledLayout.value.mainTree]),
90
125
  toRaw(fullOptions.value),
91
126
  toRaw(modelValue.value)
92
127
  ))
93
- statefulLayout.value = _statefulLayout
94
- onStatefulLayoutUpdate()
95
- _statefulLayout.events.on('update', () => {
96
- onStatefulLayoutUpdate()
97
- })
98
- emit('update:state', _statefulLayout)
99
- _statefulLayout.events.on('autofocus', () => {
100
- if (!el.value) return
101
- // @ts-ignore
102
- const autofocusNodeElement = el.value.querySelector('.vjsf-input--autofocus')
103
- if (autofocusNodeElement) {
104
- const autofocusInputElement = autofocusNodeElement.querySelector('input') ?? autofocusNodeElement.querySelector('textarea:not([style*="display: none"]')
105
- if (autofocusInputElement) autofocusInputElement.focus()
106
- }
107
- })
108
128
  }
109
129
 
110
130
  // case where options are updated from outside
111
131
  watch(fullOptions, (newOptions) => {
132
+ debug('watch fullOptions', fullOptions)
112
133
  if (statefulLayout.value) {
113
- statefulLayout.value.options = newOptions
134
+ debug(' -> update statefulLayout options')
135
+ statefulLayout.value.options = toRaw(newOptions)
114
136
  } else {
137
+ debug(' -> init statefulLayout')
115
138
  initStatefulLayout()
116
139
  }
117
140
  })
118
141
 
119
142
  // case where data is updated from outside
120
143
  watch(modelValue, (newData) => {
121
- if (statefulLayout.value && statefulLayout.value.data !== newData) statefulLayout.value.data = toRaw(newData)
144
+ const rawData = toRaw(newData)
145
+ if (statefulLayout.value && statefulLayout.value.data !== rawData) {
146
+ debug('modelValue changed from outside', rawData)
147
+ debug(' -> update statefulLayout data')
148
+ statefulLayout.value.data = toRaw(rawData)
149
+ }
122
150
  })
123
151
 
124
152
  // case where schema or compile options are updated from outside
125
153
  watch(compiledLayout, (newCompiledLayout) => {
154
+ debug('watch compiledLayout', newCompiledLayout)
155
+ debug(' -> init statefulLayout')
126
156
  initStatefulLayout()
127
157
  })
128
158
 
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 */
package/src/options.js ADDED
@@ -0,0 +1,65 @@
1
+ /** @type {import('./types.js').VjsfIcons} */
2
+ export const defaultIcons = {
3
+ // as much as possible with use standard vuetify aliases
4
+ add: '$plus',
5
+ calendar: '$calendar',
6
+ close: '$close',
7
+ edit: '$edit',
8
+ sortDown: '$sortDesc',
9
+ sortUp: '$sortAsc',
10
+ // codes are copied from here https://raw.githubusercontent.com/Templarian/MaterialDesign-JS/refs/heads/master/mdi.js
11
+ alert: 'svg:M13 14H11V9H13M13 18H11V16H13M1 21H23L12 2L1 21Z',
12
+ clock: 'svg:M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z',
13
+ delete: 'svg:M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z',
14
+ duplicate: 'svg:M11,17H4A2,2 0 0,1 2,15V3A2,2 0 0,1 4,1H16V3H4V15H11V13L15,16L11,19V17M19,21V7H8V13H6V7A2,2 0 0,1 8,5H19A2,2 0 0,1 21,7V21A2,2 0 0,1 19,23H8A2,2 0 0,1 6,21V19H8V21H19Z',
15
+ infoSymbol: 'svg:M11 9H13V7H11V9M11 17H13V11H11V17Z',
16
+ menu: 'svg:M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z',
17
+ sort: 'svg:M17.45,17.55L12,23L6.55,17.55L7.96,16.14L11,19.17V4.83L7.96,7.86L6.55,6.45L12,1L17.45,6.45L16.04,7.86L13,4.83V19.17L16.04,16.14L17.45,17.55Z'
18
+ }
19
+
20
+ export const defaultOptions = {
21
+ nodeComponents: {},
22
+ plugins: [],
23
+ pluginsOptions: {}
24
+ }
25
+
26
+ /**
27
+ *
28
+ * @param {Partial<import("./types.js").VjsfOptions> | null} options
29
+ * @param {any} form
30
+ * @param {number} width
31
+ * @param {import("vue").Slots} slots
32
+ * @param {Record<string, import('vue').Component>} defaultNodeComponents
33
+ * @param {(data: any) => void} onData
34
+ * @param {(statefulLayout: import('@json-layout/core').StatefulLayout) => void} onUpdate
35
+ * @param {(key: string) => void} onAutofocus
36
+ * @returns
37
+ */
38
+ export const getFullOptions = (options, form, width, slots, defaultNodeComponents, onData, onUpdate, onAutofocus) => {
39
+ const components = { ...options?.components }
40
+ const nodeComponents = { ...defaultNodeComponents, ...options?.nodeComponents }
41
+ if (options?.plugins) {
42
+ for (const plugin of options.plugins) {
43
+ components[plugin.info.name] = plugin.info
44
+ nodeComponents[plugin.info.name] = plugin.nodeComponent
45
+ }
46
+ }
47
+ const icons = { ...options?.icons, ...defaultIcons }
48
+
49
+ /** @type {import('./types.js').VjsfOptions} */
50
+ const fullOptions = {
51
+ ...defaultOptions,
52
+ readOnly: !!(form && (form.isDisabled.value || form.isReadonly.value)),
53
+ ...options,
54
+ onData,
55
+ onUpdate,
56
+ onAutofocus,
57
+ context: options?.context ? JSON.parse(JSON.stringify(options.context)) : {},
58
+ width: Math.round(width ?? 0),
59
+ vjsfSlots: { ...slots },
60
+ components,
61
+ nodeComponents,
62
+ icons
63
+ }
64
+ return fullOptions
65
+ }