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

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koumoul/vjsf",
3
- "version": "3.0.0-alpha.3",
3
+ "version": "3.0.0-alpha.4",
4
4
  "description": "Generate forms for the vuetify UI library (vuejs) based on annotated JSON schemas.",
5
5
  "scripts": {
6
6
  "test": "vitest",
@@ -52,7 +52,7 @@
52
52
  "vuetify": "^3.4.2"
53
53
  },
54
54
  "dependencies": {
55
- "@json-layout/core": "0.4.0",
55
+ "@json-layout/core": "0.5.0",
56
56
  "@vueuse/core": "^10.5.0",
57
57
  "debug": "^4.3.4",
58
58
  "easymde": "^2.18.0",
@@ -20,7 +20,7 @@
20
20
  </template>
21
21
 
22
22
  <script setup>
23
- import { VAlert, VBtn } from 'vuetify/components'
23
+ import { VAlert, VBtn, VSlideXReverseTransition } from 'vuetify/components'
24
24
  import { ref } from 'vue'
25
25
 
26
26
  defineProps({
@@ -1,9 +1,11 @@
1
1
  <script setup>
2
+ import { computed } from 'vue'
3
+ import { useTheme } from 'vuetify'
2
4
  import { VCol } from 'vuetify/components'
3
5
  import NodeSlot from './fragments/node-slot.vue'
4
6
  import HelpMessage from './fragments/help-message.vue'
5
7
 
6
- defineProps({
8
+ const props = defineProps({
7
9
  modelValue: {
8
10
  /** @type import('vue').PropType<import('./types.js').VjsfNode> */
9
11
  type: Object,
@@ -22,12 +24,23 @@ const beforeAfterClasses = {
22
24
  comfortable: 'my-2',
23
25
  default: 'my-3'
24
26
  }
27
+
28
+ const theme = useTheme()
29
+
30
+ const nodeClasses = computed(() => {
31
+ let classes = `vjsf-node vjsf-node-${props.modelValue.layout.comp} vjsf-density-${props.modelValue.options.density}`
32
+ if (props.modelValue.options.readOnly) classes += ' vjsf-readonly'
33
+ if (props.modelValue.options.summary) classes += ' vjsf-summary'
34
+ if (theme.current.value.dark) classes += ' vjsf-dark'
35
+ return classes
36
+ })
37
+
25
38
  </script>
26
39
 
27
40
  <template>
28
41
  <v-col
29
42
  :cols="modelValue.cols"
30
- :class="`vjsf-node vjsf-node-${modelValue.layout.comp}`"
43
+ :class="nodeClasses"
31
44
  >
32
45
  <node-slot
33
46
  v-if="modelValue.layout.slots?.before"
@@ -87,9 +87,3 @@ export default defineComponent({
87
87
  })
88
88
 
89
89
  </script>
90
-
91
- <template>
92
- <v-select
93
- v-bind="fieldProps"
94
- />
95
- </template>
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { VExpansionPanels, VExpansionPanel, VExpansionPanelTitle, VContainer } from 'vuetify/components'
2
+ import { VExpansionPanels, VExpansionPanel, VExpansionPanelTitle, VContainer, VRow, VIcon } from 'vuetify/components'
3
3
  import { isSection } from '@json-layout/core'
4
4
  import Node from '../node.vue'
5
5
  import SectionHeader from '../fragments/section-header.vue'
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { watch, computed, ref } from 'vue'
3
- import { VList, VListItem, VListItemAction, VBtn, VMenu, VIcon } from 'vuetify/components'
3
+ import { VList, VListItem, VListItemAction, VBtn, VMenu, VIcon, VSheet, VSpacer, VDivider, VRow, VListSubheader } from 'vuetify/components'
4
4
  import { isSection, clone } from '@json-layout/core'
5
5
  import Node from '../node.vue'
6
6
  import { moveArrayItem } from '../../utils/arrays.js'
@@ -20,7 +20,7 @@ const props = defineProps({
20
20
  })
21
21
 
22
22
  /* use composable for drag and drop */
23
- const { activeDnd, sortableArray, draggable, itemBind, handleBind } = useDnd(props.modelValue.children, () => {
23
+ const { activeDnd, sortableArray, draggable, hovered, dragging, itemBind, handleBind } = useDnd(props.modelValue.children, () => {
24
24
  props.statefulLayout.input(props.modelValue, sortableArray.value.map((child) => child.data))
25
25
  })
26
26
  watch(() => props.modelValue.children, (array) => { sortableArray.value = array })
@@ -29,7 +29,7 @@ watch(() => props.modelValue.children, (array) => { sortableArray.value = array
29
29
  const editedItem = computed(() => {
30
30
  return props.statefulLayout.activeItems[props.modelValue.fullKey]
31
31
  })
32
- const hoveredItem = ref(1)
32
+ const menuOpened = ref(-1)
33
33
  const activeItem = computed(() => {
34
34
  if (
35
35
  props.modelValue.layout.listActions.includes('edit') &&
@@ -38,7 +38,9 @@ const activeItem = computed(() => {
38
38
  ) {
39
39
  return editedItem.value
40
40
  }
41
- return hoveredItem.value
41
+ if (dragging.value !== -1) return -1
42
+ if (menuOpened.value !== -1) return menuOpened.value
43
+ return hovered.value
42
44
  })
43
45
 
44
46
  const buttonDensity = computed(() => {
@@ -64,8 +66,6 @@ const buttonDensity = computed(() => {
64
66
  :draggable="draggable === childIndex"
65
67
  :variant="editedItem === childIndex ? 'outlined' : 'flat'"
66
68
  class="pa-1 vjsf-list-item"
67
- @mouseenter="hoveredItem = childIndex"
68
- @mouseleave="hoveredItem = -1"
69
69
  >
70
70
  <v-row class="ma-0">
71
71
  <node
@@ -116,7 +116,10 @@ const buttonDensity = computed(() => {
116
116
  <v-list-item-action
117
117
  v-if="editedItem === undefined && (modelValue.layout.listActions.includes('delete') || modelValue.layout.listActions.includes('duplicate') || modelValue.layout.listActions.includes('sort'))"
118
118
  >
119
- <v-menu location="bottom end">
119
+ <v-menu
120
+ location="bottom end"
121
+ @update:model-value="value => {menuOpened = value ? childIndex : -1}"
122
+ >
120
123
  <template #activator="{props: activatorProps}">
121
124
  <v-btn
122
125
  v-bind="activatorProps"
@@ -1,6 +1,8 @@
1
1
  <script>
2
2
  import { defineComponent, h, computed, onMounted, ref, onUnmounted, watch } from 'vue'
3
+ import { useTheme } from 'vuetify'
3
4
  import { VInput, VLabel } from 'vuetify/components'
5
+ import { marked } from 'marked'
4
6
  import { getInputProps } from '../../utils/props.js'
5
7
  import { getCompSlots } from '../../utils/slots.js'
6
8
  import 'easymde/dist/easymde.min.css'
@@ -18,26 +20,42 @@ export default defineComponent({
18
20
  required: true
19
21
  }
20
22
  },
21
- setup (props) {
23
+ setup (props, { expose }) {
22
24
  /** @type {import('vue').Ref<null | HTMLElement>} */
23
25
  const element = ref(null)
24
26
 
27
+ const renderedValue = computed(() => {
28
+ return props.modelValue.data && marked.parse(props.modelValue.data)
29
+ })
30
+
25
31
  const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout))
26
32
  const fieldSlots = computed(() => {
27
33
  const fieldSlots = getCompSlots(props.modelValue, props.statefulLayout)
28
- fieldSlots.default = () => [
29
- h('div', { style: 'width:100%' }, [
34
+ fieldSlots.default = () => {
35
+ const children = [
30
36
  h(VLabel, { text: fieldProps.value.label }),
31
- h('textarea', { ref: element, style: 'display:none' })
32
- ])
33
- ]
37
+ h('textarea', { ref: element })
38
+ ]
39
+ if (props.modelValue.options.summary) {
40
+ children.push(h('div', { innerHTML: renderedValue.value }))
41
+ }
42
+ return h('div', { class: 'vjsf-node-markdown-content' }, children)
43
+ }
34
44
  return fieldSlots
35
45
  })
36
46
 
47
+ /** @type {ReturnType<typeof setTimeout> | null} */
48
+ let blurTimeout = null
49
+
37
50
  /** @type {EasyMDE | null} */
38
51
  let easymde = null
39
52
 
40
53
  const initEasyMDE = async () => {
54
+ if (easymde) {
55
+ easymde.toTextArea()
56
+ easymde = null
57
+ }
58
+ if (props.modelValue.options.readOnly) return
41
59
  if (!element.value) throw new Error('component was not mounted for markdown editor')
42
60
 
43
61
  const EasyMDE = (await import('easymde')).default
@@ -158,12 +176,9 @@ export default defineComponent({
158
176
  className: 'mdi mdi-help-circle text-success',
159
177
  title: messages.mdeGuide,
160
178
  noDisable: true
161
- }
162
- ],
179
+ }],
163
180
  ...props.modelValue.options.easyMDEOptions
164
181
  }
165
-
166
- if (easymde) easymde.toTextArea()
167
182
  // @ts-ignore
168
183
  easymde = new EasyMDE(config)
169
184
 
@@ -172,9 +187,8 @@ export default defineComponent({
172
187
  changed = true
173
188
  if (easymde) props.statefulLayout.input(props.modelValue, easymde.value())
174
189
  })
175
- /** @type {ReturnType<typeof setTimeout> | null} */
176
- let blurTimeout = null
177
190
  easymde.codemirror.on('blur', () => {
191
+ console.log('onblur')
178
192
  // timeout to prevent triggering save when clicking on a menu button
179
193
  blurTimeout = setTimeout(() => {
180
194
  if (changed) props.statefulLayout.blur(props.modelValue)
@@ -204,32 +218,103 @@ export default defineComponent({
204
218
  })
205
219
 
206
220
  // 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]) {
221
+ watch(() => [props.modelValue.options.readOnly, props.modelValue.messages, props.modelValue.options.easyMDEOptions], (newValues, oldValues) => {
222
+ if (newValues[0] !== oldValues[0] || newValues[1] !== oldValues[1] || newValues[2] !== oldValues[2]) {
209
223
  initEasyMDE()
210
224
  }
211
225
  })
212
226
 
213
227
  props.statefulLayout.events.on('autofocus', () => {
228
+ console.log('focus code mirror ?')
214
229
  if (props.modelValue.autofocus && easymde) {
215
230
  easymde.codemirror.focus()
216
231
  }
217
232
  })
218
233
 
219
- return () => h(VInput, fieldProps.value, fieldSlots.value)
234
+ const theme = useTheme()
235
+ const darkStyle = computed(() => getDarkStyle(theme))
236
+
237
+ return () => [
238
+ h('style', { innerHTML: darkStyle.value }),
239
+ h(VInput, fieldProps.value, fieldSlots.value)
240
+ ]
220
241
  }
221
242
  })
222
243
 
244
+ const getDarkStyle = (/** @type {import('vuetify').ThemeInstance} */theme) => {
245
+ // Inspired by https://github.com/Ionaru/easy-markdown-editor/issues/131#issuecomment-1738202589
246
+ return `
247
+ .vjsf-node-markdown.vjsf-dark .EasyMDEContainer .CodeMirror {
248
+ color: white;
249
+ border-color: ${theme.current.value.variables['border-color']};
250
+ background-color: ${theme.current.value.colors.surface};
251
+ }
252
+ .vjsf-node-markdown.vjsf-dark .EasyMDEContainer .cm-s-easymde .CodeMirror-cursor {
253
+ border-color: white;
254
+ }
255
+ .vjsf-node-markdown.vjsf-dark .CodeMirror-cursor {
256
+ border-left:1px solid white;
257
+ border-right:none;width:0;
258
+ }
259
+ .vjsf-node-markdown.vjsf-dark .EasyMDEContainer .editor-toolbar > * {
260
+ border-color: ${theme.current.value.colors.surface};
261
+ }
262
+ .vjsf-node-markdown.vjsf-dark .editor-toolbar {
263
+ border-top: 1px solid ${theme.current.value.variables['border-color']};
264
+ border-left: 1px solid ${theme.current.value.variables['border-color']};
265
+ border-right: 1px solid ${theme.current.value.variables['border-color']};
266
+ }
267
+ .vjsf-node-markdown.vjsf-dark .editor-toolbar i.separator {
268
+ border-left: 1px solid ${theme.current.value.variables['border-color']};
269
+ border-right: 1px solid ${theme.current.value.variables['border-color']};
270
+ }
271
+ .vjsf-node-markdown.vjsf-dark .EasyMDEContainer .editor-toolbar > .active, .editor-toolbar > button:hover, .editor-preview pre, .cm-s-easymde .cm-comment {
272
+ background-color: ${theme.current.value.colors.surface};
273
+ }
274
+ .vjsf-node-markdown.vjsf-dark .EasyMDEContainer .CodeMirror-fullscreen {
275
+ background: ${theme.current.value.colors.surface};
276
+ }
277
+ .vjsf-node-markdown.vjsf-dark .editor-toolbar.fullscreen {
278
+ background: ${theme.current.value.colors.surface};
279
+ }
280
+ .vjsf-node-markdown.vjsf-dark .editor-preview {
281
+ background: ${theme.current.value.colors.surface};
282
+ }
283
+ .vjsf-node-markdown.vjsf-dark .editor-preview-side {
284
+ border-color: ${theme.current.value.variables['border-color']};
285
+ }
286
+ .vjsf-node-markdown.vjsf-dark .CodeMirror-selected {
287
+ background: ${theme.current.value.colors.secondary};
288
+ }
289
+ .vjsf-node-markdown.vjsf-dark .CodeMirror-focused .CodeMirror-selected {
290
+ background: ${theme.current.value.colors.secondary};
291
+ }
292
+ .vjsf-node-markdown.vjsf-dark .CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection {
293
+ background:${theme.current.value.colors.secondary}
294
+ }
295
+ .vjsf-node-markdown.vjsf-dark .CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection {
296
+ background:${theme.current.value.colors.secondary}
297
+ }
298
+ .vjsf-node-markdown.vjsf-dark .EasyMDEContainer .CodeMirror-focused .CodeMirror-selected {
299
+ background: ${theme.current.value.colors.secondary}
300
+ }
301
+ `
302
+ }
303
+
223
304
  </script>
224
305
 
225
306
  <style>
307
+ .vjsf-node-markdown .vjsf-node-markdown-content {
308
+ width: 100%;
309
+ }
310
+
311
+ /* adjust to density */
226
312
  .vjsf-node-markdown .v-input--density-compact .editor-toolbar {
227
313
  padding: 0;
228
314
  }
229
315
  .vjsf-node-markdown .v-input--density-comfortable .editor-toolbar {
230
316
  padding: 4px;
231
317
  }
232
-
233
318
  .vjsf-node-markdown .v-input--density-compact .CodeMirror-wrap {
234
319
  padding-top: 2px;
235
320
  padding-bottom: 2px;
@@ -239,4 +324,18 @@ export default defineComponent({
239
324
  padding-bottom: 6px;
240
325
  }
241
326
 
327
+ /* adjust to readOnly/summary mode */
328
+ .vjsf-node-markdown.vjsf-readonly .EasyMDEContainer .CodeMirror {
329
+ border-width: 0;
330
+ padding: 0;
331
+ }
332
+ .vjsf-node-markdown.vjsf-summary .vjsf-node-markdown-content {
333
+ height: 96px;
334
+ overflow: hidden;
335
+ mask-image: linear-gradient(180deg, #000 66%, transparent 90%);
336
+ }
337
+ .vjsf-node-markdown.vjsf-readonly .vjsf-node-markdown-content textarea {
338
+ display: none;
339
+ }
340
+
242
341
  </style>
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { VSelect } from 'vuetify/components'
2
+ import { VSelect, VRow } from 'vuetify/components'
3
3
  import { shallowRef, watch } from 'vue'
4
4
  import { isSection } from '@json-layout/core'
5
5
  import Node from '../node.vue'
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { VRow } from 'vuetify/components'
2
3
  import Node from '../node.vue'
3
4
  import SectionHeader from '../fragments/section-header.vue'
4
5
 
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { ref, computed } from 'vue'
3
- import { VStepper, VStepperHeader, VStepperItem, VContainer } from 'vuetify/components'
3
+ import { VStepper, VStepperHeader, VStepperItem, VStepperWindow, VStepperWindowItem, VStepperActions, VContainer, VRow, VSpacer, VBtn, VDivider } from 'vuetify/components'
4
4
  import { isSection } from '@json-layout/core'
5
5
  import Node from '../node.vue'
6
6
  import SectionHeader from '../fragments/section-header.vue'
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { VTabs, VTab, VContainer } from 'vuetify/components'
2
+ import { VTabs, VTab, VContainer, VSheet, VWindow, VWindowItem, VRow, VIcon } from 'vuetify/components'
3
3
  import { ref } from 'vue'
4
4
  import { isSection } from '@json-layout/core'
5
5
  import Node from '../node.vue'
@@ -27,3 +27,9 @@ export default defineComponent({
27
27
  })
28
28
 
29
29
  </script>
30
+
31
+ <style>
32
+ .vjsf-node-text-field.vjsf-readonly.vjsf-summary input {
33
+ text-overflow: ellipsis;
34
+ }
35
+ </style>
@@ -1,5 +1,5 @@
1
1
  <script>
2
- import { defineComponent, h, computed } from 'vue'
2
+ import { defineComponent, h, computed, ref, watch } from 'vue'
3
3
  import { VTextarea } from 'vuetify/components'
4
4
  import { getInputProps } from '../../utils/props.js'
5
5
  import { getCompSlots } from '../../utils/slots.js'
@@ -18,12 +18,33 @@ export default defineComponent({
18
18
  }
19
19
  },
20
20
  setup (props) {
21
- const fieldProps = computed(() => getInputProps(props.modelValue, props.statefulLayout, ['placeholder']))
21
+ /** @type {import('vue').Ref<null | HTMLElement>} */
22
+ const textarea = ref(null)
23
+
24
+ const fieldProps = computed(() => {
25
+ const inputProps = getInputProps(props.modelValue, props.statefulLayout, ['placeholder'])
26
+ inputProps.ref = textarea
27
+ if (props.modelValue.options.readOnly && props.modelValue.options.summary) inputProps.rows = 3
28
+ return inputProps
29
+ })
22
30
  const fieldSlots = computed(() => getCompSlots(props.modelValue, props.statefulLayout))
23
31
 
32
+ watch(() => props.modelValue.options.readOnly, (readOnly) => {
33
+ if (readOnly && textarea.value) {
34
+ textarea.value.scrollTop = 0
35
+ }
36
+ })
37
+
24
38
  // @ts-ignore
25
39
  return () => h(VTextarea, fieldProps.value, fieldSlots.value)
26
40
  }
27
41
  })
28
42
 
29
43
  </script>
44
+
45
+ <style>
46
+ .vjsf-node-textarea.vjsf-readonly.vjsf-summary textarea {
47
+ overflow: hidden;
48
+ mask-image: linear-gradient(180deg, #000 66%, transparent 90%);
49
+ }
50
+ </style>
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { isSection } from '@json-layout/core'
3
- import { VTabs, VTab, VContainer } from 'vuetify/components'
3
+ import { VTabs, VTab, VContainer, VSheet, VWindow, VWindowItem, VRow, VIcon } from 'vuetify/components'
4
4
  import { ref } from 'vue'
5
5
  import Node from '../node.vue'
6
6
  import SectionHeader from '../fragments/section-header.vue'
@@ -18,10 +18,22 @@ export default function useDnd (array, callback) {
18
18
 
19
19
  const sortableArray = shallowRef(array)
20
20
 
21
+ const hovered = ref(-1)
21
22
  const draggable = ref(-1)
22
23
  const dragging = ref(-1)
23
24
 
25
+ hovered.value = 1
26
+
24
27
  const itemBind = (/** @type {number} */itemIndex) => ({
28
+ // hover the item
29
+ onMouseenter: () => {
30
+ hovered.value = itemIndex
31
+ },
32
+ onMouseleave: () => {
33
+ hovered.value = -1
34
+ },
35
+
36
+ // drag the item
25
37
  onDragstart: () => {
26
38
  dragging.value = itemIndex
27
39
  },
@@ -36,6 +48,7 @@ export default function useDnd (array, callback) {
36
48
  })
37
49
 
38
50
  const handleBind = (/** @type {number} */itemIndex) => ({
51
+ // hover the handle
39
52
  onMouseover () {
40
53
  draggable.value = itemIndex
41
54
  },
@@ -47,7 +60,9 @@ export default function useDnd (array, callback) {
47
60
  return {
48
61
  activeDnd,
49
62
  sortableArray,
63
+ hovered,
50
64
  draggable,
65
+ dragging,
51
66
  itemBind,
52
67
  handleBind
53
68
  }
@@ -84,6 +84,7 @@ export const useVjsf = (schema, modelValue, options, emit, compile, precompiledL
84
84
  })
85
85
  emit('update:state', _statefulLayout)
86
86
  _statefulLayout.events.on('autofocus', () => {
87
+ console.log('autofocus ?')
87
88
  if (!el.value) return
88
89
  // @ts-ignore
89
90
  const autofocusNodeElement = el.value.querySelector('.vjsf-input--autofocus')
@@ -96,7 +97,7 @@ export const useVjsf = (schema, modelValue, options, emit, compile, precompiledL
96
97
 
97
98
  watch(fullOptions, (newOptions) => {
98
99
  // in case of runtime compilation the watch on compiledLayout will be triggered
99
- if (!precompiledLayout) return
100
+ if (!precompiledLayout?.value) return
100
101
 
101
102
  if (statefulLayout.value) {
102
103
  statefulLayout.value.options = newOptions
@@ -113,7 +114,7 @@ export const useVjsf = (schema, modelValue, options, emit, compile, precompiledL
113
114
  // case where schema is updated from outside
114
115
  watch(compiledLayout, (newCompiledLayout) => {
115
116
  initStatefulLayout()
116
- }, { immediate: true })
117
+ })
117
118
 
118
119
  return { el, statefulLayout, stateTree }
119
120
  }