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

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 (69) hide show
  1. package/package.json +10 -4
  2. package/src/compat/v2.js +44 -6
  3. package/src/compile/index.js +25 -6
  4. package/src/compile/v-jsf-compiled.vue.ejs +14 -52
  5. package/src/components/fragments/help-message.vue +48 -0
  6. package/src/components/fragments/section-header.vue +4 -1
  7. package/src/components/fragments/select-item-icon.vue +28 -0
  8. package/src/components/fragments/select-item.vue +43 -0
  9. package/src/components/fragments/select-selection.vue +35 -0
  10. package/src/components/fragments/text-field-menu.vue +1 -1
  11. package/src/components/node.vue +10 -1
  12. package/src/components/nodes/autocomplete.vue +95 -0
  13. package/src/components/nodes/combobox.vue +73 -0
  14. package/src/components/nodes/date-picker.vue +2 -2
  15. package/src/components/nodes/list.vue +170 -86
  16. package/src/components/nodes/markdown.vue +233 -0
  17. package/src/components/nodes/number-combobox.vue +73 -0
  18. package/src/components/nodes/number-field.vue +1 -2
  19. package/src/components/nodes/select.vue +70 -50
  20. package/src/components/nodes/stepper.vue +98 -0
  21. package/src/components/nodes/text-field.vue +1 -1
  22. package/src/components/nodes/textarea.vue +1 -1
  23. package/src/components/options.js +23 -2
  24. package/src/components/tree.vue +2 -1
  25. package/src/components/types.ts +8 -0
  26. package/src/components/vjsf.vue +38 -94
  27. package/src/composables/use-dnd.js +54 -0
  28. package/src/composables/use-vjsf.js +115 -0
  29. package/src/index.js +2 -1
  30. package/src/styles/vjsf.css +10 -0
  31. package/src/utils/arrays.js +15 -0
  32. package/src/utils/props.js +25 -6
  33. package/types/compat/v2.d.ts.map +1 -1
  34. package/types/compile/index.d.ts.map +1 -1
  35. package/types/components/fragments/help-message.vue.d.ts +8 -0
  36. package/types/components/fragments/help-message.vue.d.ts.map +1 -0
  37. package/types/components/fragments/select-item-icon.vue.d.ts +15 -0
  38. package/types/components/fragments/select-item-icon.vue.d.ts.map +1 -0
  39. package/types/components/fragments/select-item.vue.d.ts +12 -0
  40. package/types/components/fragments/select-item.vue.d.ts.map +1 -0
  41. package/types/components/fragments/select-selection.vue.d.ts +12 -0
  42. package/types/components/fragments/select-selection.vue.d.ts.map +1 -0
  43. package/types/components/nodes/autocomplete.vue.d.ts +27 -0
  44. package/types/components/nodes/autocomplete.vue.d.ts.map +1 -0
  45. package/types/components/nodes/combobox.vue.d.ts +27 -0
  46. package/types/components/nodes/combobox.vue.d.ts.map +1 -0
  47. package/types/components/nodes/markdown.vue.d.ts +27 -0
  48. package/types/components/nodes/markdown.vue.d.ts.map +1 -0
  49. package/types/components/nodes/number-combobox.vue.d.ts +27 -0
  50. package/types/components/nodes/number-combobox.vue.d.ts.map +1 -0
  51. package/types/components/nodes/select.vue.d.ts +25 -8
  52. package/types/components/nodes/select.vue.d.ts.map +1 -1
  53. package/types/components/nodes/stepper.vue.d.ts +10 -0
  54. package/types/components/nodes/stepper.vue.d.ts.map +1 -0
  55. package/types/components/options.d.ts +3 -2
  56. package/types/components/options.d.ts.map +1 -1
  57. package/types/components/types.d.ts +9 -1
  58. package/types/components/types.d.ts.map +1 -1
  59. package/types/components/vjsf.vue.d.ts +5 -3
  60. package/types/composables/use-dnd.d.ts +21 -0
  61. package/types/composables/use-dnd.d.ts.map +1 -0
  62. package/types/composables/use-vjsf.d.ts +17 -0
  63. package/types/composables/use-vjsf.d.ts.map +1 -0
  64. package/types/index.d.ts +2 -1
  65. package/types/index.d.ts.map +1 -1
  66. package/types/utils/arrays.d.ts +9 -0
  67. package/types/utils/arrays.d.ts.map +1 -0
  68. package/types/utils/props.d.ts +6 -3
  69. 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.2",
4
4
  "description": "Generate forms for the vuetify UI library (vuejs) based on annotated JSON schemas.",
5
5
  "scripts": {
6
6
  "test": "vitest",
@@ -28,6 +28,12 @@
28
28
  "types": "./types/components/*.d.ts"
29
29
  }
30
30
  },
31
+ "./composables/*": {
32
+ "import": {
33
+ "default": "./src/composables/*",
34
+ "types": "./types/composables/*.d.ts"
35
+ }
36
+ },
31
37
  "./compile": {
32
38
  "import": {
33
39
  "default": "./src/compile/index.js",
@@ -43,13 +49,13 @@
43
49
  },
44
50
  "peerDependencies": {
45
51
  "vue": "^3.3.4",
46
- "vuetify": "^3.3.20"
52
+ "vuetify": "^3.4.2"
47
53
  },
48
54
  "dependencies": {
49
- "@json-layout/core": "0.1.0",
50
- "@json-layout/vocabulary": "0.1.0",
55
+ "@json-layout/core": "0.3.0",
51
56
  "@vueuse/core": "^10.5.0",
52
57
  "debug": "^4.3.4",
58
+ "easymde": "^2.18.0",
53
59
  "ejs": "^3.1.9",
54
60
  "rfdc": "^1.3.0"
55
61
  },
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,27 +4,46 @@ 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
16
38
  * @returns {string}
17
39
  */
18
- export function compile (schema, baseImport = '@koumoul/vjsf/components') {
40
+ export function compile (schema, baseImport = '@koumoul/vjsf') {
19
41
  const compiledLayout = compileLayout(schema, { code: true })
20
42
  const compiledLayoutCode = serializeCompiledLayout(compiledLayout)
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 })
@@ -5,10 +5,11 @@ import { StatefulLayout } from '@json-layout/core'
5
5
  import { ref, shallowRef, getCurrentInstance, useSlots } from 'vue'
6
6
  import { useElementSize } from '@vueuse/core'
7
7
 
8
- import { defaultOptions } from '<%- baseImport %>/options.js'
9
- import Tree from '<%- baseImport %>/tree.vue'
8
+ import { defaultOptions } from '<%- baseImport %>/components/options.js'
9
+ import Tree from '<%- baseImport %>/components/tree.vue'
10
+ import { useVjsf, emits } from '<%- baseImport %>/composables/use-vjsf.js'
10
11
  <% comps.forEach(function(comp){ %>
11
- import <%= comp.replace(/-/g, '') %>Node from '<%- baseImport %>/nodes/<%= comp %>.vue'
12
+ import <%= comp.replace(/-/g, '') %>Node from '<%- baseImport %>/components/nodes/<%= comp %>.vue'
12
13
  <% }); %>
13
14
 
14
15
  <%- compiledLayoutCode %>
@@ -21,55 +22,16 @@ if (!vueInstance?.appContext.app.component('vjsf-node-<%= comp %>')) {
21
22
  <% }); %>
22
23
 
23
24
  const props = defineProps(['modelValue', 'options'])
24
- const emit = defineEmits(['update:modelValue', 'update:state'])
25
-
26
- const statefulLayout = shallowRef(null)
27
- const stateTree = shallowRef(null)
28
-
29
- const el = ref(null)
30
- const { width } = useElementSize(el)
31
-
32
- const slots = useSlots()
33
-
34
- const fullOptions = computed(() => {
35
- if (!width.value) return null
36
- return {
37
- ...defaultOptions,
38
- ...props.options,
39
- context: props.options.context ? JSON.parse(JSON.stringify(props.options.context)) : {},
40
- width: Math.round(width.value),
41
- vjsfSlots: { ...slots }
42
- }
43
- })
44
-
45
- const initStatefulLayout = () => {
46
- if (!fullOptions.value) return
47
- const _statefulLayout = new StatefulLayout(compiledLayout, compiledLayout.skeletonTree, fullOptions.value, props.modelValue)
48
- statefulLayout.value = _statefulLayout
49
- stateTree.value = _statefulLayout.stateTree
50
- _statefulLayout.events.on('update', () => {
51
- stateTree.value = _statefulLayout.stateTree
52
- emit('update:modelValue', _statefulLayout.data)
53
- emit('update:state', _statefulLayout)
54
- })
55
- emit('update:state', _statefulLayout)
56
- }
57
-
58
- watch(fullOptions, (newOptions) => {
59
- if (!newOptions) {
60
- statefulLayout.value = null
61
- } else if (statefulLayout.value) {
62
- statefulLayout.value.options = newOptions
63
- } else {
64
- initStatefulLayout()
65
- }
66
- })
67
-
68
- // case where data is updated from outside
69
- watch(() => props.modelValue, (newData) => {
70
- if (statefulLayout.value && statefulLayout.value.data !== newData) statefulLayout.value.data = newData
71
- })
72
-
25
+ const emit = defineEmits(Object.keys(emits))
26
+
27
+ const { el, statefulLayout, stateTree } = useVjsf(
28
+ null,
29
+ computed(() => props.modelValue),
30
+ computed(() => props.options),
31
+ emit,
32
+ null,
33
+ compiledLayout
34
+ )
73
35
  </script>
74
36
 
75
37
  <template>
@@ -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
+ 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)
53
+ fieldProps.loading = loading.value
54
+ fieldProps.returnObject = false
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'