@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
@@ -1,8 +1,9 @@
1
1
  <script>
2
- import { defineComponent, h, computed } from 'vue'
3
- import { VTextarea } from 'vuetify/components'
2
+ import { defineComponent, h, computed, onMounted, ref, onUnmounted, watch } from 'vue'
3
+ import { VInput, VLabel } from 'vuetify/components'
4
4
  import { getInputProps } from '../../utils/props.js'
5
5
  import { getCompSlots } from '../../utils/slots.js'
6
+ import 'easymde/dist/easymde.min.css'
6
7
 
7
8
  export default defineComponent({
8
9
  props: {
@@ -18,12 +19,224 @@ export default defineComponent({
18
19
  }
19
20
  },
20
21
  setup (props) {
22
+ /** @type {import('vue').Ref<null | HTMLElement>} */
23
+ const element = ref(null)
24
+
21
25
  const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout))
22
- const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
26
+ const fieldSlots = computed(() => {
27
+ const fieldSlots = getCompSlots(props.modelValue, props.statefulLayout)
28
+ fieldSlots.default = () => [
29
+ h('div', { style: 'width:100%' }, [
30
+ h(VLabel, { text: fieldProps.value.label }),
31
+ h('textarea', { ref: element, style: 'display:none' })
32
+ ])
33
+ ]
34
+ return fieldSlots
35
+ })
36
+
37
+ /** @type {EasyMDE | null} */
38
+ let easymde = null
39
+
40
+ const initEasyMDE = async () => {
41
+ if (!element.value) throw new Error('component was not mounted for markdown editor')
42
+
43
+ const EasyMDE = (await import('easymde')).default
44
+
45
+ const messages = props.modelValue.messages
46
+
47
+ const config = {
48
+ element: element.value,
49
+ initialValue: props.modelValue.data ?? '',
50
+ renderingConfig: {},
51
+ status: false,
52
+ autoDownloadFontAwesome: false,
53
+ spellChecker: false,
54
+ minHeight: '300px',
55
+ insertTexts: {
56
+ link: [messages.mdeLink1, messages.mdeLink2],
57
+ image: [messages.mdeImg1, messages.mdeImg2],
58
+ table: [messages.mdeTable1, messages.mdeTable2],
59
+ horizontalRule: ['', '\n\n-----\n\n']
60
+ },
61
+ // cf https://github.com/Ionaru/easy-markdown-editor/blob/master/src/js/easymde.js#L1380
62
+ toolbar: [{
63
+ name: 'bold',
64
+ action: EasyMDE.toggleBold,
65
+ className: 'mdi mdi-format-bold',
66
+ title: messages.bold
67
+ }, {
68
+ name: 'italic',
69
+ action: EasyMDE.toggleItalic,
70
+ className: 'mdi mdi-format-italic',
71
+ title: messages.italic
72
+ }, {
73
+ name: 'heading',
74
+ action: EasyMDE.toggleHeadingSmaller,
75
+ className: 'mdi mdi-format-title',
76
+ title: messages.heading
77
+ }, /* {
78
+ name: 'heading-1',
79
+ action: EasyMDE.toggleHeading1,
80
+ className: 'mdi mdi-format-title',
81
+ title: 'Titre 1'
82
+ }, {
83
+ name: 'heading-2',
84
+ action: EasyMDE.toggleHeading2,
85
+ className: 'mdi mdi-format-title',
86
+ title: 'Titre 2'
87
+ }, {
88
+ name: 'heading-3',
89
+ action: EasyMDE.toggleHeading3,
90
+ className: 'mdi mdi-format-title',
91
+ title: 'Titre 3'
92
+ }, */
93
+ '|',
94
+ {
95
+ name: 'quote',
96
+ action: EasyMDE.toggleBlockquote,
97
+ className: 'mdi mdi-format-quote-open',
98
+ title: messages.quote
99
+ },
100
+ {
101
+ name: 'unordered-list',
102
+ action: EasyMDE.toggleUnorderedList,
103
+ className: 'mdi mdi-format-list-bulleted',
104
+ title: messages.unorderedList
105
+ },
106
+ {
107
+ name: 'ordered-list',
108
+ action: EasyMDE.toggleOrderedList,
109
+ className: 'mdi mdi-format-list-numbered',
110
+ title: messages.orderedList
111
+ },
112
+ '|',
113
+ {
114
+ name: 'link',
115
+ action: EasyMDE.drawLink,
116
+ className: 'mdi mdi-link',
117
+ title: messages.createLink
118
+ },
119
+ {
120
+ name: 'image',
121
+ action: EasyMDE.drawImage,
122
+ className: 'mdi mdi-image',
123
+ title: messages.insertImage
124
+ },
125
+ {
126
+ name: 'table',
127
+ action: EasyMDE.drawTable,
128
+ className: 'mdi mdi-table',
129
+ title: messages.createTable
130
+ },
131
+ '|',
132
+ {
133
+ name: 'preview',
134
+ action: EasyMDE.togglePreview,
135
+ className: 'mdi mdi-eye text-accent',
136
+ title: messages.preview,
137
+ noDisable: true
138
+ },
139
+ '|',
140
+ {
141
+ name: 'undo',
142
+ action: EasyMDE.undo,
143
+ className: 'mdi mdi-undo',
144
+ title: messages.undo,
145
+ noDisable: true
146
+ },
147
+ {
148
+ name: 'redo',
149
+ action: EasyMDE.redo,
150
+ className: 'mdi mdi-redo',
151
+ title: messages.redo,
152
+ noDisable: true
153
+ },
154
+ '|',
155
+ {
156
+ name: 'guide',
157
+ action: 'https://simplemde.com/markdown-guide',
158
+ className: 'mdi mdi-help-circle text-success',
159
+ title: messages.mdeGuide,
160
+ noDisable: true
161
+ }
162
+ ],
163
+ ...props.modelValue.options.easyMDEOptions
164
+ }
165
+
166
+ if (easymde) easymde.toTextArea()
167
+ // @ts-ignore
168
+ easymde = new EasyMDE(config)
169
+
170
+ let changed = false
171
+ easymde.codemirror.on('change', () => {
172
+ changed = true
173
+ if (easymde) props.statefulLayout.input(props.modelValue, easymde.value())
174
+ })
175
+ /** @type {ReturnType<typeof setTimeout> | null} */
176
+ let blurTimeout = null
177
+ easymde.codemirror.on('blur', () => {
178
+ // timeout to prevent triggering save when clicking on a menu button
179
+ blurTimeout = setTimeout(() => {
180
+ if (changed) props.statefulLayout.blur(props.modelValue)
181
+ changed = false
182
+ }, 500)
183
+ })
184
+ easymde.codemirror.on('focus', () => {
185
+ if (blurTimeout) clearTimeout(blurTimeout)
186
+ })
187
+
188
+ if (props.modelValue.autofocus) {
189
+ easymde.codemirror.focus()
190
+ }
191
+ }
192
+
193
+ onMounted(initEasyMDE)
194
+
195
+ onUnmounted(() => {
196
+ if (easymde) easymde.toTextArea()
197
+ })
198
+
199
+ // update data from outside
200
+ watch(() => props.modelValue, () => {
201
+ if (easymde && (easymde.value() !== props.modelValue.data ?? '')) {
202
+ easymde.value(props.modelValue.data ?? '')
203
+ }
204
+ })
205
+
206
+ // update easymde config from outside
207
+ watch(() => [props.modelValue.messages, props.modelValue.options.easyMDEOptions], (newValues, oldValues) => {
208
+ if (newValues[0] !== oldValues[0] || newValues[1] !== oldValues[1]) {
209
+ initEasyMDE()
210
+ }
211
+ })
23
212
 
24
- // @ts-ignore
25
- return () => h(VTextarea, fieldProps.value, fieldSlots.value)
213
+ props.statefulLayout.events.on('autofocus', () => {
214
+ if (props.modelValue.autofocus && easymde) {
215
+ easymde.codemirror.focus()
216
+ }
217
+ })
218
+
219
+ return () => h(VInput, fieldProps.value, fieldSlots.value)
26
220
  }
27
221
  })
28
222
 
29
223
  </script>
224
+
225
+ <style>
226
+ .vjsf-node-markdown .v-input--density-compact .editor-toolbar {
227
+ padding: 0;
228
+ }
229
+ .vjsf-node-markdown .v-input--density-comfortable .editor-toolbar {
230
+ padding: 4px;
231
+ }
232
+
233
+ .vjsf-node-markdown .v-input--density-compact .CodeMirror-wrap {
234
+ padding-top: 2px;
235
+ padding-bottom: 2px;
236
+ }
237
+ .vjsf-node-markdown .v-input--density-comfortable .CodeMirror-wrap {
238
+ padding-top: 6px;
239
+ padding-bottom: 6px;
240
+ }
241
+
242
+ </style>
@@ -19,7 +19,7 @@ export default defineComponent({
19
19
  },
20
20
  setup (props) {
21
21
  const fieldProps = computed(() => {
22
- const fieldProps = getInputProps(props.modelValue, props.statefulLayout, ['step', 'min', 'max'])
22
+ const fieldProps = getInputProps(props.modelValue, props.statefulLayout, ['step', 'min', 'max', 'placeholder'])
23
23
  fieldProps.type = 'number'
24
24
  fieldProps['onUpdate:modelValue'] = (/** @type string */value) => props.statefulLayout.input(props.modelValue, value && Number(value))
25
25
  return fieldProps
@@ -0,0 +1,98 @@
1
+ <script setup>
2
+ import { ref, computed } from 'vue'
3
+ import { VStepper, VStepperHeader, VStepperItem, VContainer } from 'vuetify/components'
4
+ import { isSection } from '@json-layout/core'
5
+ import Node from '../node.vue'
6
+ import SectionHeader from '../fragments/section-header.vue'
7
+
8
+ const props = defineProps({
9
+ modelValue: {
10
+ /** @type import('vue').PropType<import('../types.js').VjsfStepperNode> */
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
+
21
+ const step = ref(0)
22
+
23
+ const firstErrorIndex = computed(() => {
24
+ const index = props.modelValue.children.findIndex(child => child.validated && !!(child.error || child.childError))
25
+ return index === -1 ? props.modelValue.children.length : index
26
+ })
27
+
28
+ const goNext = () => {
29
+ console.log(props.statefulLayout.validationState)
30
+ const child = props.modelValue.children[step.value]
31
+ props.statefulLayout.validateNodeRecurse(child)
32
+ console.log(props.statefulLayout.validationState)
33
+ if (!(child.error || child.childError)) step.value++
34
+ }
35
+ </script>
36
+
37
+ <template>
38
+ <section-header :node="modelValue" />
39
+ <v-stepper v-model="step">
40
+ <v-stepper-header>
41
+ <template
42
+ v-for="(child, i) of modelValue.children"
43
+ :key="child.key"
44
+ >
45
+ <v-stepper-item
46
+ :value="i"
47
+ :title="/** @type {string | undefined} */(child.layout.title ?? child.layout.label)"
48
+ :error="child.validated && !!(child.error || child.childError)"
49
+ :complete="child.validated && !(child.error || child.childError)"
50
+ :editable="i <= firstErrorIndex"
51
+ />
52
+ <v-divider />
53
+ </template>
54
+ </v-stepper-header>
55
+ <v-stepper-window>
56
+ <v-stepper-window-item
57
+ v-for="(child) of modelValue.children"
58
+ :key="child.key"
59
+ >
60
+ <v-container
61
+ fluid
62
+ class="pa-0"
63
+ >
64
+ <v-row>
65
+ <node
66
+ v-for="grandChild of isSection(child) ? child.children : [child]"
67
+ :key="grandChild.fullKey"
68
+ :model-value="/** @type import('../types.js').VjsfNode */(grandChild)"
69
+ :stateful-layout="statefulLayout"
70
+ />
71
+ </v-row>
72
+ </v-container>
73
+ </v-stepper-window-item>
74
+ </v-stepper-window>
75
+ <v-stepper-actions>
76
+ <template #prev>
77
+ <v-btn
78
+ v-if="step > 0"
79
+ variant="text"
80
+ @click="step--"
81
+ >
82
+ Back
83
+ </v-btn>
84
+ </template>
85
+ <template #next>
86
+ <v-spacer />
87
+ <v-btn
88
+ v-if="step < modelValue.children.length - 1"
89
+ variant="flat"
90
+ color="primary"
91
+ @click="goNext"
92
+ >
93
+ Next
94
+ </v-btn>
95
+ </template>
96
+ </v-stepper-actions>
97
+ </v-stepper>
98
+ </template>
@@ -18,7 +18,7 @@ export default defineComponent({
18
18
  }
19
19
  },
20
20
  setup (props) {
21
- const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout))
21
+ const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout, ['placeholder']))
22
22
  const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
23
23
 
24
24
  // @ts-ignore
@@ -18,7 +18,7 @@ export default defineComponent({
18
18
  }
19
19
  },
20
20
  setup (props) {
21
- const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout))
21
+ const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout, ['placeholder']))
22
22
  const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
23
23
 
24
24
  // @ts-ignore
@@ -21,5 +21,26 @@ export const defaultOptions = {
21
21
  checkboxPropsReadOnly: {},
22
22
  switchProps: { hideDetails: 'auto' },
23
23
  switchPropsReadOnly: {},
24
- errorAlertProps: { type: 'error', variant: 'tonal' }
24
+ errorAlertProps: { type: 'error', variant: 'tonal' },
25
+ easyMDEOptions: {}
26
+ }
27
+
28
+ /**
29
+ *
30
+ * @param {Partial<import("./types.js").VjsfOptions>} options
31
+ * @param {any} form
32
+ * @param {number} width
33
+ * @param {import("vue").Slots} slots
34
+ * @returns
35
+ */
36
+ export const getFullOptions = (options, form, width, slots) => {
37
+ const fullOptions = {
38
+ ...defaultOptions,
39
+ readOnly: !!(form && (form.isDisabled.value || form.isReadonly.value)),
40
+ ...options,
41
+ context: options.context ? JSON.parse(JSON.stringify(options.context)) : {},
42
+ width: Math.round(width ?? 0),
43
+ vjsfSlots: { ...slots }
44
+ }
45
+ return /** @type import('./types.js').VjsfOptions */ (fullOptions)
25
46
  }
@@ -17,6 +17,7 @@ import {
17
17
  TextFieldNode,
18
18
  TextareaNode,
19
19
  VerticalTabsNode,
20
+ StepperNode,
20
21
  ComboboxNode,
21
22
  CompileOptions
22
23
  } from '@json-layout/core'
@@ -39,6 +40,7 @@ export type VjsfOptions = StatefulLayoutOptions & CompileOptions & {
39
40
  switchPropsReadOnly: Record<string, unknown>,
40
41
  errorAlertProps: Record<string, unknown>,
41
42
  vjsfSlots: Record<string, () => unknown>,
43
+ easyMDEOptions: Record<string, unknown>,
42
44
  }
43
45
 
44
46
  export type PartialVjsfOptions = Partial<Omit<VjsfOptions, 'width'>>
@@ -60,4 +62,6 @@ export type VjsfSwitchNode = Omit<SwitchNode, 'options'> & {options: VjsfOptions
60
62
  export type VjsfTextFieldNode = Omit<TextFieldNode, 'options'> & {options: VjsfOptions}
61
63
  export type VjsfTextareaNode = Omit<TextareaNode, 'options'> & {options: VjsfOptions}
62
64
  export type VjsfVerticalTabsNode = Omit<VerticalTabsNode, 'options'> & {options: VjsfOptions}
65
+ export type VjsfStepperNode = Omit<StepperNode, 'options'> & {options: VjsfOptions}
66
+
63
67
  export type VjsfComboboxNode = Omit<ComboboxNode, 'options'> & {options: VjsfOptions}
@@ -1,8 +1,10 @@
1
1
  <script setup>
2
- import { ref, shallowRef, computed, getCurrentInstance, watch, useSlots, inject, toRaw } from 'vue'
3
- import { useElementSize } from '@vueuse/core'
4
- import { StatefulLayout, compile } from '@json-layout/core'
2
+ import { computed, getCurrentInstance } from 'vue'
3
+
4
+ import { compile } from '@json-layout/core'
5
5
  import Tree from './tree.vue'
6
+ import { useVjsf, emits } from '../composables/use-vjsf.js'
7
+ import '../styles/vjsf.css'
6
8
 
7
9
  import NodeSection from './nodes/section.vue'
8
10
  import NodeTextField from './nodes/text-field.vue'
@@ -22,8 +24,9 @@ import NodeVerticalTabs from './nodes/vertical-tabs.vue'
22
24
  import NodeCombobox from './nodes/combobox.vue'
23
25
  import NodeNumberCombobox from './nodes/number-combobox.vue'
24
26
  import NodeExpansionPanels from './nodes/expansion-panels.vue'
27
+ import NodeStepper from './nodes/stepper.vue'
25
28
  import NodeList from './nodes/list.vue'
26
- import { defaultOptions } from './options.js'
29
+ import NodeMarkdown from './nodes/markdown.vue'
27
30
 
28
31
  const comps = {
29
32
  section: NodeSection,
@@ -42,9 +45,11 @@ const comps = {
42
45
  tabs: NodeTabs,
43
46
  'vertical-tabs': NodeVerticalTabs,
44
47
  'expansion-panels': NodeExpansionPanels,
48
+ stepper: NodeStepper,
45
49
  list: NodeList,
46
50
  combobox: NodeCombobox,
47
- 'number-combobox': NodeNumberCombobox
51
+ 'number-combobox': NodeNumberCombobox,
52
+ markdown: NodeMarkdown
48
53
  }
49
54
 
50
55
  const instance = getCurrentInstance()
@@ -65,7 +70,7 @@ const props = defineProps({
65
70
  default: null
66
71
  },
67
72
  modelValue: {
68
- type: [Object, String, Number, Boolean],
73
+ type: null,
69
74
  default: null
70
75
  },
71
76
  options: {
@@ -75,95 +80,16 @@ const props = defineProps({
75
80
  }
76
81
  })
77
82
 
78
- // const emit = defineEmits(['update:modelValue', 'update:state'])
79
- const emit = defineEmits({
80
- /**
81
- * @arg {any} data
82
- */
83
- 'update:modelValue': (data) => true,
84
- /**
85
- * @arg {StatefulLayout} state
86
- */
87
- 'update:state': (state) => true
88
- })
89
-
90
- /** @type import('vue').ShallowRef<StatefulLayout | null> */
91
- const statefulLayout = shallowRef(null)
92
- /** @type import('vue').ShallowRef<import('@json-layout/core').StateTree | null> */
93
- const stateTree = shallowRef(null)
94
-
95
- // cf https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/composables/form.ts
96
- const form = inject(Symbol.for('vuetify:form'))
97
- if (form) {
98
- form.register({
99
- id: 'vjsf', // TODO: a unique random id ?
100
- validate: () => statefulLayout.value?.validate(),
101
- reset: () => statefulLayout.value?.resetValidation(), // TODO: also empty the data ?
102
- resetValidation: () => statefulLayout.value?.resetValidation()
103
- })
104
- }
105
-
106
- const el = ref(null)
107
- const { width } = useElementSize(el)
108
-
109
- const slots = useSlots()
110
-
111
- const fullOptions = computed(() => {
112
- const options = {
113
- ...defaultOptions,
114
- readOnly: !!(form && (form.isDisabled.value || form.isReadonly.value)),
115
- ...props.options,
116
- context: props.options.context ? JSON.parse(JSON.stringify(props.options.context)) : {},
117
- width: Math.round(width.value ?? 0),
118
- vjsfSlots: { ...slots }
119
- }
120
- return /** @type import('./types.js').VjsfOptions */ (options)
121
- })
122
-
123
- const compiledLayout = computed(() => {
124
- if (props.precompiledLayout) return props.precompiledLayout
125
- return compile(props.schema, fullOptions.value)
126
- })
127
-
128
- const onStatefulLayoutUpdate = () => {
129
- if (!statefulLayout.value) return
130
- stateTree.value = statefulLayout.value.stateTree
131
- emit('update:modelValue', statefulLayout.value.data)
132
- emit('update:state', statefulLayout.value)
133
- if (form) {
134
- // cf https://vuetifyjs.com/en/components/forms/#validation-state
135
- if (statefulLayout.value.valid) form.update('vjsf', true, [])
136
- else if (statefulLayout.value.hasHiddenError) form.update('vjsf', null, [])
137
- else form.update('vjsf', false, [])
138
- }
139
- }
140
-
141
- const initStatefulLayout = () => {
142
- if (!width.value) return
143
- const _statefulLayout = new StatefulLayout(toRaw(compiledLayout.value), toRaw(compiledLayout.value.skeletonTree), toRaw(fullOptions.value), toRaw(props.modelValue))
144
- statefulLayout.value = _statefulLayout
145
- onStatefulLayoutUpdate()
146
- _statefulLayout.events.on('update', () => {
147
- onStatefulLayoutUpdate()
148
- })
149
- emit('update:state', _statefulLayout)
150
- }
151
-
152
- watch(fullOptions, (newOptions) => {
153
- if (statefulLayout.value) {
154
- statefulLayout.value.options = newOptions
155
- } else {
156
- initStatefulLayout()
157
- }
158
- })
159
-
160
- // case where data is updated from outside
161
- watch(() => props.modelValue, (newData) => {
162
- if (statefulLayout.value && statefulLayout.value.data !== newData) statefulLayout.value.data = newData
163
- })
83
+ const emit = defineEmits(emits)
164
84
 
165
- // case where schema is updated from outside
166
- watch(compiledLayout, (newCompiledLayout) => initStatefulLayout())
85
+ const { el, statefulLayout, stateTree } = useVjsf(
86
+ computed(() => props.schema),
87
+ computed(() => props.modelValue),
88
+ computed(() => props.options),
89
+ emit,
90
+ compile,
91
+ computed(() => props.precompiledLayout)
92
+ )
167
93
 
168
94
  </script>
169
95
 
@@ -182,14 +108,5 @@ watch(compiledLayout, (newCompiledLayout) => initStatefulLayout())
182
108
  </template>
183
109
 
184
110
  <style lang="css">
185
- /* override vuetify styles to manage readOnly fields more usable than the default disabled fields */
186
- .vjsf-input--readonly.v-input--disabled.v-text-field .v-field--disabled input {
187
- pointer-events: auto;
188
- }
189
- .vjsf-input--readonly.v-input--disabled .v-field--disabled,
190
- .vjsf-input--readonly.v-input--disabled .v-input__details,
191
- .vjsf-input--readonly.v-input--disabled .v-input__append,
192
- .vjsf-input--readonly.v-input--disabled .v-input__prepend {
193
- opacity: inherit;
194
- }
111
+ /* nothing here, use ../styles/vjsf.css */
195
112
  </style>
@@ -0,0 +1,54 @@
1
+ import { shallowRef, ref, computed } from 'vue'
2
+ import { moveArrayItem } from '../utils/arrays.js'
3
+
4
+ /**
5
+ * @template T
6
+ * @param {T[]} array
7
+ * @param {() => void} callback
8
+ * @returns
9
+ */
10
+ export default function useDnd (array, callback) {
11
+ const activeDnd = computed(() => {
12
+ // cf https://ultimatecourses.com/blog/feature-detect-javascript-drag-drop-api
13
+ if (!('draggable' in document.createElement('div'))) return false
14
+ // cf https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
15
+ if (window.matchMedia('(pointer: coarse)').matches) return false
16
+ return true
17
+ })
18
+
19
+ const sortableArray = shallowRef(array)
20
+
21
+ const draggable = ref(-1)
22
+ const dragging = ref(-1)
23
+
24
+ const itemBind = (/** @type {number} */itemIndex) => ({
25
+ onDragstart: () => {
26
+ dragging.value = itemIndex
27
+ },
28
+ onDragover: () => {
29
+ sortableArray.value = moveArrayItem(sortableArray.value, dragging.value, itemIndex)
30
+ dragging.value = itemIndex
31
+ },
32
+ onDragend: () => {
33
+ dragging.value = -1
34
+ callback()
35
+ }
36
+ })
37
+
38
+ const handleBind = (/** @type {number} */itemIndex) => ({
39
+ onMouseover () {
40
+ draggable.value = itemIndex
41
+ },
42
+ onMouseout () {
43
+ draggable.value = -1
44
+ }
45
+ })
46
+
47
+ return {
48
+ activeDnd,
49
+ sortableArray,
50
+ draggable,
51
+ itemBind,
52
+ handleBind
53
+ }
54
+ }