@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,12 +1,18 @@
1
1
  <script setup>
2
- import { VExpansionPanels, VExpansionPanel, VExpansionPanelTitle, VExpansionPanelText, VContainer, VRow, VIcon } from 'vuetify/components'
3
- import { computed } from 'vue'
2
+ import { toRef } from 'vue'
3
+ import { VExpansionPanels, VExpansionPanel, VExpansionPanelTitle, VExpansionPanelText } from 'vuetify/components/VExpansionPanel'
4
+ import { VContainer, VRow } from 'vuetify/components/VGrid'
5
+ import { VIcon } from 'vuetify/components/VIcon'
4
6
  import { isSection } from '@json-layout/core'
5
7
  import Node from '../node.vue'
6
8
  import SectionHeader from '../fragments/section-header.vue'
7
- import { getCompProps } from '../../utils/index.js'
9
+ import ChildSubtitle from '../fragments/child-subtitle.vue'
10
+ import useNode from '../../composables/use-node.js'
11
+ import { useDefaults } from 'vuetify'
8
12
 
9
- defineProps({
13
+ useDefaults({}, 'VjsfExpansionPanels')
14
+
15
+ const props = defineProps({
10
16
  modelValue: {
11
17
  /** @type import('vue').PropType<import('../../types.js').VjsfExpansionPanelsNode> */
12
18
  type: Object,
@@ -19,11 +25,13 @@ defineProps({
19
25
  }
20
26
  })
21
27
 
28
+ const { compProps } = useNode(toRef(props, 'modelValue'), props.statefulLayout)
29
+
22
30
  </script>
23
31
 
24
32
  <template>
25
33
  <section-header :node="modelValue" />
26
- <v-expansion-panels v-bind="getCompProps(modelValue, true)">
34
+ <v-expansion-panels v-bind="compProps">
27
35
  <v-expansion-panel
28
36
  v-for="(child, i) of modelValue.children"
29
37
  :key="child.key"
@@ -34,14 +42,14 @@ defineProps({
34
42
  v-if="child.validated && (child.error || child.childError)"
35
43
  color="error"
36
44
  class="mr-2"
37
- >
38
- mdi-alert
39
- </v-icon>
45
+ :icon="statefulLayout.options.icons.alert"
46
+ />
40
47
  {{ child.layout.title ?? child.layout.label }}
41
48
  </v-expansion-panel-title>
42
49
  <v-expansion-panel-text>
43
50
  <v-container fluid>
44
- <v-row>
51
+ <child-subtitle :model-value="child" />
52
+ <v-row :dense="modelValue.options?.density === 'compact' || modelValue.options?.density === 'comfortable'">
45
53
  <node
46
54
  v-for="grandChild of isSection(child) ? child.children : [child]"
47
55
  :key="grandChild.fullKey"
@@ -1,7 +1,8 @@
1
1
  <script>
2
- import { defineComponent, h, computed } from 'vue'
3
- import { VFileInput } from 'vuetify/components'
4
- import { getInputProps, getCompSlots } from '../../utils/index.js'
2
+ import { defineComponent, h, computed, toRef } from 'vue'
3
+ import { VFileInput } from 'vuetify/components/VFileInput'
4
+ import useNode from '../../composables/use-node.js'
5
+ import { useDefaults } from 'vuetify'
5
6
 
6
7
  export default defineComponent({
7
8
  props: {
@@ -17,20 +18,23 @@ export default defineComponent({
17
18
  }
18
19
  },
19
20
  setup (props) {
21
+ useDefaults({}, 'VjsfFileInput')
22
+
23
+ const { inputProps, localData, compSlots } = useNode(
24
+ toRef(props, 'modelValue'), props.statefulLayout, { layoutPropsMap: ['placeholder', 'accept', 'multiple'] }
25
+ )
26
+
20
27
  const fieldProps = computed(() => {
21
- const fieldProps = getInputProps(props.modelValue, props.statefulLayout, ['placeholder', 'accept'])
22
- if (props.modelValue.layout.multiple) {
23
- fieldProps.multiple = true
24
- } else {
25
- fieldProps.modelValue = props.modelValue.data ? [props.modelValue.data] : props.modelValue.data
26
- fieldProps['onUpdate:modelValue'] = (/** @type string */value) => props.statefulLayout.input(props.modelValue, Array.isArray(value) ? value[0] : value)
28
+ const fieldProps = { ...inputProps.value }
29
+ if (fieldProps.multiple) console.error('File input doesn\'t support multiple inputs yet')
30
+ fieldProps['onUpdate:modelValue'] = (/** @type {File} */value) => {
31
+ props.statefulLayout.input(props.modelValue, value)
27
32
  }
28
33
  return fieldProps
29
34
  })
30
- const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
31
35
 
32
36
  // @ts-ignore
33
- return () => h(VFileInput, fieldProps.value, fieldSlots.value)
37
+ return () => h(VFileInput, { ...fieldProps.value, modelValue: localData.value }, compSlots.value)
34
38
  }
35
39
  })
36
40
 
@@ -1,10 +1,24 @@
1
1
  <script setup>
2
2
  import { watch, computed, ref } from 'vue'
3
- import { VList, VListItem, VListItemAction, VBtn, VMenu, VIcon, VSheet, VSpacer, VDivider, VRow, VListSubheader } from 'vuetify/components'
4
- import { isSection, clone } from '@json-layout/core'
3
+ import { useDefaults, useTheme } from 'vuetify'
4
+ import { VList, VListItem, VListItemAction, VListItemTitle, VListSubheader } from 'vuetify/components/VList'
5
+ import { VRow } from 'vuetify/components/VGrid'
6
+ import { VTextField } from 'vuetify/components/VTextField'
7
+ import { VSheet } from 'vuetify/components/VSheet'
8
+ import { VDivider } from 'vuetify/components/VDivider'
9
+ import { VIcon } from 'vuetify/components/VIcon'
10
+ import { VBtn } from 'vuetify/components/VBtn'
11
+ import { VMenu } from 'vuetify/components/VMenu'
12
+ import { VForm } from 'vuetify/components/VForm'
13
+ import { isSection, clone, getRegexp } from '@json-layout/core'
5
14
  import Node from '../node.vue'
6
- import { moveArrayItem } from '../../utils/index.js'
15
+ import { moveDataItem } from '../../utils/arrays.js'
7
16
  import useDnd from '../../composables/use-dnd.js'
17
+ import useCompDefaults from '../../composables/use-comp-defaults.js'
18
+
19
+ useDefaults({}, 'VjsfList')
20
+ const vSheetProps = useCompDefaults('VjsfList-VSheet', { border: true })
21
+ const theme = useTheme()
8
22
 
9
23
  const props = defineProps({
10
24
  modelValue: {
@@ -19,21 +33,32 @@ const props = defineProps({
19
33
  }
20
34
  })
21
35
 
36
+ // we access vjsfNode properties through computeds so that the parts without mutations do not trigger reactivity
37
+ // this is to leverage the immutability provided by immer in json-layout
38
+ const options = computed(() => props.modelValue.options)
39
+ const layout = computed(() => props.modelValue.layout)
40
+ const children = computed(() => props.modelValue.children)
41
+
22
42
  /* use composable for drag and drop */
23
43
  const { activeDnd, sortableArray, draggable, hovered, dragging, itemBind, handleBind } = useDnd(props.modelValue.children, () => {
24
- props.statefulLayout.input(props.modelValue, sortableArray.value.map((child) => child.data))
44
+ const newData = layout.value.indexed
45
+ ? sortableArray.value.reduce((a, child) => { a[child.key] = child.data; return a }, /** @type {Record<string, any>} */({}))
46
+ : sortableArray.value.map((child) => child.data)
47
+ props.statefulLayout.input(props.modelValue, newData)
25
48
  })
26
- watch(() => props.modelValue.children, (array) => { sortableArray.value = array })
49
+ watch(children, (array) => { sortableArray.value = array })
27
50
 
28
51
  /* manage hovered and edited items */
52
+ // const editedItem = computed(() => activatedItems.value[fullKey.value])
29
53
  const editedItem = computed(() => {
30
- return props.statefulLayout.activeItems[props.modelValue.fullKey]
54
+ return props.statefulLayout.activatedItems[props.modelValue.fullKey]
31
55
  })
56
+
32
57
  const menuOpened = ref(-1)
33
58
  const activeItem = computed(() => {
34
59
  if (
35
- props.modelValue.layout.listActions.includes('edit') &&
36
- props.modelValue.layout.listEditMode === 'inline-single' &&
60
+ layout.value.listActions.includes('edit') &&
61
+ layout.value.listEditMode === 'inline-single' &&
37
62
  editedItem.value !== undefined
38
63
  ) {
39
64
  return editedItem.value
@@ -44,35 +69,101 @@ const activeItem = computed(() => {
44
69
  })
45
70
 
46
71
  const buttonDensity = computed(() => {
47
- if (props.modelValue.options.density === 'default') return 'comfortable'
48
- return props.modelValue.options.density
72
+ if (options.value.density === 'default') return 'comfortable'
73
+ return options.value.density
49
74
  })
50
75
 
51
76
  const pushEmptyItem = () => {
52
77
  const newData = (props.modelValue.data ?? []).concat([undefined])
53
78
  props.statefulLayout.input(props.modelValue, newData)
54
- props.statefulLayout.activateItem(props.modelValue, newData.length - 1)
79
+ if (layout.value.listEditMode === 'inline-single') {
80
+ props.statefulLayout.activateItem(props.modelValue, newData.length - 1)
81
+ }
82
+ }
83
+
84
+ const newKey = ref('')
85
+ /** @type {import('vue').Ref<InstanceType<typeof import('vuetify/components/VForm').VForm> | null>} */
86
+ const newKeyForm = ref(null)
87
+ const pushEmptyIndexedItem = () => {
88
+ if (!newKey.value) return
89
+ if (!newKeyForm.value) return
90
+ if (!newKeyForm.value.isValid) return
91
+ const newData = { ...(props.modelValue.data ?? {}), [newKey.value]: null }
92
+ props.statefulLayout.input(props.modelValue, newData)
93
+ if (layout.value.listEditMode === 'inline-single') {
94
+ props.statefulLayout.activateItem(props.modelValue, Object.keys(newData).length - 1)
95
+ }
96
+ newKey.value = ''
97
+ newKeyForm.value?.reset()
98
+ }
99
+
100
+ /**
101
+ * @param {number} childIndex
102
+ */
103
+ const deleteItem = (childIndex) => {
104
+ if (layout.value.indexed) {
105
+ const oldData = /** @type {Record<string, any>} */(props.modelValue.data)
106
+ const keys = Object.keys(props.modelValue.data)
107
+ /** @type {Record<string, any>} */
108
+ const newData = {}
109
+ for (let i = 0; i < keys.length; i++) {
110
+ if (i !== childIndex) newData[keys[i]] = oldData[keys[i]]
111
+ }
112
+ props.statefulLayout.input(props.modelValue, newData)
113
+ } else {
114
+ const newData = [...props.modelValue.data.slice(0, childIndex), ...props.modelValue.data.slice(childIndex + 1)]
115
+ props.statefulLayout.input(props.modelValue, newData)
116
+ }
117
+
118
+ menuOpened.value = -1
119
+ }
120
+
121
+ /**
122
+ * @param {import('@json-layout/core').StateNode} child
123
+ * @param {number} childIndex
124
+ */
125
+ const duplicateItem = (child, childIndex) => {
126
+ const newData = [...props.modelValue.data.slice(0, childIndex), clone(child.data), ...props.modelValue.data.slice(childIndex)]
127
+ props.statefulLayout.input(props.modelValue, newData)
128
+ if (layout.value.listEditMode === 'inline-single') {
129
+ props.statefulLayout.activateItem(props.modelValue, childIndex + 1)
130
+ }
131
+ menuOpened.value = -1
55
132
  }
56
133
 
134
+ const itemBorderColor = computed(() => (/** @type {import('@json-layout/core').StateNode} */child, /** @type {number} */childIndex) => {
135
+ if (editedItem.value === childIndex) return theme.current.value.colors.primary
136
+ if (child.validated && (child.error || child.childError)) return theme.current.value.colors.error
137
+ if (options.value.readOnly) return 'transparent'
138
+ if (activeItem.value === childIndex) return theme.current.value.colors.primary
139
+ return 'transparent'
140
+ })
141
+
57
142
  </script>
58
143
 
59
144
  <template>
60
- <v-sheet :elevation="1">
61
- <v-list :density="modelValue.options.density">
145
+ <v-sheet v-bind="vSheetProps">
146
+ <v-list class="py-0">
62
147
  <v-list-subheader v-if="modelValue.layout.title">
63
148
  {{ modelValue.layout.title }}
64
149
  </v-list-subheader>
65
150
  <template
66
151
  v-for="(child, childIndex) of sortableArray"
67
- :key="props.modelValue.children.findIndex(c => c === child)"
152
+ :key="children.findIndex(c => c === child)"
68
153
  >
69
154
  <v-list-item
70
155
  v-bind="itemBind(childIndex)"
71
- :density="modelValue.options.density"
72
156
  :draggable="draggable === childIndex"
73
- :variant="editedItem === childIndex ? 'outlined' : 'flat'"
157
+ variant="flat"
158
+ :style="`border: 1px solid ${itemBorderColor(child, childIndex)}`"
74
159
  class="pa-1 vjsf-list-item"
75
160
  >
161
+ <v-list-item-title
162
+ v-if="modelValue.layout.indexed"
163
+ class="pl-4 pt-2"
164
+ >
165
+ {{ child.key }}
166
+ </v-list-item-title>
76
167
  <v-row class="ma-0">
77
168
  <node
78
169
  v-for="grandChild of isSection(child) ? child.children : [child]"
@@ -82,115 +173,152 @@ const pushEmptyItem = () => {
82
173
  />
83
174
  </v-row>
84
175
  <template
85
- v-if="activeItem === childIndex"
176
+ v-if="!modelValue.options.readOnly && modelValue.layout.listActions.length"
86
177
  #append
87
178
  >
88
- <div>
89
- <v-list-item-action
90
- v-if="modelValue.layout.listActions.includes('edit') && modelValue.layout.listEditMode === 'inline-single'"
91
- >
179
+ <div class="vjsf-list-item-actions-wrapper">
180
+ <v-list-item-action v-if="activeItem !== childIndex">
92
181
  <v-btn
93
- v-if="editedItem !== childIndex"
94
- :title="modelValue.messages.edit"
95
- icon="mdi-pencil"
182
+ style="visibility:hidden"
96
183
  variant="text"
97
- color="primary"
98
- :density="buttonDensity"
99
- @click="statefulLayout.activateItem(modelValue, childIndex)"
100
- />
101
- <v-btn
102
- v-else
103
- :title="modelValue.messages.edit"
104
- icon="mdi-pencil"
105
- variant="flat"
106
- color="primary"
107
184
  :density="buttonDensity"
108
- @click="statefulLayout.deactivateItem(modelValue)"
185
+ :icon="statefulLayout.options.icons.edit"
109
186
  />
110
187
  </v-list-item-action>
111
- <v-list-item-action
112
- v-if="editedItem === undefined && modelValue.layout.listActions.includes('sort') && activeDnd"
113
- >
114
- <v-btn
115
- :title="modelValue.messages.sort"
116
- icon="mdi-arrow-up-down"
117
- variant="plain"
118
- :density="buttonDensity"
119
- v-bind="handleBind(childIndex)"
120
- />
121
- </v-list-item-action>
122
- <v-list-item-action
123
- v-if="editedItem === undefined && (modelValue.layout.listActions.includes('delete') || modelValue.layout.listActions.includes('duplicate') || modelValue.layout.listActions.includes('sort'))"
124
- >
125
- <v-menu
126
- location="bottom end"
127
- @update:model-value="value => {menuOpened = value ? childIndex : -1}"
188
+ <template v-else>
189
+ <v-list-item-action
190
+ v-if="modelValue.layout.listActions.includes('edit') && modelValue.layout.listEditMode === 'inline-single'"
128
191
  >
129
- <template #activator="{props: activatorProps}">
130
- <v-btn
131
- v-bind="activatorProps"
132
- icon="mdi-dots-vertical"
133
- variant="plain"
134
- slim
135
- :density="buttonDensity"
136
- />
137
- </template>
138
- <v-list :density="modelValue.options.density">
139
- <v-list-item
140
- v-if="modelValue.layout.listActions.includes('delete')"
141
- base-color="warning"
142
- @click="statefulLayout.input(modelValue, [...modelValue.data.slice(0, childIndex), ...modelValue.data.slice(childIndex + 1)])"
143
- >
144
- <template #prepend>
145
- <v-icon icon="mdi-delete" />
146
- </template>
147
- {{ modelValue.messages.delete }}
148
- </v-list-item>
149
- <v-list-item
150
- v-if="modelValue.layout.listActions.includes('duplicate')"
151
- @click="statefulLayout.input(modelValue, [...modelValue.data.slice(0, childIndex), clone(child.data), ...modelValue.data.slice(childIndex)])"
152
- >
153
- <template #prepend>
154
- <v-icon icon="mdi-content-duplicate" />
155
- </template>
156
- {{ modelValue.messages.duplicate }}
157
- </v-list-item>
158
- <v-list-item
159
- v-if="modelValue.layout.listActions.includes('sort')"
160
- @click="statefulLayout.input(modelValue, moveArrayItem(modelValue.data, childIndex, childIndex - 1))"
161
- >
162
- <template #prepend>
163
- <v-icon icon="mdi-arrow-up" />
164
- </template>
165
- {{ modelValue.messages.up }}
166
- </v-list-item>
167
- <v-list-item
168
- v-if="modelValue.layout.listActions.includes('sort')"
169
- @click="statefulLayout.input(modelValue, moveArrayItem(modelValue.data, childIndex, childIndex + 1))"
170
- >
171
- <template #prepend>
172
- <v-icon icon="mdi-arrow-down" />
173
- </template>
174
- {{ modelValue.messages.down }}
175
- </v-list-item>
176
- </v-list>
177
- </v-menu>
178
- </v-list-item-action>
192
+ <v-btn
193
+ v-if="editedItem !== childIndex"
194
+ :title="modelValue.messages.edit"
195
+ :icon="statefulLayout.options.icons.edit"
196
+ variant="text"
197
+ color="primary"
198
+ :density="buttonDensity"
199
+ @click="statefulLayout.activateItem(modelValue, childIndex)"
200
+ />
201
+ <v-btn
202
+ v-else
203
+ :title="modelValue.messages.close"
204
+ :icon="statefulLayout.options.icons.edit"
205
+ variant="flat"
206
+ color="primary"
207
+ :density="buttonDensity"
208
+ @click="statefulLayout.deactivateItem(modelValue)"
209
+ />
210
+ </v-list-item-action>
211
+ <v-list-item-action
212
+ v-if="editedItem === undefined && modelValue.layout.listActions.includes('sort') && activeDnd"
213
+ >
214
+ <v-btn
215
+ :title="modelValue.messages.sort"
216
+ :icon="statefulLayout.options.icons.sort"
217
+ variant="plain"
218
+ :density="buttonDensity"
219
+ v-bind="handleBind(childIndex)"
220
+ />
221
+ </v-list-item-action>
222
+ <v-list-item-action
223
+ v-if="editedItem === undefined && (modelValue.layout.listActions.includes('delete') || modelValue.layout.listActions.includes('duplicate') || modelValue.layout.listActions.includes('sort'))"
224
+ >
225
+ <v-menu
226
+ location="bottom end"
227
+ @update:model-value="value => {menuOpened = value ? childIndex : -1}"
228
+ >
229
+ <template #activator="{props: activatorProps}">
230
+ <v-btn
231
+ v-bind="activatorProps"
232
+ :icon="statefulLayout.options.icons.menu"
233
+ variant="plain"
234
+ slim
235
+ :density="buttonDensity"
236
+ />
237
+ </template>
238
+ <v-list>
239
+ <v-list-item
240
+ v-if="modelValue.layout.listActions.includes('delete')"
241
+ base-color="warning"
242
+ @click="deleteItem(childIndex)"
243
+ >
244
+ <template #prepend>
245
+ <v-icon :icon="statefulLayout.options.icons.delete" />
246
+ </template>
247
+ {{ modelValue.messages.delete }}
248
+ </v-list-item>
249
+ <v-list-item
250
+ v-if="modelValue.layout.listActions.includes('duplicate')"
251
+ @click="duplicateItem(child, childIndex)"
252
+ >
253
+ <template #prepend>
254
+ <v-icon :icon="statefulLayout.options.icons.duplicate" />
255
+ </template>
256
+ {{ modelValue.messages.duplicate }}
257
+ </v-list-item>
258
+ <v-list-item
259
+ v-if="modelValue.layout.listActions.includes('sort')"
260
+ @click="statefulLayout.input(modelValue, moveDataItem(modelValue.data, childIndex, childIndex - 1))"
261
+ >
262
+ <template #prepend>
263
+ <v-icon :icon="statefulLayout.options.icons.sortUp" />
264
+ </template>
265
+ {{ modelValue.messages.up }}
266
+ </v-list-item>
267
+ <v-list-item
268
+ v-if="modelValue.layout.listActions.includes('sort')"
269
+ @click="statefulLayout.input(modelValue, moveDataItem(modelValue.data, childIndex, childIndex + 1))"
270
+ >
271
+ <template #prepend>
272
+ <v-icon :icon="statefulLayout.options.icons.sortDown" />
273
+ </template>
274
+ {{ modelValue.messages.down }}
275
+ </v-list-item>
276
+ </v-list>
277
+ </v-menu>
278
+ </v-list-item-action>
279
+ </template>
179
280
  </div>
180
281
  </template>
181
282
  </v-list-item>
182
283
  <v-divider v-if="childIndex < modelValue.children.length - 1" />
183
284
  </template>
184
- <v-list-item v-if="!modelValue.options.readOnly && modelValue.layout.listActions.includes('add')">
185
- <v-spacer />
186
- <v-btn
187
- color="primary"
188
- :density="modelValue.options.density"
189
- @click="pushEmptyItem"
190
- >
191
- {{ modelValue.messages.addItem }}
192
- </v-btn>
193
- <v-spacer />
285
+ <v-list-item
286
+ v-if="!modelValue.options.readOnly && modelValue.layout.listActions.includes('add')"
287
+ class="py-2"
288
+ >
289
+ <template v-if="modelValue.layout.indexed">
290
+ <v-form
291
+ ref="newKeyForm"
292
+ style="max-width: 250px;"
293
+ @submit.prevent
294
+ >
295
+ <v-text-field
296
+ v-model="newKey"
297
+ variant="outlined"
298
+ :placeholder="modelValue.messages.addItem"
299
+ hide-details
300
+ :rules="[(/** @type {string} */v) => !modelValue.children.some(c => c.key === v), v => !v || !!modelValue.layout.indexed?.some(pattern => v.match(getRegexp(pattern)))]"
301
+ @keypress.enter="pushEmptyIndexedItem"
302
+ >
303
+ <template #append>
304
+ <v-icon
305
+ color="primary"
306
+ size="large"
307
+ :icon="statefulLayout.options.icons.add"
308
+ @click="pushEmptyIndexedItem"
309
+ />
310
+ </template>
311
+ </v-text-field>
312
+ </v-form>
313
+ </template>
314
+ <template v-else>
315
+ <v-btn
316
+ color="primary"
317
+ @click="pushEmptyItem"
318
+ >
319
+ {{ modelValue.messages.addItem }}
320
+ </v-btn>
321
+ </template>
194
322
  </v-list-item>
195
323
  </v-list>
196
324
  </v-sheet>
@@ -201,4 +329,10 @@ const pushEmptyItem = () => {
201
329
  height: 100%;
202
330
  align-items: start;
203
331
  }
332
+ .vjsf-list-item .v-list-item__content {
333
+ padding-right: 4px;
334
+ }
335
+ .vjsf-list-item-actions-wrapper {
336
+ /*margin: -4px;*/
337
+ }
204
338
  </style>
@@ -1,7 +1,9 @@
1
1
  <script>
2
- import { defineComponent, h, computed, shallowRef, ref } from 'vue'
3
- import { VCombobox } from 'vuetify/components'
4
- import { getInputProps, getCompSlots } from '../../utils/index.js'
2
+ import { defineComponent, h, computed, toRef } from 'vue'
3
+ import { VCombobox } from 'vuetify/components/VCombobox'
4
+ import useNode from '../../composables/use-node.js'
5
+ import useGetItems from '../../composables/use-get-items.js'
6
+ import { useDefaults } from 'vuetify'
5
7
 
6
8
  export default defineComponent({
7
9
  props: {
@@ -17,55 +19,32 @@ export default defineComponent({
17
19
  }
18
20
  },
19
21
  setup (props) {
20
- /** @type import('vue').Ref<import('@json-layout/vocabulary').SelectItems> */
21
- const items = shallowRef(props.modelValue.layout.items ?? [])
22
- /** @type import('vue').Ref<boolean> */
23
- const loading = ref(false)
22
+ useDefaults({}, 'VjsfCombobox')
24
23
 
25
- /** @type import('@json-layout/core').StateTree | null */
26
- let lastStateTree = null
27
- /** @type Record<string, any> | null */
28
- let lastContext = null
29
-
30
- const hasItems = computed(() => {
31
- return !!(props.modelValue.layout.items || props.modelValue.layout.getItems)
32
- })
33
-
34
- const refresh = async () => {
35
- if (props.modelValue.layout.items) return
36
- if (props.statefulLayout.stateTree === lastStateTree && props.statefulLayout.options.context === lastContext) return
37
- lastStateTree = props.statefulLayout.stateTree
38
- lastContext = props.statefulLayout.options.context ?? null
39
- if (hasItems.value) {
40
- loading.value = true
41
- items.value = await props.statefulLayout.getItems(props.modelValue)
42
- loading.value = false
43
- }
44
- }
45
-
46
- if (!props.modelValue.layout.items) {
47
- refresh()
48
- }
24
+ const nodeRef = toRef(props, 'modelValue')
25
+ const getItems = useGetItems(nodeRef, props.statefulLayout)
26
+ const { inputProps, compSlots, localData, layout, options } = useNode(nodeRef, props.statefulLayout, { bindData: false, layoutPropsMap: ['step', 'min', 'max'] })
49
27
 
50
28
  const fieldProps = computed(() => {
51
- const fieldProps = getInputProps(props.modelValue, props.statefulLayout, ['step', 'min', 'max'])
29
+ const fieldProps = { ...inputProps.value }
52
30
  fieldProps.type = 'number'
53
- fieldProps.loading = loading.value
54
- if (hasItems.value) fieldProps.items = items.value
55
- if (props.modelValue.options.readOnly) fieldProps.menuProps = { modelValue: false }
56
- if (props.modelValue.layout.multiple) {
31
+ fieldProps.returnObject = false
32
+ if (options.value.readOnly) fieldProps.menuProps = { modelValue: false }
33
+ if (getItems.hasItems.value) {
34
+ fieldProps.items = getItems.items.value
35
+ fieldProps.loading = getItems.loading.value
36
+ }
37
+ if (layout.value.multiple) {
57
38
  fieldProps.multiple = true
58
39
  fieldProps.chips = true
59
40
  fieldProps.closableChips = true
60
41
  }
61
- fieldProps['onUpdate:menu'] = () => refresh()
62
42
  fieldProps['onUpdate:modelValue'] = (/** @type string[] */value) => props.statefulLayout.input(props.modelValue, value && value.map(Number))
63
43
  return fieldProps
64
44
  })
65
- const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
66
45
 
67
46
  // @ts-ignore
68
- return () => h(VCombobox, fieldProps.value, fieldSlots.value)
47
+ return () => h(VCombobox, { ...fieldProps.value, modelValue: localData.value }, compSlots.value)
69
48
  }
70
49
  })
71
50