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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/package.json +10 -4
  2. package/src/compat/v2.js +44 -6
  3. package/src/compile/index.js +24 -5
  4. package/src/components/fragments/help-message.vue +48 -0
  5. package/src/components/fragments/section-header.vue +4 -1
  6. package/src/components/fragments/select-item-icon.vue +28 -0
  7. package/src/components/fragments/select-item.vue +43 -0
  8. package/src/components/fragments/select-selection.vue +35 -0
  9. package/src/components/fragments/text-field-menu.vue +1 -1
  10. package/src/components/node.vue +10 -1
  11. package/src/components/nodes/autocomplete.vue +95 -0
  12. package/src/components/nodes/combobox.vue +73 -0
  13. package/src/components/nodes/date-picker.vue +2 -2
  14. package/src/components/nodes/markdown.vue +29 -0
  15. package/src/components/nodes/number-combobox.vue +73 -0
  16. package/src/components/nodes/number-field.vue +1 -2
  17. package/src/components/nodes/select.vue +70 -50
  18. package/src/components/options.js +1 -1
  19. package/src/components/tree.vue +2 -1
  20. package/src/components/types.ts +4 -0
  21. package/src/components/vjsf.vue +36 -9
  22. package/src/index.js +2 -1
  23. package/src/utils/props.js +9 -1
  24. package/types/compat/v2.d.ts.map +1 -1
  25. package/types/compile/index.d.ts.map +1 -1
  26. package/types/components/fragments/help-message.vue.d.ts +8 -0
  27. package/types/components/fragments/help-message.vue.d.ts.map +1 -0
  28. package/types/components/fragments/select-item-icon.vue.d.ts +15 -0
  29. package/types/components/fragments/select-item-icon.vue.d.ts.map +1 -0
  30. package/types/components/fragments/select-item.vue.d.ts +12 -0
  31. package/types/components/fragments/select-item.vue.d.ts.map +1 -0
  32. package/types/components/fragments/select-selection.vue.d.ts +12 -0
  33. package/types/components/fragments/select-selection.vue.d.ts.map +1 -0
  34. package/types/components/nodes/autocomplete.vue.d.ts +27 -0
  35. package/types/components/nodes/autocomplete.vue.d.ts.map +1 -0
  36. package/types/components/nodes/combobox.vue.d.ts +27 -0
  37. package/types/components/nodes/combobox.vue.d.ts.map +1 -0
  38. package/types/components/nodes/markdown.vue.d.ts +27 -0
  39. package/types/components/nodes/markdown.vue.d.ts.map +1 -0
  40. package/types/components/nodes/number-combobox.vue.d.ts +27 -0
  41. package/types/components/nodes/number-combobox.vue.d.ts.map +1 -0
  42. package/types/components/nodes/select.vue.d.ts +25 -8
  43. package/types/components/nodes/select.vue.d.ts.map +1 -1
  44. package/types/components/options.d.ts +2 -2
  45. package/types/components/options.d.ts.map +1 -1
  46. package/types/components/tree.vue.d.ts +2 -2
  47. package/types/components/types.d.ts +5 -1
  48. package/types/components/types.d.ts.map +1 -1
  49. package/types/components/vjsf.vue.d.ts +4 -1
  50. package/types/components/vjsf.vue.d.ts.map +1 -1
  51. package/types/index.d.ts +2 -1
  52. package/types/index.d.ts.map +1 -1
  53. package/types/utils/props.d.ts +2 -1
  54. package/types/utils/props.d.ts.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koumoul/vjsf",
3
- "version": "3.0.0-alpha.0",
3
+ "version": "3.0.0-alpha.1",
4
4
  "description": "Generate forms for the vuetify UI library (vuejs) based on annotated JSON schemas.",
5
5
  "scripts": {
6
6
  "test": "vitest",
@@ -43,21 +43,27 @@
43
43
  },
44
44
  "peerDependencies": {
45
45
  "vue": "^3.3.4",
46
- "vuetify": "^3.3.20"
46
+ "vuetify": "^3.4.2"
47
47
  },
48
48
  "dependencies": {
49
- "@json-layout/core": "0.1.0",
50
- "@json-layout/vocabulary": "0.1.0",
49
+ "@json-layout/core": "0.2.0",
50
+ "@json-layout/vocabulary": "0.2.0",
51
51
  "@vueuse/core": "^10.5.0",
52
52
  "debug": "^4.3.4",
53
+ "easymde": "^2.18.0",
53
54
  "ejs": "^3.1.9",
54
55
  "rfdc": "^1.3.0"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@types/debug": "^4.1.8",
58
59
  "@types/ejs": "^3.1.2",
60
+ "relative-deps": "^1.0.7",
59
61
  "vitest": "^0.34.5",
60
62
  "vue": "^3.3.4",
61
63
  "vue-tsc": "^1.8.15"
64
+ },
65
+ "relativeDependencies": {
66
+ "@json-layout/core": "../../json-layout/core/",
67
+ "@json-layout/vocabulary": "../../json-layout/vocabulary/"
62
68
  }
63
69
  }
package/src/compat/v2.js CHANGED
@@ -2,6 +2,7 @@ import ajvModule from 'ajv'
2
2
  import rfdc from 'rfdc'
3
3
  import addFormats from 'ajv-formats'
4
4
  import { resolveRefs } from '@json-layout/core'
5
+ import { isPartialGetItemsObj } from '@json-layout/vocabulary'
5
6
 
6
7
  // @ts-ignore
7
8
  const Ajv = /** @type {typeof ajvModule.default} */ (ajvModule)
@@ -10,24 +11,61 @@ const clone = rfdc()
10
11
 
11
12
  const processFragment = (/** @type {import("ajv").SchemaObject} */schema) => {
12
13
  if (!schema.layout) {
13
- schema.layout = {}
14
+ /** @type import('@json-layout/vocabulary').PartialCompObject */
15
+ const layout = {}
16
+
17
+ if (schema['x-display'] === 'icon' && (schema.enum || schema.items?.enum)) {
18
+ layout.getItems = { itemIcon: schema['x-itemIcon'] || 'data.value' }
19
+ delete schema['x-display']
20
+ }
21
+
14
22
  if (schema['x-display']) {
15
- schema.layout.comp = schema['x-display']
23
+ layout.comp = schema['x-display']
16
24
  delete schema['x-display']
17
25
  }
18
26
 
19
27
  if (schema.format === 'hexcolor') {
20
- schema.layout.comp = 'color-picker'
28
+ layout.comp = 'color-picker'
21
29
  delete schema.format
22
30
  }
23
31
 
24
32
  if (schema['x-props']) {
25
- schema.layout.props = schema['x-props']
33
+ layout.props = schema['x-props']
26
34
  delete schema['x-props']
27
35
  }
28
36
 
29
- if (Object.keys(schema.layout).length === 1 && 'comp' in schema.layout) {
30
- schema.layout = schema.layout.comp
37
+ if (schema['x-fromData']) {
38
+ layout.comp = 'select'
39
+ layout.getItems = { expr: schema['x-fromData'] }
40
+ delete schema['x-fromData']
41
+ }
42
+
43
+ if (schema['x-fromUrl']) {
44
+ /** @type string */
45
+ let url = schema['x-fromUrl']
46
+ for (const expressionMatch of url.matchAll(/\{(.*?)\}/g)) {
47
+ if (expressionMatch[1] !== 'q') url = url.replace(expressionMatch[0], '$' + expressionMatch[0])
48
+ }
49
+ layout.getItems = { url }
50
+ delete schema['x-fromUrl']
51
+ }
52
+ if (layout.getItems && isPartialGetItemsObj(layout.getItems)) {
53
+ if (schema['x-itemKey']) layout.getItems.itemKey = `data["${schema['x-itemKey']}"]`
54
+ if (schema['x-itemTitle']) layout.getItems.itemTitle = `data["${schema['x-itemTitle']}"]`
55
+ if (schema['x-itemValue']) layout.getItems.itemValue = `data["${schema['x-itemValue']}"]`
56
+ if (schema['x-itemIcon']) layout.getItems.itemIcon = `data["${schema['x-itemIcon']}"]`
57
+ if (schema['x-itemsProp']) layout.getItems.itemsResults = `data["${schema['x-itemsProp']}"]`
58
+ delete schema['x-itemKey']
59
+ delete schema['x-itemTitle']
60
+ delete schema['x-itemValue']
61
+ delete schema['x-itemsProp']
62
+ }
63
+
64
+ // compact the layout keyword if possible
65
+ if (Object.keys(layout).length === 1 && 'comp' in layout) {
66
+ schema.layout = layout.comp
67
+ } else if (Object.keys(layout).length > 0) {
68
+ schema.layout = layout
31
69
  }
32
70
  }
33
71
 
@@ -4,12 +4,34 @@ import path from 'path'
4
4
  import ejs from 'ejs'
5
5
  import { compile as compileLayout } from '@json-layout/core'
6
6
  import { serialize as serializeCompiledLayout } from '@json-layout/core/src/compile/serialize'
7
- import { isCompObject, isSwitchStruct } from '@json-layout/vocabulary'
7
+ import { childIsCompObject, isCompObject, isSwitchStruct } from '@json-layout/vocabulary'
8
8
 
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
10
 
11
11
  const template = readFileSync(path.join(__dirname, 'v-jsf-compiled.vue.ejs'), 'utf8')
12
12
 
13
+ /**
14
+ *
15
+ * @param {Set<string>} comps
16
+ * @param {import('@json-layout/vocabulary').NormalizedLayout} layout
17
+ */
18
+ function listComps (comps, layout) {
19
+ if (isCompObject(layout)) {
20
+ comps.add(layout.comp)
21
+ if (layout.children) {
22
+ for (const child of /** @type {import('@json-layout/vocabulary').Children} */(layout.children)) {
23
+ if (childIsCompObject(child)) {
24
+ listComps(comps, child)
25
+ }
26
+ }
27
+ }
28
+ } else if (isSwitchStruct(layout)) {
29
+ for (const switchCase of layout.switch) {
30
+ listComps(comps, switchCase)
31
+ }
32
+ }
33
+ }
34
+
13
35
  /**
14
36
  * @param {object} schema
15
37
  * @param {string} baseImport
@@ -21,10 +43,7 @@ export function compile (schema, baseImport = '@koumoul/vjsf/components') {
21
43
  /** @type Set<string> */
22
44
  const comps = new Set([])
23
45
  for (const layout of Object.values(compiledLayout.normalizedLayouts)) {
24
- if (isCompObject(layout)) comps.add(layout.comp)
25
- if (isSwitchStruct(layout)) {
26
- for (const switchCase of layout.switch) comps.add(switchCase.comp)
27
- }
46
+ listComps(comps, layout)
28
47
  }
29
48
  comps.delete('none')
30
49
  const code = ejs.render(template, { compiledLayoutCode, comps, baseImport })
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <div class="vjsf-help-message">
3
+ <v-slide-x-reverse-transition>
4
+ <v-alert
5
+ v-show="show"
6
+ color="info"
7
+ >
8
+ <div v-html="node.layout.help" />
9
+ </v-alert>
10
+ </v-slide-x-reverse-transition>
11
+ <v-btn
12
+ color="info"
13
+ class="vjsf-help-message-toggle"
14
+ :icon="show ? 'mdi-close-circle' : 'mdi-information'"
15
+ density="compact"
16
+ :title="show ? '' : node.messages.showHelp"
17
+ @click="show = !show"
18
+ />
19
+ </div>
20
+ </template>
21
+
22
+ <script setup>
23
+ import { VAlert, VBtn } from 'vuetify/components'
24
+ import { ref } from 'vue'
25
+
26
+ defineProps({
27
+ node: {
28
+ /** @type import('vue').PropType<import('../types.js').VjsfNode> */
29
+ type: Object,
30
+ required: true
31
+ }
32
+ })
33
+
34
+ const show = ref(false)
35
+ </script>
36
+
37
+ <style>
38
+ .vjsf-help-message {
39
+ position: relative;
40
+ min-height: 10px;
41
+ }
42
+ .vjsf-help-message-toggle {
43
+ position: absolute;
44
+ top: -10px;
45
+ right: -4px;
46
+ z-index: 1;
47
+ }
48
+ </style>
@@ -26,7 +26,10 @@ const titleClass = computed(() => {
26
26
  </script>
27
27
 
28
28
  <template>
29
- <div :class="`mb-${titleDepthBase - node.options.titleDepth} mt-${titleDepthBase - node.options.titleDepth}`">
29
+ <div
30
+ v-if="node.layout.title ?? node.layout.subtitle ?? (node.error && node.validated)"
31
+ :class="`mb-${titleDepthBase - node.options.titleDepth} mt-${titleDepthBase - node.options.titleDepth}`"
32
+ >
30
33
  <component
31
34
  :is="`h${node.options.titleDepth}`"
32
35
  v-if="node.layout.title"
@@ -0,0 +1,28 @@
1
+ <script>
2
+ // cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/components/VSelect/VSelect.tsx#L374
3
+
4
+ import { defineComponent, h, computed } from 'vue'
5
+ import { VIcon } from 'vuetify/components'
6
+
7
+ export default defineComponent({
8
+ props: {
9
+ icon: {
10
+ type: String,
11
+ required: true
12
+ }
13
+ },
14
+ setup (props) {
15
+ const isUrl = computed(() => props.icon.startsWith('http://') || props.icon.startsWith('https://'))
16
+ const isSVG = computed(() => props.icon.startsWith('<?xml') || props.icon.startsWith('<svg'))
17
+ return () => {
18
+ if (isUrl.value) {
19
+ return h('img', { src: props.icon, style: 'height:100%;width:100%;' })
20
+ } else if (isSVG.value) {
21
+ return h('div', { innerHTML: props.icon.replace('<svg ', '<svg class="v-icon__svg" '), class: 'v-icon' })
22
+ } else {
23
+ return h(VIcon, null, props.icon)
24
+ }
25
+ }
26
+ }
27
+ })
28
+ </script>
@@ -0,0 +1,43 @@
1
+ <script setup>
2
+ // cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/components/VSelect/VSelect.tsx#L374
3
+
4
+ import { VListItem, VCheckboxBtn } from 'vuetify/components'
5
+ import VSelectItemIcon from './select-item-icon.vue'
6
+
7
+ defineProps({
8
+ multiple: {
9
+ type: Boolean,
10
+ default: false
11
+ },
12
+ itemProps: {
13
+ type: Object,
14
+ required: true
15
+ },
16
+ item: {
17
+ /** @type import('vue').PropType<import('@json-layout/vocabulary').SelectItem> */
18
+ type: Object,
19
+ required: true
20
+ }
21
+ })
22
+ </script>
23
+
24
+ <template>
25
+ <v-list-item v-bind="itemProps">
26
+ <template
27
+ v-if="item.icon || multiple"
28
+ #prepend="{isSelected}"
29
+ >
30
+ <v-checkbox-btn
31
+ v-if="multiple"
32
+ :key="item.key"
33
+ :ripple="false"
34
+ tabindex="-1"
35
+ :model-value="isSelected"
36
+ />
37
+ <v-select-item-icon
38
+ v-if="item.icon"
39
+ :icon="item.icon"
40
+ />
41
+ </template>
42
+ </v-list-item>
43
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup>
2
+ // cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/components/VSelect/VSelect.tsx#L453
3
+
4
+ import VSelectItemIcon from './select-item-icon.vue'
5
+
6
+ defineProps({
7
+ multiple: {
8
+ type: Boolean,
9
+ default: false
10
+ },
11
+ last: {
12
+ type: Boolean,
13
+ default: false
14
+ },
15
+ item: {
16
+ /** @type import('vue').PropType<import('@json-layout/vocabulary').SelectItem> */
17
+ type: Object,
18
+ required: true
19
+ }
20
+ })
21
+ </script>
22
+
23
+ <template>
24
+ <span class="v-select__selection-text">
25
+ <v-select-item-icon
26
+ v-if="item.icon"
27
+ :icon="item.icon"
28
+ />
29
+ {{ item.title }}
30
+ <span
31
+ v-if="multiple && !last"
32
+ class="v-select__selection-comma"
33
+ >,</span>
34
+ </span>
35
+ </template>
@@ -22,7 +22,7 @@ const props = defineProps({
22
22
  })
23
23
 
24
24
  const fieldProps = computed(() => {
25
- const fieldProps = getInputProps(props.modelValue, props.statefulLayout, false)
25
+ const fieldProps = getInputProps(props.modelValue, props.statefulLayout, [], false)
26
26
  fieldProps.readonly = true
27
27
  return fieldProps
28
28
  })
@@ -1,5 +1,7 @@
1
1
  <script setup>
2
- import nodeSlot from './fragments/node-slot.vue'
2
+ import { VCol } from 'vuetify/components'
3
+ import NodeSlot from './fragments/node-slot.vue'
4
+ import HelpMessage from './fragments/help-message.vue'
3
5
 
4
6
  defineProps({
5
7
  modelValue: {
@@ -25,6 +27,7 @@ const beforeAfterClasses = {
25
27
  <template>
26
28
  <v-col
27
29
  :cols="modelValue.cols"
30
+ :class="`vjsf-node vjsf-node-${modelValue.layout.comp}`"
28
31
  >
29
32
  <node-slot
30
33
  v-if="modelValue.layout.slots?.before"
@@ -35,6 +38,11 @@ const beforeAfterClasses = {
35
38
  :class="beforeAfterClasses[/** @type import('./types.js').VjsfOptions */(modelValue.options).density]"
36
39
  />
37
40
 
41
+ <help-message
42
+ v-if="modelValue.layout.help"
43
+ :node="modelValue"
44
+ :class="beforeAfterClasses[/** @type import('./types.js').VjsfOptions */(modelValue.options).density]"
45
+ />
38
46
  <node-slot
39
47
  v-if="modelValue.layout.slots?.component"
40
48
  key="component"
@@ -48,6 +56,7 @@ const beforeAfterClasses = {
48
56
  :model-value="modelValue"
49
57
  :stateful-layout="statefulLayout"
50
58
  />
59
+
51
60
  <node-slot
52
61
  v-if="modelValue.layout.slots?.after"
53
62
  key="after"
@@ -0,0 +1,95 @@
1
+ <script>
2
+ import { VAutocomplete } from 'vuetify/components'
3
+ import { defineComponent, computed, ref, shallowRef, h } from 'vue'
4
+ import { getInputProps } from '../../utils/props.js'
5
+ import { getCompSlots } from '../../utils/slots.js'
6
+ import SelectItem from '../fragments/select-item.vue'
7
+ import SelectSelection from '../fragments/select-selection.vue'
8
+
9
+ export default defineComponent({
10
+ props: {
11
+ modelValue: {
12
+ /** @type import('vue').PropType<import('../types.js').VjsfSelectNode> */
13
+ type: Object,
14
+ required: true
15
+ },
16
+ statefulLayout: {
17
+ /** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
18
+ type: Object,
19
+ required: true
20
+ }
21
+ },
22
+ setup (props) {
23
+ /** @type import('vue').ShallowRef<import('@json-layout/vocabulary').SelectItems> */
24
+ const items = shallowRef([])
25
+ /** @type import('vue').Ref<boolean> */
26
+ const loading = ref(false)
27
+ /** @type import('vue').Ref<string> */
28
+ const search = ref('')
29
+
30
+ const fieldProps = computed(() => {
31
+ const fieldProps = getInputProps(props.modelValue, props.statefulLayout, ['multiple'])
32
+ if (props.modelValue.options.readOnly) fieldProps.menuProps = { modelValue: false }
33
+ fieldProps.noFilter = true
34
+ fieldProps['onUpdate:search'] = (/** @type string */searchValue) => {
35
+ search.value = searchValue
36
+ refresh()
37
+ }
38
+ fieldProps['onUpdate:menu'] = refresh
39
+ fieldProps.items = items.value
40
+ fieldProps.loading = loading.value
41
+ return fieldProps
42
+ })
43
+
44
+ /** @type import('@json-layout/core').StateTree | null */
45
+ let lastStateTree = null
46
+ /** @type Record<string, any> | null */
47
+ let lastContext = null
48
+ /** @type string */
49
+ let lastSearch = ''
50
+
51
+ const refresh = async () => {
52
+ if (props.statefulLayout.stateTree === lastStateTree && props.statefulLayout.options.context === lastContext && search.value === lastSearch) return
53
+ loading.value = true
54
+ items.value = await props.statefulLayout.getItems(props.modelValue, search.value)
55
+ lastStateTree = props.statefulLayout.stateTree
56
+ lastContext = props.statefulLayout.options.context ?? null
57
+ lastSearch = search.value
58
+ loading.value = false
59
+ }
60
+
61
+ if (!props.modelValue.layout.items) {
62
+ refresh()
63
+ }
64
+
65
+ const fieldSlots = computed(() => {
66
+ const slots = getCompSlots(props.modelValue, props.statefulLayout)
67
+ if (!slots.item) {
68
+ slots.item = (/** @type {any} */ context) => h(SelectItem, {
69
+ multiple: props.modelValue.layout.multiple,
70
+ itemProps: context.props,
71
+ item: context.item.raw
72
+ })
73
+ }
74
+ if (!slots.selection) {
75
+ slots.selection = (/** @type {any} */ context) => h(SelectSelection, {
76
+ multiple: props.modelValue.layout.multiple,
77
+ last: props.modelValue.layout.multiple && context.index === props.modelValue.data.length - 1,
78
+ item: context.item.raw
79
+ })
80
+ }
81
+ return slots
82
+ })
83
+
84
+ // @ts-ignore
85
+ return () => h(VAutocomplete, fieldProps.value, fieldSlots.value)
86
+ }
87
+ })
88
+
89
+ </script>
90
+
91
+ <template>
92
+ <v-select
93
+ v-bind="fieldProps"
94
+ />
95
+ </template>
@@ -0,0 +1,73 @@
1
+ <script>
2
+ import { defineComponent, h, computed, shallowRef, ref } from 'vue'
3
+ import { VCombobox } from 'vuetify/components'
4
+ import { getInputProps } from '../../utils/props.js'
5
+ import { getCompSlots } from '../../utils/slots.js'
6
+
7
+ export default defineComponent({
8
+ props: {
9
+ modelValue: {
10
+ /** @type import('vue').PropType<import('../types.js').VjsfComboboxNode> */
11
+ type: Object,
12
+ required: true
13
+ },
14
+ statefulLayout: {
15
+ /** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
16
+ type: Object,
17
+ required: true
18
+ }
19
+ },
20
+ setup (props) {
21
+ /** @type import('vue').Ref<import('@json-layout/vocabulary').SelectItems> */
22
+ const items = shallowRef(props.modelValue.layout.items ?? [])
23
+ /** @type import('vue').Ref<boolean> */
24
+ const loading = ref(false)
25
+
26
+ /** @type import('@json-layout/core').StateTree | null */
27
+ let lastStateTree = null
28
+ /** @type Record<string, any> | null */
29
+ let lastContext = null
30
+
31
+ const hasItems = computed(() => {
32
+ return !!(props.modelValue.layout.items || props.modelValue.layout.getItems)
33
+ })
34
+
35
+ const refresh = async () => {
36
+ if (props.modelValue.layout.items) return
37
+ if (props.statefulLayout.stateTree === lastStateTree && props.statefulLayout.options.context === lastContext) return
38
+ lastStateTree = props.statefulLayout.stateTree
39
+ lastContext = props.statefulLayout.options.context ?? null
40
+ console.log('HAS ITEMS ?', hasItems.value)
41
+ if (hasItems.value) {
42
+ loading.value = true
43
+ items.value = await props.statefulLayout.getItems(props.modelValue)
44
+ loading.value = false
45
+ }
46
+ }
47
+
48
+ if (!props.modelValue.layout.items) {
49
+ refresh()
50
+ }
51
+
52
+ const fieldProps = computed(() => {
53
+ const fieldProps = getInputProps(props.modelValue, props.statefulLayout)
54
+ fieldProps.loading = loading.value
55
+ if (hasItems.value) fieldProps.items = items.value
56
+ if (props.modelValue.options.readOnly) fieldProps.menuProps = { modelValue: false }
57
+ if (props.modelValue.layout.multiple) {
58
+ fieldProps.multiple = true
59
+ fieldProps.chips = true
60
+ fieldProps.closableChips = true
61
+ }
62
+ fieldProps['onUpdate:menu'] = () => refresh()
63
+ return fieldProps
64
+ })
65
+
66
+ const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
67
+
68
+ // @ts-ignore
69
+ return () => h(VCombobox, fieldProps.value, fieldSlots.value)
70
+ }
71
+ })
72
+
73
+ </script>
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
2
  import TextFieldMenu from '../fragments/text-field-menu.vue'
3
- import { VDatePicker } from 'vuetify/labs/VDatePicker'
4
- import { useDate } from 'vuetify/labs/date'
3
+ import { VDatePicker } from 'vuetify/components/VDatePicker'
4
+ import { useDate } from 'vuetify'
5
5
  import { computed } from 'vue'
6
6
  import { getCompProps } from '../../utils/props.js'
7
7
  import { getDateTimeParts } from '../../utils/dates.js'
@@ -0,0 +1,29 @@
1
+ <script>
2
+ import { defineComponent, h, computed } from 'vue'
3
+ import { VTextarea } from 'vuetify/components'
4
+ import { getInputProps } from '../../utils/props.js'
5
+ import { getCompSlots } from '../../utils/slots.js'
6
+
7
+ export default defineComponent({
8
+ props: {
9
+ modelValue: {
10
+ /** @type import('vue').PropType<import('../types.js').VjsfTextareaNode> */
11
+ type: Object,
12
+ required: true
13
+ },
14
+ statefulLayout: {
15
+ /** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
16
+ type: Object,
17
+ required: true
18
+ }
19
+ },
20
+ setup (props) {
21
+ const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout))
22
+ const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
23
+
24
+ // @ts-ignore
25
+ return () => h(VTextarea, fieldProps.value, fieldSlots.value)
26
+ }
27
+ })
28
+
29
+ </script>
@@ -0,0 +1,73 @@
1
+ <script>
2
+ import { defineComponent, h, computed, shallowRef, ref } from 'vue'
3
+ import { VCombobox } from 'vuetify/components'
4
+ import { getInputProps } from '../../utils/props.js'
5
+ import { getCompSlots } from '../../utils/slots.js'
6
+
7
+ export default defineComponent({
8
+ props: {
9
+ modelValue: {
10
+ /** @type import('vue').PropType<import('../types.js').VjsfComboboxNode> */
11
+ type: Object,
12
+ required: true
13
+ },
14
+ statefulLayout: {
15
+ /** @type import('vue').PropType<import('@json-layout/core').StatefulLayout> */
16
+ type: Object,
17
+ required: true
18
+ }
19
+ },
20
+ setup (props) {
21
+ /** @type import('vue').Ref<import('@json-layout/vocabulary').SelectItems> */
22
+ const items = shallowRef(props.modelValue.layout.items ?? [])
23
+ /** @type import('vue').Ref<boolean> */
24
+ const loading = ref(false)
25
+
26
+ /** @type import('@json-layout/core').StateTree | null */
27
+ let lastStateTree = null
28
+ /** @type Record<string, any> | null */
29
+ let lastContext = null
30
+
31
+ const hasItems = computed(() => {
32
+ return !!(props.modelValue.layout.items || props.modelValue.layout.getItems)
33
+ })
34
+
35
+ const refresh = async () => {
36
+ if (props.modelValue.layout.items) return
37
+ if (props.statefulLayout.stateTree === lastStateTree && props.statefulLayout.options.context === lastContext) return
38
+ lastStateTree = props.statefulLayout.stateTree
39
+ lastContext = props.statefulLayout.options.context ?? null
40
+ if (hasItems.value) {
41
+ loading.value = true
42
+ items.value = await props.statefulLayout.getItems(props.modelValue)
43
+ loading.value = false
44
+ }
45
+ }
46
+
47
+ if (!props.modelValue.layout.items) {
48
+ refresh()
49
+ }
50
+
51
+ const fieldProps = computed(() => {
52
+ const fieldProps = getInputProps(props.modelValue, props.statefulLayout, ['step', 'min', 'max'])
53
+ fieldProps.type = 'number'
54
+ fieldProps.loading = loading.value
55
+ if (hasItems.value) fieldProps.items = items.value
56
+ if (props.modelValue.options.readOnly) fieldProps.menuProps = { modelValue: false }
57
+ if (props.modelValue.layout.multiple) {
58
+ fieldProps.multiple = true
59
+ fieldProps.chips = true
60
+ fieldProps.closableChips = true
61
+ }
62
+ fieldProps['onUpdate:menu'] = () => refresh()
63
+ fieldProps['onUpdate:modelValue'] = (/** @type string[] */value) => props.statefulLayout.input(props.modelValue, value && value.map(Number))
64
+ return fieldProps
65
+ })
66
+ const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
67
+
68
+ // @ts-ignore
69
+ return () => h(VCombobox, fieldProps.value, fieldSlots.value)
70
+ }
71
+ })
72
+
73
+ </script>
@@ -19,9 +19,8 @@ export default defineComponent({
19
19
  },
20
20
  setup (props) {
21
21
  const fieldProps = computed(() => {
22
- const fieldProps = getInputProps(props.modelValue, props.statefulLayout)
22
+ const fieldProps = getInputProps(props.modelValue, props.statefulLayout, ['step', 'min', 'max'])
23
23
  fieldProps.type = 'number'
24
- if ('step' in props.modelValue.layout) fieldProps.step = props.modelValue.layout.step
25
24
  fieldProps['onUpdate:modelValue'] = (/** @type string */value) => props.statefulLayout.input(props.modelValue, value && Number(value))
26
25
  return fieldProps
27
26
  })