@koumoul/vjsf 3.0.0-alpha.1 → 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 (38) hide show
  1. package/package.json +8 -8
  2. package/src/compile/index.js +1 -1
  3. package/src/compile/v-jsf-compiled.vue.ejs +14 -52
  4. package/src/components/nodes/combobox.vue +1 -1
  5. package/src/components/nodes/list.vue +170 -86
  6. package/src/components/nodes/markdown.vue +209 -5
  7. package/src/components/nodes/number-field.vue +1 -1
  8. package/src/components/nodes/stepper.vue +98 -0
  9. package/src/components/nodes/text-field.vue +1 -1
  10. package/src/components/nodes/textarea.vue +1 -1
  11. package/src/components/options.js +22 -1
  12. package/src/components/types.ts +4 -0
  13. package/src/components/vjsf.vue +21 -104
  14. package/src/composables/use-dnd.js +54 -0
  15. package/src/composables/use-vjsf.js +115 -0
  16. package/src/styles/vjsf.css +10 -0
  17. package/src/utils/arrays.js +15 -0
  18. package/src/utils/props.js +16 -5
  19. package/types/components/fragments/select-item.vue.d.ts +2 -2
  20. package/types/components/fragments/select-selection.vue.d.ts +2 -2
  21. package/types/components/nodes/markdown.vue.d.ts.map +1 -1
  22. package/types/components/nodes/stepper.vue.d.ts +10 -0
  23. package/types/components/nodes/stepper.vue.d.ts.map +1 -0
  24. package/types/components/options.d.ts +1 -0
  25. package/types/components/options.d.ts.map +1 -1
  26. package/types/components/tree.vue.d.ts +2 -2
  27. package/types/components/types.d.ts +5 -1
  28. package/types/components/types.d.ts.map +1 -1
  29. package/types/components/vjsf.vue.d.ts +5 -6
  30. package/types/components/vjsf.vue.d.ts.map +1 -1
  31. package/types/composables/use-dnd.d.ts +21 -0
  32. package/types/composables/use-dnd.d.ts.map +1 -0
  33. package/types/composables/use-vjsf.d.ts +17 -0
  34. package/types/composables/use-vjsf.d.ts.map +1 -0
  35. package/types/utils/arrays.d.ts +9 -0
  36. package/types/utils/arrays.d.ts.map +1 -0
  37. package/types/utils/props.d.ts +4 -2
  38. 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.1",
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",
@@ -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.3.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
  }
@@ -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
+ 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,13 @@
1
1
  <script setup>
2
+ import { watch, computed, ref } from 'vue'
2
3
  import { VList, VListItem, VListItemAction, VBtn, VMenu, VIcon } from 'vuetify/components'
3
4
  import { isSection } from '@json-layout/core'
4
5
  import Node from '../node.vue'
5
6
  import clone from '../../utils/clone.js'
7
+ import { moveArrayItem } from '../../utils/arrays.js'
8
+ import useDnd from '../../composables/use-dnd.js'
6
9
 
7
- defineProps({
10
+ const props = defineProps({
8
11
  modelValue: {
9
12
  /** @type import('vue').PropType<import('../types.js').VjsfListNode> */
10
13
  type: Object,
@@ -17,96 +20,177 @@ defineProps({
17
20
  }
18
21
  })
19
22
 
23
+ /* use composable for drag and drop */
24
+ const { activeDnd, sortableArray, draggable, itemBind, handleBind } = useDnd(props.modelValue.children, () => {
25
+ props.statefulLayout.input(props.modelValue, sortableArray.value.map((child) => child.data))
26
+ })
27
+ watch(() => props.modelValue.children, (array) => { sortableArray.value = array })
28
+
29
+ /* manage hovered and edited items */
30
+ const editedItem = computed(() => {
31
+ return props.statefulLayout.activeItems[props.modelValue.fullKey]
32
+ })
33
+ const hoveredItem = ref(1)
34
+ const activeItem = computed(() => {
35
+ if (
36
+ props.modelValue.layout.listActions.includes('edit') &&
37
+ props.modelValue.layout.listEditMode === 'inline-single' &&
38
+ editedItem.value !== undefined
39
+ ) {
40
+ return editedItem.value
41
+ }
42
+ return hoveredItem.value
43
+ })
44
+
45
+ const buttonDensity = computed(() => {
46
+ if (props.modelValue.options.density === 'default') return 'comfortable'
47
+ return props.modelValue.options.density
48
+ })
49
+
20
50
  </script>
21
51
 
22
52
  <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"
53
+ <v-sheet :elevation="1">
54
+ <v-list :density="modelValue.options.density">
55
+ <v-list-subheader v-if="modelValue.layout.title">
56
+ {{ modelValue.layout.title }}
57
+ </v-list-subheader>
58
+ <template
59
+ v-for="(child, childIndex) of sortableArray"
60
+ :key="props.modelValue.children.findIndex(c => c === child)"
61
+ >
62
+ <v-list-item
63
+ v-bind="itemBind(childIndex)"
64
+ :density="modelValue.options.density"
65
+ :draggable="draggable === childIndex"
66
+ :variant="editedItem === childIndex ? 'outlined' : 'flat'"
67
+ class="pa-1 vjsf-list-item"
68
+ @mouseenter="hoveredItem = childIndex"
69
+ @mouseleave="hoveredItem = -1"
66
70
  >
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)])"
71
+ <v-row class="ma-0">
72
+ <node
73
+ v-for="grandChild of isSection(child) ? child.children : [child]"
74
+ :key="grandChild.fullKey"
75
+ :model-value="/** @type import('../types.js').VjsfNode */(grandChild)"
76
+ :stateful-layout="statefulLayout"
77
+ />
78
+ </v-row>
79
+ <template
80
+ v-if="activeItem === childIndex"
81
+ #append
82
+ >
83
+ <div>
84
+ <v-list-item-action
85
+ v-if="modelValue.layout.listActions.includes('edit') && modelValue.layout.listEditMode === 'inline-single'"
81
86
  >
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)])"
87
+ <v-btn
88
+ v-if="editedItem !== childIndex"
89
+ :title="modelValue.messages.edit"
90
+ icon="mdi-pencil"
91
+ variant="text"
92
+ color="primary"
93
+ :density="buttonDensity"
94
+ @click="statefulLayout.activateItem(modelValue, childIndex)"
95
+ />
96
+ <v-btn
97
+ v-else
98
+ :title="modelValue.messages.edit"
99
+ icon="mdi-pencil"
100
+ variant="flat"
101
+ color="primary"
102
+ :density="buttonDensity"
103
+ @click="statefulLayout.deactivateItem(modelValue)"
104
+ />
105
+ </v-list-item-action>
106
+ <v-list-item-action
107
+ v-if="editedItem === undefined && modelValue.layout.listActions.includes('sort') && activeDnd"
108
+ >
109
+ <v-btn
110
+ :title="modelValue.messages.sort"
111
+ icon="mdi-arrow-up-down"
112
+ variant="plain"
113
+ :density="buttonDensity"
114
+ v-bind="handleBind(childIndex)"
115
+ />
116
+ </v-list-item-action>
117
+ <v-list-item-action
118
+ v-if="editedItem === undefined && (modelValue.layout.listActions.includes('delete') || modelValue.layout.listActions.includes('duplicate') || modelValue.layout.listActions.includes('sort'))"
91
119
  >
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>
120
+ <v-menu location="bottom end">
121
+ <template #activator="{props: activatorProps}">
122
+ <v-btn
123
+ v-bind="activatorProps"
124
+ icon="mdi-dots-vertical"
125
+ variant="plain"
126
+ slim
127
+ :density="buttonDensity"
128
+ />
129
+ </template>
130
+ <v-list :density="modelValue.options.density">
131
+ <v-list-item
132
+ v-if="modelValue.layout.listActions.includes('delete')"
133
+ base-color="warning"
134
+ @click="statefulLayout.input(modelValue, [...modelValue.data.slice(0, childIndex), ...modelValue.data.slice(childIndex + 1)])"
135
+ >
136
+ <template #prepend>
137
+ <v-icon icon="mdi-delete" />
138
+ </template>
139
+ {{ modelValue.messages.delete }}
140
+ </v-list-item>
141
+ <v-list-item
142
+ v-if="modelValue.layout.listActions.includes('duplicate')"
143
+ @click="statefulLayout.input(modelValue, [...modelValue.data.slice(0, childIndex), clone(child.data), ...modelValue.data.slice(childIndex)])"
144
+ >
145
+ <template #prepend>
146
+ <v-icon icon="mdi-content-duplicate" />
147
+ </template>
148
+ {{ modelValue.messages.duplicate }}
149
+ </v-list-item>
150
+ <v-list-item
151
+ v-if="modelValue.layout.listActions.includes('sort')"
152
+ @click="statefulLayout.input(modelValue, moveArrayItem(modelValue.data, childIndex, childIndex - 1))"
153
+ >
154
+ <template #prepend>
155
+ <v-icon icon="mdi-arrow-up" />
156
+ </template>
157
+ {{ modelValue.messages.up }}
158
+ </v-list-item>
159
+ <v-list-item
160
+ v-if="modelValue.layout.listActions.includes('sort')"
161
+ @click="statefulLayout.input(modelValue, moveArrayItem(modelValue.data, childIndex, childIndex + 1))"
162
+ >
163
+ <template #prepend>
164
+ <v-icon icon="mdi-arrow-down" />
165
+ </template>
166
+ {{ modelValue.messages.down }}
167
+ </v-list-item>
168
+ </v-list>
169
+ </v-menu>
170
+ </v-list-item-action>
171
+ </div>
172
+ </template>
173
+ </v-list-item>
174
+ <v-divider v-if="childIndex < modelValue.children.length - 1" />
100
175
  </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>
176
+ <v-list-item v-if="!modelValue.options.readOnly && modelValue.layout.listActions.includes('add')">
177
+ <v-spacer />
178
+ <v-btn
179
+ color="primary"
180
+ :density="modelValue.options.density"
181
+ @click="statefulLayout.input(modelValue, (modelValue.data ?? []).concat([undefined]))"
182
+ >
183
+ {{ modelValue.messages.addItem }}
184
+ </v-btn>
185
+ <v-spacer />
186
+ </v-list-item>
187
+ </v-list>
188
+ </v-sheet>
112
189
  </template>
190
+
191
+ <style>
192
+ .vjsf-list-item .v-list-item__append {
193
+ height: 100%;
194
+ align-items: start;
195
+ }
196
+ </style>
@@ -1,8 +1,10 @@
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'
7
+ // import EasyMDE from 'easymde'
6
8
 
7
9
  export default defineComponent({
8
10
  props: {
@@ -18,12 +20,214 @@ export default defineComponent({
18
20
  }
19
21
  },
20
22
  setup (props) {
23
+ /** @type {import('vue').Ref<null | HTMLElement>} */
24
+ const element = ref(null)
25
+
21
26
  const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout))
22
- const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
27
+ const fieldSlots = computed(() => {
28
+ const fieldSlots = getCompSlots(props.modelValue, props.statefulLayout)
29
+ fieldSlots.default = () => [
30
+ h('div', { style: 'width:100%' }, [
31
+ h(VLabel, { text: fieldProps.value.label }),
32
+ h('textarea', { ref: element })
33
+ ])
34
+ ]
35
+ return fieldSlots
36
+ })
37
+
38
+ /** @type {EasyMDE | null} */
39
+ let easymde = null
40
+
41
+ const initEasyMDE = async () => {
42
+ if (!element.value) throw new Error('component was not mounted for markdown editor')
43
+
44
+ // @ts-ignore
45
+ await import('easymde/dist/easymde.min.css')
46
+ const EasyMDE = (await import('easymde')).default
47
+
48
+ const messages = props.modelValue.messages
49
+
50
+ const config = {
51
+ element: element.value,
52
+ initialValue: props.modelValue.data ?? '',
53
+ renderingConfig: {},
54
+ status: false,
55
+ autoDownloadFontAwesome: false,
56
+ spellChecker: false,
57
+ minHeight: '300px',
58
+ insertTexts: {
59
+ link: [messages.mdeLink1, messages.mdeLink2],
60
+ image: [messages.mdeImg1, messages.mdeImg2],
61
+ table: [messages.mdeTable1, messages.mdeTable2],
62
+ horizontalRule: ['', '\n\n-----\n\n']
63
+ },
64
+ // cf https://github.com/Ionaru/easy-markdown-editor/blob/master/src/js/easymde.js#L1380
65
+ toolbar: [{
66
+ name: 'bold',
67
+ action: EasyMDE.toggleBold,
68
+ className: 'mdi mdi-format-bold',
69
+ title: messages.bold
70
+ }, {
71
+ name: 'italic',
72
+ action: EasyMDE.toggleItalic,
73
+ className: 'mdi mdi-format-italic',
74
+ title: messages.italic
75
+ }, {
76
+ name: 'heading',
77
+ action: EasyMDE.toggleHeadingSmaller,
78
+ className: 'mdi mdi-format-title',
79
+ title: messages.heading
80
+ }, /* {
81
+ name: 'heading-1',
82
+ action: EasyMDE.toggleHeading1,
83
+ className: 'mdi mdi-format-title',
84
+ title: 'Titre 1'
85
+ }, {
86
+ name: 'heading-2',
87
+ action: EasyMDE.toggleHeading2,
88
+ className: 'mdi mdi-format-title',
89
+ title: 'Titre 2'
90
+ }, {
91
+ name: 'heading-3',
92
+ action: EasyMDE.toggleHeading3,
93
+ className: 'mdi mdi-format-title',
94
+ title: 'Titre 3'
95
+ }, */
96
+ '|',
97
+ {
98
+ name: 'quote',
99
+ action: EasyMDE.toggleBlockquote,
100
+ className: 'mdi mdi-format-quote-open',
101
+ title: messages.quote
102
+ },
103
+ {
104
+ name: 'unordered-list',
105
+ action: EasyMDE.toggleUnorderedList,
106
+ className: 'mdi mdi-format-list-bulleted',
107
+ title: messages.unorderedList
108
+ },
109
+ {
110
+ name: 'ordered-list',
111
+ action: EasyMDE.toggleOrderedList,
112
+ className: 'mdi mdi-format-list-numbered',
113
+ title: messages.orderedList
114
+ },
115
+ '|',
116
+ {
117
+ name: 'link',
118
+ action: EasyMDE.drawLink,
119
+ className: 'mdi mdi-link',
120
+ title: messages.createLink
121
+ },
122
+ {
123
+ name: 'image',
124
+ action: EasyMDE.drawImage,
125
+ className: 'mdi mdi-image',
126
+ title: messages.insertImage
127
+ },
128
+ {
129
+ name: 'table',
130
+ action: EasyMDE.drawTable,
131
+ className: 'mdi mdi-table',
132
+ title: messages.createTable
133
+ },
134
+ '|',
135
+ {
136
+ name: 'preview',
137
+ action: EasyMDE.togglePreview,
138
+ className: 'mdi mdi-eye text-accent',
139
+ title: messages.preview,
140
+ noDisable: true
141
+ },
142
+ '|',
143
+ {
144
+ name: 'undo',
145
+ action: EasyMDE.undo,
146
+ className: 'mdi mdi-undo',
147
+ title: messages.undo,
148
+ noDisable: true
149
+ },
150
+ {
151
+ name: 'redo',
152
+ action: EasyMDE.redo,
153
+ className: 'mdi mdi-redo',
154
+ title: messages.redo,
155
+ noDisable: true
156
+ },
157
+ '|',
158
+ {
159
+ name: 'guide',
160
+ action: 'https://simplemde.com/markdown-guide',
161
+ className: 'mdi mdi-help-circle text-success',
162
+ title: messages.mdeGuide,
163
+ noDisable: true
164
+ }
165
+ ],
166
+ ...props.modelValue.options.easyMDEOptions
167
+ }
168
+
169
+ if (easymde) easymde.toTextArea()
170
+ // @ts-ignore
171
+ easymde = new EasyMDE(config)
172
+
173
+ let changed = false
174
+ easymde.codemirror.on('change', () => {
175
+ changed = true
176
+ if (easymde) props.statefulLayout.input(props.modelValue, easymde.value())
177
+ })
178
+ /** @type {ReturnType<typeof setTimeout> | null} */
179
+ let blurTimeout = null
180
+ easymde.codemirror.on('blur', () => {
181
+ // timeout to prevent triggering save when clicking on a menu button
182
+ blurTimeout = setTimeout(() => {
183
+ if (changed) props.statefulLayout.blur(props.modelValue)
184
+ changed = false
185
+ }, 500)
186
+ })
187
+ easymde.codemirror.on('focus', () => {
188
+ if (blurTimeout) clearTimeout(blurTimeout)
189
+ })
190
+ }
191
+
192
+ onMounted(initEasyMDE)
193
+
194
+ onUnmounted(() => {
195
+ if (easymde) easymde.toTextArea()
196
+ })
197
+
198
+ // update data from outside
199
+ watch(() => props.modelValue, () => {
200
+ if (easymde && (easymde.value() !== props.modelValue.data ?? '')) {
201
+ easymde.value(props.modelValue.data ?? '')
202
+ }
203
+ })
23
204
 
24
- // @ts-ignore
25
- return () => h(VTextarea, fieldProps.value, fieldSlots.value)
205
+ // update easymde config from outside
206
+ watch(() => [props.modelValue.messages, props.modelValue.options.easyMDEOptions], () => {
207
+ initEasyMDE()
208
+ })
209
+
210
+ return () => h(VInput, fieldProps.value, fieldSlots.value)
26
211
  }
27
212
  })
28
213
 
29
214
  </script>
215
+
216
+ <style>
217
+ .vjsf-node-markdown .v-input--density-compact .editor-toolbar {
218
+ padding: 0;
219
+ }
220
+ .vjsf-node-markdown .v-input--density-comfortable .editor-toolbar {
221
+ padding: 4px;
222
+ }
223
+
224
+ .vjsf-node-markdown .v-input--density-compact .CodeMirror-wrap {
225
+ padding-top: 2px;
226
+ padding-bottom: 2px;
227
+ }
228
+ .vjsf-node-markdown .v-input--density-comfortable .CodeMirror-wrap {
229
+ padding-top: 6px;
230
+ padding-bottom: 6px;
231
+ }
232
+
233
+ </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