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

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 (41) hide show
  1. package/package.json +8 -8
  2. package/src/compat/v2.js +1 -4
  3. package/src/compile/index.js +1 -1
  4. package/src/compile/v-jsf-compiled.vue.ejs +14 -52
  5. package/src/components/nodes/combobox.vue +1 -1
  6. package/src/components/nodes/list.vue +171 -88
  7. package/src/components/nodes/markdown.vue +218 -5
  8. package/src/components/nodes/number-field.vue +1 -1
  9. package/src/components/nodes/stepper.vue +98 -0
  10. package/src/components/nodes/text-field.vue +1 -1
  11. package/src/components/nodes/textarea.vue +1 -1
  12. package/src/components/options.js +22 -1
  13. package/src/components/types.ts +4 -0
  14. package/src/components/vjsf.vue +21 -104
  15. package/src/composables/use-dnd.js +54 -0
  16. package/src/composables/use-vjsf.js +119 -0
  17. package/src/styles/vjsf.css +10 -0
  18. package/src/utils/arrays.js +15 -0
  19. package/src/utils/props.js +16 -5
  20. package/types/compat/v2.d.ts.map +1 -1
  21. package/types/components/fragments/select-item.vue.d.ts +2 -2
  22. package/types/components/fragments/select-selection.vue.d.ts +2 -2
  23. package/types/components/nodes/markdown.vue.d.ts.map +1 -1
  24. package/types/components/nodes/stepper.vue.d.ts +10 -0
  25. package/types/components/nodes/stepper.vue.d.ts.map +1 -0
  26. package/types/components/options.d.ts +1 -0
  27. package/types/components/options.d.ts.map +1 -1
  28. package/types/components/tree.vue.d.ts +2 -2
  29. package/types/components/types.d.ts +5 -1
  30. package/types/components/types.d.ts.map +1 -1
  31. package/types/components/vjsf.vue.d.ts +5 -6
  32. package/types/components/vjsf.vue.d.ts.map +1 -1
  33. package/types/composables/use-dnd.d.ts +21 -0
  34. package/types/composables/use-dnd.d.ts.map +1 -0
  35. package/types/composables/use-vjsf.d.ts +17 -0
  36. package/types/composables/use-vjsf.d.ts.map +1 -0
  37. package/types/utils/arrays.d.ts +9 -0
  38. package/types/utils/arrays.d.ts.map +1 -0
  39. package/types/utils/props.d.ts +4 -2
  40. package/types/utils/props.d.ts.map +1 -1
  41. package/src/utils/clone.js +0 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koumoul/vjsf",
3
- "version": "3.0.0-alpha.1",
3
+ "version": "3.0.0-alpha.3",
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",
@@ -46,8 +52,7 @@
46
52
  "vuetify": "^3.4.2"
47
53
  },
48
54
  "dependencies": {
49
- "@json-layout/core": "0.2.0",
50
- "@json-layout/vocabulary": "0.2.0",
55
+ "@json-layout/core": "0.4.0",
51
56
  "@vueuse/core": "^10.5.0",
52
57
  "debug": "^4.3.4",
53
58
  "easymde": "^2.18.0",
@@ -57,13 +62,8 @@
57
62
  "devDependencies": {
58
63
  "@types/debug": "^4.1.8",
59
64
  "@types/ejs": "^3.1.2",
60
- "relative-deps": "^1.0.7",
61
65
  "vitest": "^0.34.5",
62
66
  "vue": "^3.3.4",
63
67
  "vue-tsc": "^1.8.15"
64
- },
65
- "relativeDependencies": {
66
- "@json-layout/core": "../../json-layout/core/",
67
- "@json-layout/vocabulary": "../../json-layout/vocabulary/"
68
68
  }
69
69
  }
package/src/compat/v2.js CHANGED
@@ -1,14 +1,11 @@
1
1
  import ajvModule from 'ajv'
2
- import rfdc from 'rfdc'
3
2
  import addFormats from 'ajv-formats'
4
- import { resolveRefs } from '@json-layout/core'
3
+ import { resolveRefs, clone } from '@json-layout/core'
5
4
  import { isPartialGetItemsObj } from '@json-layout/vocabulary'
6
5
 
7
6
  // @ts-ignore
8
7
  const Ajv = /** @type {typeof ajvModule.default} */ (ajvModule)
9
8
 
10
- const clone = rfdc()
11
-
12
9
  const processFragment = (/** @type {import("ajv").SchemaObject} */schema) => {
13
10
  if (!schema.layout) {
14
11
  /** @type import('@json-layout/vocabulary').PartialCompObject */
@@ -37,7 +37,7 @@ function listComps (comps, layout) {
37
37
  * @param {string} baseImport
38
38
  * @returns {string}
39
39
  */
40
- export function compile (schema, baseImport = '@koumoul/vjsf/components') {
40
+ export function compile (schema, baseImport = '@koumoul/vjsf') {
41
41
  const compiledLayout = compileLayout(schema, { code: true })
42
42
  const compiledLayoutCode = serializeCompiledLayout(compiledLayout)
43
43
  /** @type Set<string> */
@@ -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
+ computed(() => compiledLayout)
34
+ )
73
35
  </script>
74
36
 
75
37
  <template>
@@ -37,7 +37,6 @@ export default defineComponent({
37
37
  if (props.statefulLayout.stateTree === lastStateTree && props.statefulLayout.options.context === lastContext) return
38
38
  lastStateTree = props.statefulLayout.stateTree
39
39
  lastContext = props.statefulLayout.options.context ?? null
40
- console.log('HAS ITEMS ?', hasItems.value)
41
40
  if (hasItems.value) {
42
41
  loading.value = true
43
42
  items.value = await props.statefulLayout.getItems(props.modelValue)
@@ -52,6 +51,7 @@ export default defineComponent({
52
51
  const fieldProps = computed(() => {
53
52
  const fieldProps = getInputProps(props.modelValue, props.statefulLayout)
54
53
  fieldProps.loading = loading.value
54
+ fieldProps.returnObject = false
55
55
  if (hasItems.value) fieldProps.items = items.value
56
56
  if (props.modelValue.options.readOnly) fieldProps.menuProps = { modelValue: false }
57
57
  if (props.modelValue.layout.multiple) {
@@ -1,10 +1,12 @@
1
1
  <script setup>
2
+ import { watch, computed, ref } from 'vue'
2
3
  import { VList, VListItem, VListItemAction, VBtn, VMenu, VIcon } from 'vuetify/components'
3
- import { isSection } from '@json-layout/core'
4
+ import { isSection, clone } from '@json-layout/core'
4
5
  import Node from '../node.vue'
5
- import clone from '../../utils/clone.js'
6
+ import { moveArrayItem } from '../../utils/arrays.js'
7
+ import useDnd from '../../composables/use-dnd.js'
6
8
 
7
- defineProps({
9
+ const props = defineProps({
8
10
  modelValue: {
9
11
  /** @type import('vue').PropType<import('../types.js').VjsfListNode> */
10
12
  type: Object,
@@ -17,96 +19,177 @@ defineProps({
17
19
  }
18
20
  })
19
21
 
22
+ /* use composable for drag and drop */
23
+ const { activeDnd, sortableArray, draggable, itemBind, handleBind } = useDnd(props.modelValue.children, () => {
24
+ props.statefulLayout.input(props.modelValue, sortableArray.value.map((child) => child.data))
25
+ })
26
+ watch(() => props.modelValue.children, (array) => { sortableArray.value = array })
27
+
28
+ /* manage hovered and edited items */
29
+ const editedItem = computed(() => {
30
+ return props.statefulLayout.activeItems[props.modelValue.fullKey]
31
+ })
32
+ const hoveredItem = ref(1)
33
+ const activeItem = computed(() => {
34
+ if (
35
+ props.modelValue.layout.listActions.includes('edit') &&
36
+ props.modelValue.layout.listEditMode === 'inline-single' &&
37
+ editedItem.value !== undefined
38
+ ) {
39
+ return editedItem.value
40
+ }
41
+ return hoveredItem.value
42
+ })
43
+
44
+ const buttonDensity = computed(() => {
45
+ if (props.modelValue.options.density === 'default') return 'comfortable'
46
+ return props.modelValue.options.density
47
+ })
48
+
20
49
  </script>
21
50
 
22
51
  <template>
23
- <v-list :density="modelValue.options.density">
24
- <v-list-subheader v-if="modelValue.layout.title">
25
- {{ modelValue.layout.title }}
26
- </v-list-subheader>
27
- <v-list-item
28
- v-for="(child, childIndex) of modelValue.children"
29
- :key="child.key"
30
- >
31
- <v-row>
32
- <node
33
- v-for="grandChild of isSection(child) ? child.children : [child]"
34
- :key="grandChild.fullKey"
35
- :model-value="/** @type import('../types.js').VjsfNode */(grandChild)"
36
- :stateful-layout="statefulLayout"
37
- />
38
- </v-row>
39
- <template #append>
40
- <v-list-item-action
41
- v-if="modelValue.layout.listActions.includes('edit') && modelValue.layout.listEditMode === 'inline-single'"
42
- class="ml-1"
43
- >
44
- <v-btn
45
- v-if="child.options.readOnly"
46
- :title="modelValue.messages.edit"
47
- icon="mdi-pencil"
48
- variant="text"
49
- color="primary"
50
- :density="modelValue.options.density"
51
- @click="statefulLayout.activateItem(modelValue, childIndex)"
52
- />
53
- <v-btn
54
- v-else
55
- :title="modelValue.messages.edit"
56
- icon="mdi-pencil"
57
- variant="flat"
58
- color="primary"
59
- :density="modelValue.options.density"
60
- @click="statefulLayout.deactivateItem(modelValue)"
61
- />
62
- </v-list-item-action>
63
- <v-list-item-action
64
- v-if="modelValue.layout.listActions.includes('delete') || modelValue.layout.listActions.includes('duplicate')"
65
- class="ml-1"
52
+ <v-sheet :elevation="1">
53
+ <v-list :density="modelValue.options.density">
54
+ <v-list-subheader v-if="modelValue.layout.title">
55
+ {{ modelValue.layout.title }}
56
+ </v-list-subheader>
57
+ <template
58
+ v-for="(child, childIndex) of sortableArray"
59
+ :key="props.modelValue.children.findIndex(c => c === child)"
60
+ >
61
+ <v-list-item
62
+ v-bind="itemBind(childIndex)"
63
+ :density="modelValue.options.density"
64
+ :draggable="draggable === childIndex"
65
+ :variant="editedItem === childIndex ? 'outlined' : 'flat'"
66
+ class="pa-1 vjsf-list-item"
67
+ @mouseenter="hoveredItem = childIndex"
68
+ @mouseleave="hoveredItem = -1"
66
69
  >
67
- <v-menu location="bottom end">
68
- <template #activator="{props: activatorProps}">
69
- <v-btn
70
- v-bind="activatorProps"
71
- icon="mdi-dots-vertical"
72
- variant="plain"
73
- :density="modelValue.options.density"
74
- />
75
- </template>
76
- <v-list :density="modelValue.options.density">
77
- <v-list-item
78
- v-if="modelValue.layout.listActions.includes('delete')"
79
- base-color="warning"
80
- @click="statefulLayout.input(modelValue, [...modelValue.data.slice(0, childIndex), ...modelValue.data.slice(childIndex + 1)])"
70
+ <v-row class="ma-0">
71
+ <node
72
+ v-for="grandChild of isSection(child) ? child.children : [child]"
73
+ :key="grandChild.fullKey"
74
+ :model-value="/** @type import('../types.js').VjsfNode */(grandChild)"
75
+ :stateful-layout="statefulLayout"
76
+ />
77
+ </v-row>
78
+ <template
79
+ v-if="activeItem === childIndex"
80
+ #append
81
+ >
82
+ <div>
83
+ <v-list-item-action
84
+ v-if="modelValue.layout.listActions.includes('edit') && modelValue.layout.listEditMode === 'inline-single'"
81
85
  >
82
- <template #prepend>
83
- <v-icon icon="mdi-delete" />
84
- </template>
85
- {{ modelValue.messages.delete }}
86
- </v-list-item>
87
-
88
- <v-list-item
89
- v-if="modelValue.layout.listActions.includes('duplicate')"
90
- @click="statefulLayout.input(modelValue, [...modelValue.data.slice(0, childIndex), clone(child.data), ...modelValue.data.slice(childIndex)])"
86
+ <v-btn
87
+ v-if="editedItem !== childIndex"
88
+ :title="modelValue.messages.edit"
89
+ icon="mdi-pencil"
90
+ variant="text"
91
+ color="primary"
92
+ :density="buttonDensity"
93
+ @click="statefulLayout.activateItem(modelValue, childIndex)"
94
+ />
95
+ <v-btn
96
+ v-else
97
+ :title="modelValue.messages.edit"
98
+ icon="mdi-pencil"
99
+ variant="flat"
100
+ color="primary"
101
+ :density="buttonDensity"
102
+ @click="statefulLayout.deactivateItem(modelValue)"
103
+ />
104
+ </v-list-item-action>
105
+ <v-list-item-action
106
+ v-if="editedItem === undefined && modelValue.layout.listActions.includes('sort') && activeDnd"
107
+ >
108
+ <v-btn
109
+ :title="modelValue.messages.sort"
110
+ icon="mdi-arrow-up-down"
111
+ variant="plain"
112
+ :density="buttonDensity"
113
+ v-bind="handleBind(childIndex)"
114
+ />
115
+ </v-list-item-action>
116
+ <v-list-item-action
117
+ v-if="editedItem === undefined && (modelValue.layout.listActions.includes('delete') || modelValue.layout.listActions.includes('duplicate') || modelValue.layout.listActions.includes('sort'))"
91
118
  >
92
- <template #prepend>
93
- <v-icon icon="mdi-content-duplicate" />
94
- </template>
95
- {{ modelValue.messages.duplicate }}
96
- </v-list-item>
97
- </v-list>
98
- </v-menu>
99
- </v-list-item-action>
119
+ <v-menu location="bottom end">
120
+ <template #activator="{props: activatorProps}">
121
+ <v-btn
122
+ v-bind="activatorProps"
123
+ icon="mdi-dots-vertical"
124
+ variant="plain"
125
+ slim
126
+ :density="buttonDensity"
127
+ />
128
+ </template>
129
+ <v-list :density="modelValue.options.density">
130
+ <v-list-item
131
+ v-if="modelValue.layout.listActions.includes('delete')"
132
+ base-color="warning"
133
+ @click="statefulLayout.input(modelValue, [...modelValue.data.slice(0, childIndex), ...modelValue.data.slice(childIndex + 1)])"
134
+ >
135
+ <template #prepend>
136
+ <v-icon icon="mdi-delete" />
137
+ </template>
138
+ {{ modelValue.messages.delete }}
139
+ </v-list-item>
140
+ <v-list-item
141
+ v-if="modelValue.layout.listActions.includes('duplicate')"
142
+ @click="statefulLayout.input(modelValue, [...modelValue.data.slice(0, childIndex), clone(child.data), ...modelValue.data.slice(childIndex)])"
143
+ >
144
+ <template #prepend>
145
+ <v-icon icon="mdi-content-duplicate" />
146
+ </template>
147
+ {{ modelValue.messages.duplicate }}
148
+ </v-list-item>
149
+ <v-list-item
150
+ v-if="modelValue.layout.listActions.includes('sort')"
151
+ @click="statefulLayout.input(modelValue, moveArrayItem(modelValue.data, childIndex, childIndex - 1))"
152
+ >
153
+ <template #prepend>
154
+ <v-icon icon="mdi-arrow-up" />
155
+ </template>
156
+ {{ modelValue.messages.up }}
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-down" />
164
+ </template>
165
+ {{ modelValue.messages.down }}
166
+ </v-list-item>
167
+ </v-list>
168
+ </v-menu>
169
+ </v-list-item-action>
170
+ </div>
171
+ </template>
172
+ </v-list-item>
173
+ <v-divider v-if="childIndex < modelValue.children.length - 1" />
100
174
  </template>
101
- </v-list-item>
102
- <v-list-item v-if="!modelValue.options.readOnly && modelValue.layout.listActions.includes('add')">
103
- <v-btn
104
- color="primary"
105
- :density="modelValue.options.density"
106
- @click="statefulLayout.input(modelValue, (modelValue.data || []).concat([clone(modelValue.skeleton.childrenTrees?.[0]?.root.defaultData)]), (modelValue.data || []).length)"
107
- >
108
- {{ modelValue.messages.addItem }}
109
- </v-btn>
110
- </v-list-item>
111
- </v-list>
175
+ <v-list-item v-if="!modelValue.options.readOnly && modelValue.layout.listActions.includes('add')">
176
+ <v-spacer />
177
+ <v-btn
178
+ color="primary"
179
+ :density="modelValue.options.density"
180
+ @click="statefulLayout.input(modelValue, (modelValue.data ?? []).concat([undefined]))"
181
+ >
182
+ {{ modelValue.messages.addItem }}
183
+ </v-btn>
184
+ <v-spacer />
185
+ </v-list-item>
186
+ </v-list>
187
+ </v-sheet>
112
188
  </template>
189
+
190
+ <style>
191
+ .vjsf-list-item .v-list-item__append {
192
+ height: 100%;
193
+ align-items: start;
194
+ }
195
+ </style>