@koumoul/vjsf 2.12.0-beta.8 → 2.12.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.
package/lib/VJsf.js CHANGED
@@ -28,8 +28,12 @@ import {
28
28
  VIcon,
29
29
  VInput,
30
30
  VRow,
31
+ VList,
32
+ VListItem,
31
33
  VListItemContent,
34
+ VListItemIcon,
32
35
  VListItemTitle,
36
+ VListItemAction,
33
37
  VMenu,
34
38
  VRadio,
35
39
  VRadioGroup,
@@ -80,8 +84,12 @@ export default {
80
84
  VIcon,
81
85
  VInput,
82
86
  VRow,
87
+ VList,
88
+ VListItem,
83
89
  VListItemContent,
90
+ VListItemIcon,
84
91
  VListItemTitle,
92
+ VListItemAction,
85
93
  VMenu,
86
94
  VRadio,
87
95
  VRadioGroup,
package/lib/VJsfNoDeps.js CHANGED
@@ -50,7 +50,8 @@ export default {
50
50
  modelKey: { type: [String, Number], default: 'root' },
51
51
  parentKey: { type: String, default: '' },
52
52
  required: { type: Boolean, default: false },
53
- sectionDepth: { type: Number, default: 0 }
53
+ sectionDepth: { type: Number, default: 0 },
54
+ sharedData: { type: Object, default: () => ({}) }
54
55
  },
55
56
  data() {
56
57
  return {
@@ -395,7 +396,7 @@ export default {
395
396
  },
396
397
  input(value, initial = false, fastForward = true) {
397
398
  if (Array.isArray(value) && this.separator) value = value.join(this.separator)
398
- if (value === null || value === undefined || (value === '' && this.fullSchema.default !== '')) {
399
+ if (value === null || value === undefined) {
399
400
  if (this.fullSchema.nullable) {
400
401
  if (this.value !== null) {
401
402
  this.changed = true
@@ -105,7 +105,8 @@ export default {
105
105
  options: { ...this.fullOptions, hideReadOnly: false },
106
106
  optionsRoot: this.initialOptions,
107
107
  sectionDepth: this.sectionDepth + 1,
108
- separateValidation: false
108
+ separateValidation: false,
109
+ sharedData: this.sharedData
109
110
  },
110
111
  ref: modelKey,
111
112
  scopedSlots: this.childScopedSlots(this.fullSchema.key)
@@ -173,7 +174,8 @@ export default {
173
174
  options,
174
175
  optionsRoot: this.initialOptions,
175
176
  sectionDepth: this.sectionDepth + 1,
176
- separateValidation: this.fullOptions.editMode !== 'inline'
177
+ separateValidation: this.fullOptions.editMode !== 'inline',
178
+ sharedData: this.sharedData
177
179
  },
178
180
  scopedSlots: this.childScopedSlots(this.fullSchema.key),
179
181
  ref: 'item-' + i,
@@ -195,6 +197,110 @@ export default {
195
197
  }
196
198
  }, this.childSlots(h, this.fullSchema.key))
197
199
  },
200
+ renderArrayItemMenu(h, item, header, isCurrentInlineEdit) {
201
+ if (this.disabled || this.fromUrl || this.fullSchema.fromData) return
202
+ const menuItems = []
203
+
204
+ for (const operation of this.fullOptions.arrayOperations) {
205
+ if (operation === 'duplicate') {
206
+ menuItems.push({
207
+ title: this.fullOptions.messages.duplicate,
208
+ color: 'default',
209
+ icon: this.fullOptions.icons.duplicate,
210
+ disabled: false,
211
+ on: { click: () => {
212
+ const index = this.value.findIndex(i => i === item)
213
+ const value = [...this.value]
214
+ value.splice(index, 0, { ...item })
215
+ this.input(value)
216
+ this.change()
217
+ this.shouldValidate = true
218
+ header.componentInstance.validate()
219
+ } }
220
+ })
221
+ }
222
+
223
+ if (operation === 'delete') {
224
+ menuItems.push({
225
+ title: this.fullOptions.messages.delete,
226
+ color: 'warning',
227
+ icon: this.fullOptions.icons.delete,
228
+ disabled: isCurrentInlineEdit,
229
+ on: { click: () => {
230
+ const value = this.value.filter(i => i !== item)
231
+ this.input(value)
232
+ this.change()
233
+ this.shouldValidate = true
234
+ header.componentInstance.validate()
235
+ } }
236
+ })
237
+ }
238
+
239
+ if (operation === 'copy' && this.fullSchema['x-arrayGroup']) {
240
+ menuItems.push({
241
+ title: this.fullOptions.messages.copy,
242
+ color: 'default',
243
+ icon: this.fullOptions.icons.copy,
244
+ disabled: false,
245
+ on: { click: () => {
246
+ this.$set(this.sharedData, 'clipboard_' + this.fullSchema['x-arrayGroup'], item)
247
+ } }
248
+ })
249
+ }
250
+
251
+ if (operation === 'paste' && this.fullSchema['x-arrayGroup']) {
252
+ menuItems.push({
253
+ title: this.fullOptions.messages.paste,
254
+ color: 'primary',
255
+ icon: this.fullOptions.icons.paste,
256
+ disabled: !(this.sharedData['clipboard_' + this.fullSchema['x-arrayGroup']]),
257
+ on: { click: () => {
258
+ if (this.fullOptions.editMode === 'inline') {
259
+ this.editabledArrayProp.editItem = null
260
+ this.editabledArrayProp.currentDialog = null
261
+ }
262
+ const index = this.value.findIndex(i => i === item)
263
+ const value = [...this.value]
264
+ value[index] = this.sharedData['clipboard_' + this.fullSchema['x-arrayGroup']]
265
+ this.input(value)
266
+ this.change()
267
+ this.shouldValidate = true
268
+ header.componentInstance.validate()
269
+ } }
270
+ })
271
+ }
272
+ }
273
+
274
+ if (!menuItems.length) return
275
+
276
+ // if there is only one item, do not create a menu but instead a single button
277
+ if (menuItems.length === 1) {
278
+ return h('v-btn', {
279
+ props: { icon: true, disabled: menuItems[0].disabled },
280
+ on: menuItems[0].on,
281
+ attrs: { title: menuItems[0].title },
282
+ class: 'ml-1'
283
+ }, [h('v-icon', { props: { color: menuItems[0].color } }, [menuItems[0].icon])])
284
+ }
285
+ return h('v-menu', {
286
+ props: { offsetY: true, left: true },
287
+ scopedSlots: {
288
+ activator: ({ on }) => h('v-btn', {
289
+ props: { icon: true },
290
+ attrs: { title: this.fullOptions.messages.openMenu },
291
+ class: 'ml-1',
292
+ on
293
+ }, [h('v-icon', this.fullOptions.icons.arrayMenu)]),
294
+ default: () => h('v-list', { class: 'pa-0', props: { dense: true } }, menuItems.map(menuItem => h('v-list-item', {
295
+ on: menuItem.on,
296
+ props: { disabled: menuItem.disabled }
297
+ }, [
298
+ h('v-list-item-icon', { class: 'mr-2' }, [h('v-icon', { props: { color: menuItem.color, small: true } }, [menuItem.icon])]),
299
+ h('v-list-item-content', {}, [h('v-list-item-title', {}, [menuItem.title])])
300
+ ])))
301
+ }
302
+ })
303
+ },
198
304
  renderEditableArray(h) {
199
305
  if (!this.isEditableArray) return
200
306
  const headerChildren = []
@@ -207,13 +313,15 @@ export default {
207
313
  }
208
314
  }
209
315
  const header = h('v-input', {
210
- class: 'mt-2 pr-1 vjsf-array-header',
316
+ class: 'mt-2 mb-3 pr-1 vjsf-array-header',
211
317
  props: { label: this.label, rules: this.rules, value: this.value, validateOnBlur: !this.shouldValidate, hideDetails: 'auto' }
212
318
  }, headerChildren)
213
319
 
214
- let list
320
+ const sortable = !this.fullOptions.disableSorting && !this.fullSchema.readOnly
321
+
322
+ let listItems
215
323
  if (this.value && this.value.length) {
216
- const listItems = this.value.filter(item => !!item).map((item, i) => {
324
+ listItems = this.value.filter(item => !!item).map((item, i) => {
217
325
  let editAction
218
326
  const isCurrentInlineEdit = this.fullOptions.editMode === 'inline' && this.editabledArrayProp.currentDialog === i
219
327
  if (!this.disabled && this.fullOptions.arrayOperations.includes('update')) {
@@ -224,20 +332,7 @@ export default {
224
332
  }
225
333
  }
226
334
 
227
- let deleteAction
228
- if (!this.disabled && !this.fromUrl && !this.fullSchema.fromData && this.fullOptions.arrayOperations.includes('delete')) {
229
- deleteAction = h('v-btn', { props: { icon: true, color: 'warning', disabled: isCurrentInlineEdit },
230
- attrs: { id: this.fullOptions.idPrefix + this.dashKey + '-' + i + '--delete-button' },
231
- class: { 'vjsf-array-delete-button': true },
232
- on: { click: () => {
233
- const value = this.value.filter(i => i !== item)
234
- this.input(value)
235
- this.change()
236
- this.shouldValidate = true
237
- header.componentInstance.validate()
238
- } } }, [h('v-icon', this.fullOptions.icons.delete)])
239
- }
240
- const actions = h('v-card-actions', { class: 'pa-0' }, [h('v-spacer'), editAction, deleteAction])
335
+ const actions = h('v-card-actions', { class: 'pa-0' }, [h('v-spacer'), editAction, this.renderArrayItemMenu(h, item, header, isCurrentInlineEdit)])
241
336
 
242
337
  let itemChild, cardStyle, itemKey
243
338
  if (isCurrentInlineEdit) {
@@ -245,12 +340,8 @@ export default {
245
340
  itemKey = 'item-edit-' + i
246
341
  } else {
247
342
  itemChild = this.renderArrayItemRO(h, item, i)
248
- itemKey = this.cached(`item-key-${i}`, { item }, () => {
249
- return `${i}-${new Date().getTime()}`
250
- })
251
- if (!this.fullOptions.disableSorting) {
252
- cardStyle = 'cursor: move;'
253
- }
343
+ itemKey = this.cached(`item-key-${i}`, { item }, () => `${i}-${new Date().getTime()}`)
344
+ if (sortable) cardStyle = 'cursor: move;'
254
345
  }
255
346
 
256
347
  const titleClass = 'py-2 pr-2 ' + this.fullOptions.arrayItemsTitlesClasses[this.sectionDepth] || this.fullOptions.arrayItemsTitlesClasses[this.fullOptions.arrayItemsTitlesClasses.length - 1]
@@ -291,7 +382,7 @@ export default {
291
382
  })
292
383
  }
293
384
 
294
- let itemClass = 'pa-2 vjsf-array-item'
385
+ let itemClass = 'py-1 vjsf-array-item'
295
386
  if (isCurrentInlineEdit) itemClass += ' vjsf-array-item-active'
296
387
  return h('v-col', { props: this.fullOptions.arrayItemColProps, class: itemClass, key: itemKey }, [
297
388
  h('v-card', {
@@ -301,18 +392,26 @@ export default {
301
392
  }, cardChildren)
302
393
  ])
303
394
  })
304
- const noSort = this.fullOptions.disableSorting
305
- list = noSort ? h('v-row', { class: 'pt-2 vjsf-array' }, listItems) : h('draggable', {
306
- props: { value: this.value },
307
- class: 'row pt-2 vjsf-array',
308
- on: { input: (value) => {
395
+ }
396
+
397
+ let newValue
398
+ const list = !sortable ? h('v-row', { class: 'vjsf-array' }, listItems) : h('draggable', {
399
+ props: { value: this.value },
400
+ attrs: { group: this.fullSchema['x-arrayGroup'] || this.fullKey, ...this.fullOptions.sortableOptions },
401
+ class: 'row draggable vjsf-array',
402
+ on: {
403
+ change: async (evt) => {
404
+ if (evt.added) await this.$nextTick()
309
405
  this.editabledArrayProp.editItem = null
310
406
  this.editabledArrayProp.currentDialog = null
311
- this.input(value)
407
+ this.input(newValue)
312
408
  this.change()
313
409
  this.shouldValidate = true
314
- } } }, listItems)
315
- }
410
+ },
411
+ input: async (value) => {
412
+ newValue = value
413
+ }
414
+ } }, listItems)
316
415
 
317
416
  return [header, list]
318
417
  }
@@ -144,7 +144,6 @@ export default {
144
144
  h('v-card-text', { class: { 'pa-0': true } }, [childProp]),
145
145
  isLast ? null : h('v-card-actions', { class: { 'px-0': true } }, [h('v-btn', { props: { color: 'primary' },
146
146
  on: { click: () => {
147
- console.log(childProp)
148
147
  if (childProp.componentInstance.validate(true)) {
149
148
  this.currentStep += 1
150
149
  }
@@ -191,7 +190,8 @@ export default {
191
190
  required: forceRequired || !!(this.fullSchema.required && this.fullSchema.required.includes(schema.key)),
192
191
  options: { ...this.fullOptions, autofocus: this.fullOptions.autofocus && this.objectContainerChildrenCount === 1 },
193
192
  optionsRoot: this.initialOptions,
194
- sectionDepth
193
+ sectionDepth,
194
+ sharedData: this.sharedData
195
195
  },
196
196
  class: this.fullOptions.childrenClass,
197
197
  scopedSlots: this.childScopedSlots(modelKey),
@@ -345,6 +345,10 @@ export default {
345
345
 
346
346
  if (this.openEndedSelect) {
347
347
  tag = 'v-combobox'
348
+ if (!props.multiple) {
349
+ props.hideSelected = true
350
+ on.input = value => this.input(value || '')
351
+ }
348
352
  }
349
353
 
350
354
  tag = this.customTag ? this.customTag : tag
@@ -1,4 +1,4 @@
1
- import { mdiCalendar, mdiClock, mdiInformation, mdiPlus, mdiPencil, mdiDelete } from '@mdi/js'
1
+ import { mdiCalendar, mdiClock, mdiInformation, mdiPlus, mdiPencil, mdiDelete, mdiDotsVertical, mdiContentDuplicate, mdiContentCopy, mdiContentPaste } from '@mdi/js'
2
2
 
3
3
  export const defaultOptions = {
4
4
  locale: '',
@@ -45,13 +45,14 @@ export const defaultOptions = {
45
45
  filesAsDataUrl: false,
46
46
  hideTooltips: false,
47
47
  disableSorting: false,
48
+ sortableOptions: {},
48
49
  context: {},
49
50
  rules: {},
50
51
  initialValidation: 'defined',
51
52
  idPrefix: '',
52
53
  markdownit: {},
53
54
  editMode: 'dialog',
54
- arrayOperations: ['create', 'update', 'delete'],
55
+ arrayOperations: ['create', 'update', 'duplicate', 'copy', 'paste', 'delete'],
55
56
  autofocus: false,
56
57
  httpOptions: {},
57
58
  selectAll: false,
@@ -94,7 +95,12 @@ export const localizedMessages = {
94
95
  undo: 'Undo',
95
96
  redo: 'Redo',
96
97
  selectAll: 'Select all',
97
- stepperContinue: 'continue'
98
+ stepperContinue: 'continue',
99
+ openMenu: 'open menu',
100
+ delete: 'delete',
101
+ duplicate: 'duplicate',
102
+ copy: 'copy',
103
+ paste: 'paste'
98
104
  },
99
105
  fr: {
100
106
  required: 'Cette information est obligatoire',
@@ -129,7 +135,12 @@ export const localizedMessages = {
129
135
  undo: 'Défaire',
130
136
  redo: 'Refaire',
131
137
  selectAll: 'Tout sélectionner',
132
- stepperContinue: 'continuer'
138
+ stepperContinue: 'continuer',
139
+ openMenu: 'ouvrir le menu',
140
+ delete: 'supprimer',
141
+ duplicate: 'dupliquer',
142
+ copy: 'copier',
143
+ paste: 'coller'
133
144
  },
134
145
  es: {
135
146
  required: 'Esta información es requerida',
@@ -234,7 +245,11 @@ export const iconSets = {
234
245
  info: mdiInformation,
235
246
  add: mdiPlus,
236
247
  edit: mdiPencil,
237
- delete: mdiDelete
248
+ delete: mdiDelete,
249
+ arrayMenu: mdiDotsVertical,
250
+ duplicate: mdiContentDuplicate,
251
+ copy: mdiContentCopy,
252
+ paste: mdiContentPaste
238
253
  },
239
254
  mdi: {
240
255
  calendar: 'mdi-calendar',
@@ -242,7 +257,11 @@ export const iconSets = {
242
257
  info: 'mdi-information',
243
258
  add: 'mdi-plus',
244
259
  edit: 'mdi-pencil',
245
- delete: 'mdi-delete'
260
+ delete: 'mdi-delete',
261
+ arrayMenu: 'mdi-dots-vertical',
262
+ duplicate: 'mdi-plus-circle-multiple-outline',
263
+ copy: 'mdi-content-copy',
264
+ paste: 'mdi-content-paste'
246
265
  },
247
266
  md: {
248
267
  calendar: 'event',
@@ -250,7 +269,11 @@ export const iconSets = {
250
269
  info: 'info',
251
270
  add: 'add',
252
271
  edit: 'create',
253
- delete: 'delete'
272
+ delete: 'delete',
273
+ arrayMenu: 'more_vert',
274
+ duplicate: 'control_point_duplicate',
275
+ copy: 'copy',
276
+ paste: 'paste'
254
277
  },
255
278
  fa: {
256
279
  calendar: 'fa-calendar',
@@ -258,6 +281,10 @@ export const iconSets = {
258
281
  info: 'fa-info',
259
282
  add: 'fa-plus',
260
283
  edit: 'fa-edit',
261
- delete: 'fa-trash'
284
+ delete: 'fa-trash',
285
+ arrayMenu: 'fa-ellipsis-vertical',
286
+ duplicate: 'fa-layer-plus',
287
+ copy: 'fa-copy',
288
+ paste: 'fa-paste'
262
289
  }
263
290
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koumoul/vjsf",
3
- "version": "2.12.0-beta.8",
3
+ "version": "2.12.1",
4
4
  "description": "Generate forms for the vuetify UI library (vuejs) based on annotated JSON schemas.",
5
5
  "main": "dist/main.js",
6
6
  "scripts": {
@@ -1,10 +0,0 @@
1
- # VJSF - test apps - compiled
2
-
3
- This page tests loading vjsf and all dependencies from compiled sources.
4
-
5
- ```
6
- npm install
7
- (cd ../.. && npm run build)
8
- ln -s ../../dist dist
9
- npm start
10
- ```
@@ -1,85 +0,0 @@
1
- <html>
2
- <head>
3
- <title>VJSF - test apps - compiled</title>
4
-
5
- <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
6
- <link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.x/css/materialdesignicons.min.css" rel="stylesheet">
7
- <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
8
- <link href="/dist/main.css" rel="stylesheet">
9
- </head>
10
- <body>
11
- <div id="app">
12
- <v-app>
13
- <v-main>
14
- <v-container>
15
- <h1>VJSF - Test app - Compiled</h1>
16
-
17
- <h2>Very basic form</h2>
18
- <v-form v-model="form1.valid">
19
- <v-jsf :schema="form1.schema" :options="form1.options" v-model="form1.model"></v-jsf>
20
- </v-form>
21
- valid={{form1.valid}}, model={{form1.model}}
22
-
23
- <h2>Form with draggable array elements</h2>
24
- <v-form v-model="form2.valid">
25
- <v-jsf :schema="form2.schema" :options="form2.options" v-model="form2.model"></v-jsf>
26
- </v-form>
27
- valid={{form2.valid}}, model={{form2.model}}
28
- </v-container>
29
-
30
- </v-main>
31
- </v-app>
32
- </div>
33
-
34
- <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
35
- <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
36
- <script src="/dist/main.js"></script>
37
- <script src="/dist/third-party.js"></script>
38
- <script>
39
- Vue.component('VJsf', VJsf.default)
40
- new Vue({
41
- el: '#app',
42
- vuetify: new Vuetify(),
43
- data() {
44
- return {
45
- form1: {
46
- valid: null,
47
- schema: {
48
- type: 'object',
49
- properties: {
50
- stringProp: { type: 'string', title: `I'm a string`, description: 'This description is used as a help message.' }
51
- }
52
- },
53
- options: {},
54
- model: {}
55
- },
56
- form2: {
57
- valid: null,
58
- schema: {
59
- type: 'object',
60
- properties: {
61
- arrayProp: {
62
- type: 'array',
63
- items: {
64
- type: 'object',
65
- properties: {
66
- stringProp: { type: 'string', title: `I'm a string` }
67
- }
68
- }
69
- }
70
- }
71
- },
72
- options: {},
73
- model: {
74
- arrayProp: [
75
- {stringProp: 'value 1'},
76
- {stringProp: 'value 2'}
77
- ]
78
- }
79
- }
80
- }
81
- }
82
- })
83
- </script>
84
- </body>
85
- </html>