@koumoul/vjsf 3.0.0-beta.3 → 3.0.0-beta.31

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 (103) hide show
  1. package/README.md +21 -0
  2. package/package.json +5 -4
  3. package/src/compat/v2.js +132 -27
  4. package/src/compile/index.js +18 -4
  5. package/src/compile/options.js +3 -7
  6. package/src/compile/v-jsf-compiled.vue.ejs +1 -1
  7. package/src/components/fragments/help-message.vue +10 -1
  8. package/src/components/fragments/section-header.vue +1 -3
  9. package/src/components/fragments/select-item-icon.vue +1 -1
  10. package/src/components/fragments/select-selection.vue +2 -1
  11. package/src/components/fragments/selection-group.vue +100 -0
  12. package/src/components/fragments/text-field-menu.vue +5 -1
  13. package/src/components/node.vue +47 -42
  14. package/src/components/nodes/autocomplete.vue +11 -35
  15. package/src/components/nodes/checkbox-group.vue +39 -0
  16. package/src/components/nodes/checkbox.vue +9 -1
  17. package/src/components/nodes/color-picker.vue +5 -1
  18. package/src/components/nodes/combobox.vue +3 -0
  19. package/src/components/nodes/date-picker.vue +4 -2
  20. package/src/components/nodes/date-time-picker.vue +3 -0
  21. package/src/components/nodes/expansion-panels.vue +24 -12
  22. package/src/components/nodes/file-input.vue +3 -0
  23. package/src/components/nodes/list.vue +145 -91
  24. package/src/components/nodes/number-combobox.vue +3 -0
  25. package/src/components/nodes/number-field.vue +3 -0
  26. package/src/components/nodes/one-of-select.vue +45 -24
  27. package/src/components/nodes/radio-group.vue +53 -0
  28. package/src/components/nodes/section.vue +3 -0
  29. package/src/components/nodes/select.vue +10 -27
  30. package/src/components/nodes/slider.vue +3 -0
  31. package/src/components/nodes/stepper.vue +3 -0
  32. package/src/components/nodes/switch-group.vue +39 -0
  33. package/src/components/nodes/switch.vue +9 -3
  34. package/src/components/nodes/tabs.vue +10 -3
  35. package/src/components/nodes/text-field.vue +3 -0
  36. package/src/components/nodes/textarea.vue +3 -0
  37. package/src/components/nodes/vertical-tabs.vue +6 -1
  38. package/src/components/options.js +21 -5
  39. package/src/components/vjsf.vue +6 -0
  40. package/src/composables/use-comp-defaults.js +19 -0
  41. package/src/composables/use-dnd.js +1 -0
  42. package/src/composables/use-get-items.js +48 -0
  43. package/src/composables/use-vjsf.js +76 -40
  44. package/src/index.js +3 -0
  45. package/src/types.ts +21 -6
  46. package/src/utils/build.js +1 -1
  47. package/src/utils/index.js +0 -1
  48. package/src/utils/props.js +9 -30
  49. package/types/compat/v2.d.ts.map +1 -1
  50. package/types/compile/index.d.ts +2 -2
  51. package/types/compile/index.d.ts.map +1 -1
  52. package/types/compile/options.d.ts.map +1 -1
  53. package/types/components/fragments/selection-group.vue.d.ts +35 -0
  54. package/types/components/fragments/selection-group.vue.d.ts.map +1 -0
  55. package/types/components/fragments/text-field-menu.vue.d.ts.map +1 -1
  56. package/types/components/nodes/autocomplete.vue.d.ts.map +1 -1
  57. package/types/components/nodes/checkbox-group copy.vue.d.ts +27 -0
  58. package/types/components/nodes/checkbox-group copy.vue.d.ts.map +1 -0
  59. package/types/components/nodes/checkbox-group.vue.d.ts +27 -0
  60. package/types/components/nodes/checkbox-group.vue.d.ts.map +1 -0
  61. package/types/components/nodes/combobox.vue.d.ts.map +1 -1
  62. package/types/components/nodes/file-input.vue.d.ts.map +1 -1
  63. package/types/components/nodes/number-combobox.vue.d.ts.map +1 -1
  64. package/types/components/nodes/number-field.vue.d.ts.map +1 -1
  65. package/types/components/nodes/radio-group.vue.d.ts +27 -0
  66. package/types/components/nodes/radio-group.vue.d.ts.map +1 -0
  67. package/types/components/nodes/radio.vue.d.ts +10 -0
  68. package/types/components/nodes/radio.vue.d.ts.map +1 -0
  69. package/types/components/nodes/select.vue.d.ts.map +1 -1
  70. package/types/components/nodes/switch-group.vue.d.ts +27 -0
  71. package/types/components/nodes/switch-group.vue.d.ts.map +1 -0
  72. package/types/components/nodes/text-field.vue.d.ts.map +1 -1
  73. package/types/components/nodes/textarea.vue.d.ts.map +1 -1
  74. package/types/components/options.d.ts +1 -1
  75. package/types/components/options.d.ts.map +1 -1
  76. package/types/components/vjsf.vue.d.ts +2 -2
  77. package/types/composables/use-comp-defaults.d.ts +8 -0
  78. package/types/composables/use-comp-defaults.d.ts.map +1 -0
  79. package/types/composables/use-dnd.d.ts.map +1 -1
  80. package/types/composables/use-get-items.d.ts +13 -0
  81. package/types/composables/use-get-items.d.ts.map +1 -0
  82. package/types/composables/use-vjsf.d.ts.map +1 -1
  83. package/types/index.d.ts +2 -0
  84. package/types/index.d.ts.map +1 -1
  85. package/types/types.d.ts +20 -8
  86. package/types/types.d.ts.map +1 -1
  87. package/types/utils/build.d.ts +1 -1
  88. package/types/utils/index.d.ts +0 -1
  89. package/types/utils/props.d.ts +3 -4
  90. package/types/utils/props.d.ts.map +1 -1
  91. package/src/utils/global-register.js +0 -13
  92. package/types/components/global-register.d.ts +0 -8
  93. package/types/components/global-register.d.ts.map +0 -1
  94. package/types/components/nodes/markdown.vue.d.ts +0 -27
  95. package/types/components/nodes/markdown.vue.d.ts.map +0 -1
  96. package/types/components/nodes/text-field copy.vue.d.ts +0 -10
  97. package/types/components/nodes/text-field copy.vue.d.ts.map +0 -1
  98. package/types/components/types.d.ts +0 -91
  99. package/types/components/types.d.ts.map +0 -1
  100. package/types/components/v-jsf.vue.d.ts +0 -13
  101. package/types/components/v-jsf.vue.d.ts.map +0 -1
  102. package/types/utils/clone.d.ts +0 -3
  103. package/types/utils/clone.d.ts.map +0 -1
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # VJSF
2
+
3
+ *vuetify-json-schema-form* - *@koumoul/vjsf on npm*
4
+
5
+ Easily create beautiful forms that output valid data.
6
+
7
+ Based on [Vue.js](https://vuejs.org/) / [Vuetify](https://vuetifyjs.com/) / [JSON Schema](https://json-schema.org/) / [JSON Layout](https://github.com/json-layout/json-layout).
8
+
9
+ See [the documentation](https://koumoul-dev.github.io/vuetify-jsonschema-form/latest/).
10
+
11
+ See [the documentation for deprecated v2](https://koumoul-dev.github.io/vuetify-jsonschema-form/2.x/).
12
+
13
+ ![](doc/static/vjsf.gif)
14
+
15
+ ## Bug reports
16
+
17
+ Bug reports are created using github issues. The examples in the documentation include codepen links, as much as possible please save a duplicate codepen with the minimal schema/config to reproduce your problem.
18
+
19
+ ## Contribute
20
+
21
+ See [CONTRIBUTE.md](./CONTRIBUTE.md).
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@koumoul/vjsf",
3
- "version": "3.0.0-beta.3",
3
+ "version": "3.0.0-beta.31",
4
4
  "description": "Generate forms for the vuetify UI library (vuejs) based on annotated JSON schemas.",
5
5
  "scripts": {
6
6
  "test": "vitest",
7
7
  "build": "vue-tsc",
8
- "watch:build": "vue-tsc --watch"
8
+ "watch:build": "vue-tsc --watch",
9
+ "prepublishOnly": "cp ../README.md README.md && cp ../LICENSE LICENSE"
9
10
  },
10
11
  "author": "Alban Mouton <alban.mouton@gmail.com>",
11
12
  "license": "MIT",
@@ -72,10 +73,10 @@
72
73
  },
73
74
  "peerDependencies": {
74
75
  "vue": "^3.4.3",
75
- "vuetify": "^3.4.9"
76
+ "vuetify": "^3.6.8"
76
77
  },
77
78
  "dependencies": {
78
- "@json-layout/core": "0.9.3",
79
+ "@json-layout/core": "0.26.0",
79
80
  "@vueuse/core": "^10.5.0",
80
81
  "debug": "^4.3.4",
81
82
  "ejs": "^3.1.9"
package/src/compat/v2.js CHANGED
@@ -1,23 +1,99 @@
1
1
  import ajvModule from 'ajv'
2
2
  import addFormats from 'ajv-formats'
3
- import { resolveRefs, clone } from '@json-layout/core'
3
+ import { resolveLocaleRefs, clone } from '@json-layout/core'
4
4
  import { isPartialGetItemsObj } from '@json-layout/vocabulary'
5
5
 
6
6
  // @ts-ignore
7
7
  const Ajv = /** @type {typeof ajvModule.default} */ (ajvModule)
8
8
 
9
- const processFragment = (/** @type {import("ajv").SchemaObject} */schema) => {
9
+ const expressionKeyMappings = {
10
+ modelRoot: 'rootData',
11
+ root: 'rootData',
12
+ model: 'data',
13
+ value: 'data'
14
+ }
15
+
16
+ /**
17
+ * @param {string} expression
18
+ * @returns {string}
19
+ */
20
+ const prefixExpression = (expression) => {
21
+ // looks like a simple expression missing the data. prefix
22
+ if (expression.match(/^[a-z.]*$/i) && !['data', 'context', 'rootData', 'parent'].some(key => expression.startsWith(key + '.'))) {
23
+ return 'rootData.' + expression
24
+ }
25
+ return expression
26
+ }
27
+
28
+ /**
29
+ *
30
+ * @param {string} expression
31
+ * @param {'js-eval' | 'js-tpl'} [type]
32
+ * @returns {{type: 'js-eval' | 'js-tpl', expr: string, pure: boolean}}
33
+ */
34
+ const fixExpression = (expression, type = 'js-eval') => {
35
+ let expr = expression
36
+ let pure = true
37
+
38
+ if (expr.includes('parent.value')) {
39
+ pure = false
40
+ expr = expr.replace(/parent\.value/g, 'parent.data')
41
+ }
42
+
43
+ for (const [key, value] of Object.entries(expressionKeyMappings)) {
44
+ expr = expr.replace(new RegExp(`${key}\\.`, 'g'), value + '.')
45
+ }
46
+ if (type === 'js-eval') {
47
+ expr = prefixExpression(expr)
48
+ }
49
+ if (type === 'js-tpl') {
50
+ for (const expressionMatch of expr.matchAll(/\{(.*?)\}/g)) {
51
+ if (expressionMatch[1] !== 'q') expr = expr.replace(expressionMatch[0], '${' + prefixExpression(expressionMatch[1]) + '}')
52
+ }
53
+ }
54
+
55
+ if (expr.includes('rootData')) {
56
+ pure = false
57
+ }
58
+
59
+ return { type, expr, pure }
60
+ }
61
+
62
+ /**
63
+ *
64
+ * @param {import("ajv").SchemaObject} schema
65
+ * @param {(schemaId: string, ref: string) => [any, string, string]} getJSONRef
66
+ * @param {string} schemaId
67
+ * @param {any[]} processed
68
+ */
69
+ const processFragment = (schema, getJSONRef, schemaId, processed) => {
70
+ if (processed.includes(schema)) return
71
+ processed.push(schema)
72
+ if (schema.$ref) {
73
+ const [refFragment, refSchemaId] = getJSONRef(schemaId, schema.$ref)
74
+ processFragment(refFragment, getJSONRef, refSchemaId, processed)
75
+ }
10
76
  if (!schema.layout) {
11
77
  /** @type import('@json-layout/vocabulary').PartialCompObject */
12
78
  const layout = {}
13
79
 
80
+ if (schema.separator || schema['x-separator']) {
81
+ layout.separator = schema.separator || schema['x-separator']
82
+ delete schema.separator
83
+ delete schema['x-separator']
84
+ }
85
+
14
86
  if (schema['x-display'] === 'icon' && (schema.enum || schema.items?.enum)) {
15
87
  layout.getItems = { itemIcon: schema['x-itemIcon'] || 'data.value' }
16
88
  delete schema['x-display']
17
89
  }
18
90
 
19
91
  if (schema['x-display']) {
20
- layout.comp = schema['x-display']
92
+ let display = schema['x-display']
93
+ if (display === 'radio') display = 'radio-group'
94
+ if (display === 'checkbox' && schema.type !== 'boolean') display = 'checkbox-group'
95
+ if (display === 'switch' && schema.type !== 'boolean') display = 'switch-group'
96
+ layout.comp = display
21
97
  delete schema['x-display']
22
98
  }
23
99
 
@@ -32,18 +108,20 @@ const processFragment = (/** @type {import("ajv").SchemaObject} */schema) => {
32
108
  }
33
109
 
34
110
  if (schema['x-fromData']) {
35
- layout.comp = 'select'
36
- layout.getItems = { expr: schema['x-fromData'] }
111
+ layout.comp = layout.comp ?? 'select'
112
+ layout.getItems = fixExpression(schema['x-fromData'])
37
113
  delete schema['x-fromData']
38
114
  }
39
115
 
116
+ if (schema['x-if']) {
117
+ layout.if = fixExpression(schema['x-if'])
118
+ delete schema['x-if']
119
+ }
120
+
40
121
  if (schema['x-fromUrl']) {
41
122
  /** @type string */
42
- let url = schema['x-fromUrl']
43
- for (const expressionMatch of url.matchAll(/\{(.*?)\}/g)) {
44
- if (expressionMatch[1] !== 'q') url = url.replace(expressionMatch[0], '$' + expressionMatch[0])
45
- }
46
- layout.getItems = { url }
123
+ const url = schema['x-fromUrl']
124
+ layout.getItems = { url: fixExpression(url, 'js-tpl') }
47
125
  delete schema['x-fromUrl']
48
126
  }
49
127
  if (layout.getItems && isPartialGetItemsObj(layout.getItems)) {
@@ -58,6 +136,10 @@ const processFragment = (/** @type {import("ajv").SchemaObject} */schema) => {
58
136
  delete schema['x-itemsProp']
59
137
  }
60
138
 
139
+ if (schema['x-cols']) {
140
+ layout.cols = schema['x-cols']
141
+ }
142
+
61
143
  // compact the layout keyword if possible
62
144
  if (Object.keys(layout).length === 1 && 'comp' in layout) {
63
145
  schema.layout = layout.comp
@@ -66,30 +148,51 @@ const processFragment = (/** @type {import("ajv").SchemaObject} */schema) => {
66
148
  }
67
149
  }
68
150
 
69
- if (schema.type === 'object') {
70
- if (schema.properties) {
71
- for (const propertyKey of Object.keys(schema.properties)) {
72
- processFragment(schema.properties[propertyKey])
73
- }
74
- }
75
- if (schema.allOf) {
76
- for (const item of schema.allOf) processFragment(item)
151
+ if (schema.properties) {
152
+ for (const propertyKey of Object.keys(schema.properties)) {
153
+ processFragment(schema.properties[propertyKey], getJSONRef, schemaId, processed)
77
154
  }
78
- if (schema.oneOf) {
79
- for (const item of schema.oneOf) processFragment(item)
80
- }
81
- if (schema.anyOf) {
82
- for (const item of schema.anyOf) processFragment(item)
155
+ }
156
+
157
+ if (schema.allOf) {
158
+ for (const item of schema.allOf) processFragment(item, getJSONRef, schemaId, processed)
159
+ }
160
+
161
+ if (schema.oneOf) {
162
+ if (!schema.oneOfLayout) {
163
+ const constPropertyKey = Object.keys(schema.oneOf[0]?.properties || {})
164
+ .find(key => !!schema.oneOf[0]?.properties[key].const)
165
+ if (constPropertyKey) {
166
+ const constProperty = schema.oneOf[0]?.properties[constPropertyKey]
167
+ if (constProperty?.title) schema.oneOfLayout = { label: constProperty.title }
168
+ if (schema.required && Array.isArray(schema.required)) {
169
+ schema.required = schema.required.filter(key => key !== constPropertyKey)
170
+ }
171
+ }
83
172
  }
173
+ for (const item of schema.oneOf) processFragment(item, getJSONRef, schemaId, processed)
174
+ }
175
+
176
+ if (schema.anyOf) {
177
+ for (const item of schema.anyOf) processFragment(item, getJSONRef, schemaId, processed)
84
178
  }
85
179
 
86
180
  if (schema.type === 'array' && schema.items) {
87
181
  if (Array.isArray(schema.items)) {
88
- for (const item of schema.items) processFragment(item)
182
+ for (const item of schema.items) processFragment(item, getJSONRef, schemaId, processed)
89
183
  } else {
90
- processFragment(schema.items)
184
+ processFragment(schema.items, getJSONRef, schemaId, processed)
91
185
  }
92
186
  }
187
+ if (schema.dependencies) {
188
+ for (const key of Object.keys(schema.dependencies)) {
189
+ processFragment(schema.dependencies[key], getJSONRef, schemaId, processed)
190
+ }
191
+ }
192
+ if (schema.if) {
193
+ if (schema.then) processFragment(schema.then, getJSONRef, schemaId, processed)
194
+ if (schema.else) processFragment(schema.else, getJSONRef, schemaId, processed)
195
+ }
93
196
  }
94
197
 
95
198
  /**
@@ -111,7 +214,9 @@ export function v2compat (_schema, _ajv, lang = 'en') {
111
214
 
112
215
  const schema = /** @type {import("ajv").SchemaObject} */ (clone(_schema))
113
216
  schema.$id = schema.$id ?? '_jl'
114
- resolveRefs(schema, ajv, lang)
115
- processFragment(schema)
217
+ const getJSONRef = resolveLocaleRefs(schema, ajv, lang)
218
+ /** @type {any[]} */
219
+ const processed = []
220
+ processFragment(schema, getJSONRef, schema.$id, processed)
116
221
  return schema
117
222
  }
@@ -37,10 +37,17 @@ function listComps (comps, layout) {
37
37
  * @param {object} schema
38
38
  * @param {import('../types.js').PartialVjsfCompileOptions} [options]
39
39
  * @param {string} [baseImport]
40
- * @returns {string}
40
+ * @returns {Promise<string>}
41
41
  */
42
- export function compile (schema, options = {}, baseImport = '@koumoul/vjsf') {
42
+ export async function compile (schema, options = {}, baseImport = '@koumoul/vjsf') {
43
43
  const fullOptions = getFullOptions(options)
44
+ /** @type {Record<string, string>} */
45
+ const pluginsImportsByName = {}
46
+ for (const pluginImport of fullOptions.pluginsImports) {
47
+ const componentInfo = /** @type {import('@json-layout/vocabulary').ComponentInfo} */((await import(pluginImport + '/info.js')).default)
48
+ fullOptions.components[componentInfo.name] = componentInfo
49
+ pluginsImportsByName[componentInfo.name] = pluginImport
50
+ }
44
51
  const compiledLayout = compileLayout(schema, { ...fullOptions, code: true })
45
52
  const compiledLayoutCode = serializeCompiledLayout(compiledLayout)
46
53
  /** @type Set<string> */
@@ -50,9 +57,16 @@ export function compile (schema, options = {}, baseImport = '@koumoul/vjsf') {
50
57
  }
51
58
  comps.delete('none')
52
59
 
60
+ /** @type {Record<string, any>} */
61
+ const pluginsComponents = {}
62
+
53
63
  const compImports = [...comps].map(comp => {
54
64
  const compName = comp.replace(/-/g, '') + 'Node'
55
- const compImport = fullOptions.nodeComponentImports[comp] ?? `${baseImport}/components/nodes/${comp}.vue`
65
+ let compImport = `${baseImport}/components/nodes/${comp}.vue`
66
+ if (pluginsImportsByName[comp]) {
67
+ compImport = `${pluginsImportsByName[comp]}/node.vue`
68
+ pluginsComponents[comp] = fullOptions.components[comp]
69
+ }
56
70
  return {
57
71
  comp,
58
72
  compName,
@@ -60,6 +74,6 @@ export function compile (schema, options = {}, baseImport = '@koumoul/vjsf') {
60
74
  }
61
75
  })
62
76
 
63
- const code = ejs.render(template, { compiledLayoutCode, compImports, baseImport })
77
+ const code = ejs.render(template, { compiledLayoutCode, compImports, baseImport, pluginsComponents })
64
78
  return code
65
79
  }
@@ -1,8 +1,7 @@
1
1
  /** @type import("../types.js").PartialVjsfCompileOptions */
2
2
  export const defaultOptions = {
3
- nodeComponentImports: {
4
- markdown: '@koumoul/vjsf-markdown/components/nodes/markdown.vue'
5
- }
3
+ pluginsImports: ['@koumoul/vjsf-markdown'],
4
+ components: {}
6
5
  }
7
6
 
8
7
  /**
@@ -11,9 +10,6 @@ export const defaultOptions = {
11
10
  * @returns
12
11
  */
13
12
  export const getFullOptions = (options) => {
14
- const fullOptions = {
15
- ...defaultOptions,
16
- nodeComponentImports: { ...defaultOptions.nodeComponentImports, ...options.nodeComponentImports }
17
- }
13
+ const fullOptions = { ...defaultOptions }
18
14
  return /** @type import('../types.js').VjsfCompileOptions */ (fullOptions)
19
15
  }
@@ -39,7 +39,7 @@ const emit = defineEmits(emits)
39
39
  const { el, statefulLayout, stateTree } = useVjsf(
40
40
  null,
41
41
  computed(() => props.modelValue),
42
- computed(() => props.options),
42
+ computed(() => ({...props.options, components: <%- JSON.stringify(pluginsComponents) %>})),
43
43
  nodeComponents,
44
44
  emit,
45
45
  null,
@@ -10,9 +10,10 @@
10
10
  </v-slide-x-reverse-transition>
11
11
  <v-btn
12
12
  color="info"
13
- class="vjsf-help-message-toggle"
13
+ :class="`vjsf-help-message-toggle vjsf-help-message-toggle-${node.options.density}`"
14
14
  :icon="show ? 'mdi-close-circle' : 'mdi-information'"
15
15
  density="compact"
16
+ :size="node.options.density !== 'default' ? 'small' : 'default'"
16
17
  :title="show ? '' : node.messages.showHelp"
17
18
  @click="show = !show"
18
19
  />
@@ -45,5 +46,13 @@ const show = ref(false)
45
46
  right: -4px;
46
47
  z-index: 1;
47
48
  }
49
+ .vjsf-help-message-toggle-comfortable {
50
+ top: -4px;
51
+ right: -4px;
52
+ }
53
+ .vjsf-help-message-toggle-compact {
54
+ top: -4px;
55
+ right: -4px;
56
+ }
48
57
  </style>
49
58
  ../../../types.js
@@ -45,12 +45,10 @@ const titleClass = computed(() => {
45
45
  </p>
46
46
  <v-alert
47
47
  v-if="node.error && node.validated"
48
- v-bind="node.options.errorAlertProps"
48
+ type="error"
49
49
  :class="`mt-${titleDepthBase - node.options.titleDepth}`"
50
- :density="node.options.density"
51
50
  >
52
51
  {{ node.error }}
53
52
  </v-alert>
54
53
  </div>
55
54
  </template>
56
- ../../../types.js
@@ -20,7 +20,7 @@ export default defineComponent({
20
20
  } else if (isSVG.value) {
21
21
  return h('div', { innerHTML: props.icon.replace('<svg ', '<svg class="v-icon__svg" '), class: 'v-icon' })
22
22
  } else {
23
- return h(VIcon, null, props.icon)
23
+ return h(VIcon, null, () => props.icon)
24
24
  }
25
25
  }
26
26
  }
@@ -18,6 +18,7 @@ defineProps({
18
18
  required: true
19
19
  }
20
20
  })
21
+
21
22
  </script>
22
23
 
23
24
  <template>
@@ -26,7 +27,7 @@ defineProps({
26
27
  v-if="item.icon"
27
28
  :icon="item.icon"
28
29
  />
29
- {{ item.title }}
30
+ {{ item.title ?? item.key ?? item.value }}
30
31
  <span
31
32
  v-if="multiple && !last"
32
33
  class="v-select__selection-comma"
@@ -0,0 +1,100 @@
1
+ <script>
2
+ import { VInput, VLabel, VCheckbox, VSwitch, VSkeletonLoader } from 'vuetify/components'
3
+ import { defineComponent, h, computed } from 'vue'
4
+ import { getInputProps, getCompSlots } from '../../utils/index.js'
5
+ import useGetItems from '../../composables/use-get-items.js'
6
+
7
+ export default defineComponent({
8
+ props: {
9
+ modelValue: {
10
+ /** @type import('vue').PropType<import('../../types.js').VjsfCheckboxGroupNode> */
11
+ type: Object,
12
+ required: true
13
+ },
14
+ statefulLayout: {
15
+ /** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
16
+ type: Object,
17
+ required: true
18
+ },
19
+ type: {
20
+ type: String,
21
+ required: true
22
+ }
23
+ },
24
+ setup (props) {
25
+ const getItems = useGetItems(props)
26
+
27
+ const fieldProps = computed(() => {
28
+ const fieldProps = getInputProps(props.modelValue, props.statefulLayout)
29
+ fieldProps.class.push('v-radio-group') // reuse some styles from radio-group
30
+ fieldProps.class.push('vjsf-selection-group')
31
+ return fieldProps
32
+ })
33
+
34
+ const fieldSlots = computed(() => {
35
+ const slots = getCompSlots(props.modelValue, props.statefulLayout)
36
+
37
+ if (!slots.default) {
38
+ slots.default = () => {
39
+ /** @type {import('vue').VNode[]} */
40
+ const children = [h(VLabel, { text: fieldProps.value.label })]
41
+ if (getItems.loading.value) {
42
+ children.push(h(VSkeletonLoader, { type: 'chip' }))
43
+ } else {
44
+ /** @type {import('vue').VNode[]} */
45
+ const checkboxes = []
46
+ for (const item of getItems.items.value) {
47
+ let modelValue = false
48
+ if (props.modelValue.layout.multiple) {
49
+ modelValue = props.modelValue.data?.includes(item.value)
50
+ } else {
51
+ modelValue = props.modelValue.data === item.value
52
+ }
53
+ checkboxes.push(h(props.type === 'switch' ? VSwitch : VCheckbox, {
54
+ label: item.title,
55
+ hideDetails: true,
56
+ key: item.key,
57
+ modelValue,
58
+ onClick: () => {
59
+ let newValue
60
+ if (props.modelValue.layout.multiple) {
61
+ newValue = props.modelValue.data ? [...props.modelValue.data] : []
62
+ if (newValue.includes(item.value)) {
63
+ newValue = newValue.filter((/** @type {any} */v) => v !== item.value)
64
+ } else {
65
+ newValue.push(item.value)
66
+ }
67
+ } else {
68
+ if (props.modelValue.data === item.value) {
69
+ newValue = undefined
70
+ } else {
71
+ newValue = item.value
72
+ }
73
+ }
74
+ props.statefulLayout.input(props.modelValue, newValue)
75
+ }
76
+ }))
77
+ }
78
+ children.push(h('div', { class: 'v-selection-control-group' }, checkboxes))
79
+ }
80
+ return children
81
+ }
82
+ }
83
+
84
+ return slots
85
+ })
86
+
87
+ // @ts-ignore
88
+ return () => {
89
+ return h(VInput, fieldProps.value, fieldSlots.value)
90
+ }
91
+ }
92
+ })
93
+
94
+ </script>
95
+
96
+ <style>
97
+ .vjsf-selection-group .v-selection-control-group>.v-input .v-selection-control {
98
+ min-height: auto;
99
+ }
100
+ </style>
@@ -24,11 +24,15 @@ const props = defineProps({
24
24
  const fieldProps = computed(() => {
25
25
  const fieldProps = getInputProps(props.modelValue, props.statefulLayout, [], false)
26
26
  fieldProps.readonly = true
27
+ fieldProps.clearable = fieldProps.clearable ?? !props.modelValue.skeleton.required
28
+ fieldProps['onClick:clear'] = () => {
29
+ props.statefulLayout.input(props.modelValue, null)
30
+ }
27
31
  return fieldProps
28
32
  })
29
33
 
30
34
  const menuProps = computed(() => {
31
- const menuProps = getCompProps(props.modelValue, 'menu', false)
35
+ const menuProps = getCompProps(props.modelValue)
32
36
  menuProps.closeOnContentClick = false
33
37
  menuProps.disabled = true
34
38
  return menuProps
@@ -1,10 +1,12 @@
1
1
  <script setup>
2
2
  import { computed } from 'vue'
3
- import { useTheme } from 'vuetify'
4
- import { VCol } from 'vuetify/components'
3
+ import { useTheme, useDefaults } from 'vuetify'
4
+ import { VCol, VDefaultsProvider } from 'vuetify/components'
5
5
  import NodeSlot from './fragments/node-slot.vue'
6
6
  import HelpMessage from './fragments/help-message.vue'
7
7
 
8
+ useDefaults({}, 'VjsfNode')
9
+
8
10
  const props = defineProps({
9
11
  modelValue: {
10
12
  /** @type import('vue').PropType<import('../types.js').VjsfNode> */
@@ -35,52 +37,55 @@ const nodeClasses = computed(() => {
35
37
  return classes
36
38
  })
37
39
 
38
- if (!props.statefulLayout.options.nodeComponents[props.modelValue.layout.comp]) {
40
+ if (props.modelValue.layout.comp !== 'none' && !props.statefulLayout.options.nodeComponents[props.modelValue.layout.comp]) {
39
41
  console.error(`vjsf: missing component to render vjsf node "${props.modelValue.layout.comp}", maybe you forgot to register a component from a plugin ?`)
40
42
  }
41
43
 
42
44
  </script>
43
45
 
44
46
  <template>
45
- <v-col
46
- :cols="modelValue.cols"
47
- :class="nodeClasses"
48
- >
49
- <node-slot
50
- v-if="modelValue.layout.slots?.before"
51
- key="before"
52
- :layout-slot="modelValue.layout.slots?.before"
53
- :node="modelValue"
54
- :stateful-layout="statefulLayout"
55
- :class="beforeAfterClasses[modelValue.options.density]"
56
- />
47
+ <v-defaults-provider :defaults="{global: { density: props.modelValue.options.density }}">
48
+ <v-col
49
+ v-if="modelValue.layout.comp !== 'none'"
50
+ :cols="modelValue.cols"
51
+ :class="nodeClasses"
52
+ >
53
+ <node-slot
54
+ v-if="modelValue.layout.slots?.before"
55
+ key="before"
56
+ :layout-slot="modelValue.layout.slots?.before"
57
+ :node="modelValue"
58
+ :stateful-layout="statefulLayout"
59
+ :class="beforeAfterClasses[modelValue.options.density]"
60
+ />
57
61
 
58
- <help-message
59
- v-if="modelValue.layout.help"
60
- :node="modelValue"
61
- :class="beforeAfterClasses[modelValue.options.density]"
62
- />
63
- <node-slot
64
- v-if="modelValue.layout.slots?.component"
65
- key="component"
66
- :layout-slot="modelValue.layout.slots?.component"
67
- :node="modelValue"
68
- :stateful-layout="statefulLayout"
69
- />
70
- <component
71
- :is="props.statefulLayout.options.nodeComponents[modelValue.layout.comp]"
72
- v-else-if="modelValue.layout.comp !== 'none' "
73
- :model-value="modelValue"
74
- :stateful-layout="statefulLayout"
75
- />
62
+ <help-message
63
+ v-if="modelValue.layout.help && !modelValue.options.summary"
64
+ :node="modelValue"
65
+ :class="beforeAfterClasses[modelValue.options.density]"
66
+ />
67
+ <node-slot
68
+ v-if="modelValue.layout.slots?.component"
69
+ key="component"
70
+ :layout-slot="modelValue.layout.slots?.component"
71
+ :node="modelValue"
72
+ :stateful-layout="statefulLayout"
73
+ />
74
+ <component
75
+ :is="props.statefulLayout.options.nodeComponents[modelValue.layout.comp]"
76
+ v-else
77
+ :model-value="modelValue"
78
+ :stateful-layout="statefulLayout"
79
+ />
76
80
 
77
- <node-slot
78
- v-if="modelValue.layout.slots?.after"
79
- key="after"
80
- :layout-slot="modelValue.layout.slots?.after"
81
- :node="modelValue"
82
- :stateful-layout="statefulLayout"
83
- :class="beforeAfterClasses[modelValue.options.density]"
84
- />
85
- </v-col>
81
+ <node-slot
82
+ v-if="modelValue.layout.slots?.after"
83
+ key="after"
84
+ :layout-slot="modelValue.layout.slots?.after"
85
+ :node="modelValue"
86
+ :stateful-layout="statefulLayout"
87
+ :class="beforeAfterClasses[modelValue.options.density]"
88
+ />
89
+ </v-col>
90
+ </v-defaults-provider>
86
91
  </template>