@ditojs/admin 2.4.0 → 2.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/admin",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "type": "module",
5
5
  "description": "Dito.js Admin is a schema based admin interface for Dito.js Server, featuring auto-generated views and forms and built with Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/master/packages/admin",
@@ -33,8 +33,8 @@
33
33
  "not ie_mob > 0"
34
34
  ],
35
35
  "dependencies": {
36
- "@ditojs/ui": "^2.4.0",
37
- "@ditojs/utils": "^2.4.0",
36
+ "@ditojs/ui": "^2.4.1",
37
+ "@ditojs/utils": "^2.4.1",
38
38
  "@kyvg/vue3-notification": "^2.9.0",
39
39
  "@lk77/vue3-color": "^3.0.6",
40
40
  "@tiptap/core": "^2.0.3",
@@ -74,7 +74,7 @@
74
74
  "vue-upload-component": "^3.1.8"
75
75
  },
76
76
  "devDependencies": {
77
- "@ditojs/build": "^2.4.0",
77
+ "@ditojs/build": "^2.4.1",
78
78
  "@vitejs/plugin-vue": "^4.1.0",
79
79
  "@vue/compiler-sfc": "^3.2.47",
80
80
  "pug": "^3.0.2",
@@ -83,7 +83,7 @@
83
83
  "vite": "^4.3.1"
84
84
  },
85
85
  "types": "types",
86
- "gitHead": "11402f86e31da1b8ebd0a6ea5c984d3abf12d7be",
86
+ "gitHead": "3041bd1b47272ac99276fbeca8152cbfb1d75b77",
87
87
  "scripts": {
88
88
  "build": "vite build",
89
89
  "watch": "yarn build --mode 'development' --watch",
@@ -216,6 +216,7 @@ export default DitoComponent.component('DitoContainer', {
216
216
  .dito-container {
217
217
  $self: &;
218
218
 
219
+ position: relative;
219
220
  display: flex;
220
221
  flex-flow: column;
221
222
  align-items: flex-start;
@@ -260,12 +261,6 @@ export default DitoComponent.component('DitoContainer', {
260
261
  }
261
262
  }
262
263
 
263
- &--drop-target {
264
- background: $content-color-background;
265
- border-radius: $border-radius;
266
- z-index: $drag-overlay-z-index + 1;
267
- }
268
-
269
264
  &--omit-padding {
270
265
  padding: 0;
271
266
 
@@ -5,17 +5,66 @@
5
5
  ul
6
6
  li(
7
7
  v-for="error of errors"
8
- )
9
- | {{ error }}
8
+ ) {{ error }}
10
9
  </template>
11
10
 
12
11
  <script>
12
+ import tippy from 'tippy.js'
13
13
  import DitoComponent from '../DitoComponent.js'
14
+ import { markRaw } from 'vue'
14
15
 
15
16
  // @vue/component
16
17
  export default DitoComponent.component('DitoErrors', {
17
18
  props: {
18
19
  errors: { type: Array, default: null }
20
+ },
21
+
22
+ data() {
23
+ return {
24
+ tip: null
25
+ }
26
+ },
27
+
28
+ watch: {
29
+ errors() {
30
+ this.$nextTick(this.updateErrors)
31
+ }
32
+ },
33
+
34
+ unmounted() {
35
+ this.tip?.destroy()
36
+ },
37
+
38
+ methods: {
39
+ updateErrors() {
40
+ let { tip } = this
41
+ tip?.hide()
42
+ if (this.errors) {
43
+ tip = this.tip ??= markRaw(tippy(this.$el.closest('.dito-container')))
44
+ tip.setProps({
45
+ content: this.errors.join('\n'),
46
+ theme: 'error',
47
+ trigger: 'manual',
48
+ appendTo: 'parent',
49
+ placement: 'bottom-start',
50
+ animation: 'shift-away-subtle',
51
+ popperOptions: {
52
+ modifiers: [
53
+ {
54
+ name: 'flip',
55
+ enabled: false
56
+ }
57
+ ]
58
+ },
59
+ interactive: true,
60
+ hideOnClick: false,
61
+ offset: [3, 3], // 1/2 form-spacing
62
+ zIndex: 1
63
+ })
64
+ tip.popper.addEventListener('mousedown', () => tip.hide())
65
+ tip.show()
66
+ }
67
+ }
19
68
  }
20
69
  })
21
70
  </script>
@@ -25,10 +74,9 @@ export default DitoComponent.component('DitoErrors', {
25
74
 
26
75
  .dito-errors {
27
76
  position: absolute;
28
- z-index: 1;
77
+ opacity: 0;
29
78
 
30
79
  ul {
31
- margin-top: 1px;
32
80
  color: $color-error;
33
81
  }
34
82
  }
@@ -137,7 +137,7 @@ export default DitoComponent.component('DitoHeader', {
137
137
  }
138
138
 
139
139
  .dito-spinner {
140
- margin-top: $menu-padding-ver;
140
+ margin-top: $header-padding-ver;
141
141
  }
142
142
 
143
143
  .dito-dirty {
@@ -142,7 +142,7 @@ export default DitoComponent.component('DitoPane', {
142
142
 
143
143
  focus() {
144
144
  if (this.tab) {
145
- this.$router.push({ hash: `#${this.tab}` })
145
+ return this.$router.push({ hash: `#${this.tab}` })
146
146
  }
147
147
  },
148
148
 
@@ -151,10 +151,9 @@ export default DitoComponent.component('DitoRoot', {
151
151
 
152
152
  const toggleDropTargetClass = enabled => {
153
153
  for (const upload of uploads) {
154
- upload.closest('.dito-container').classList.toggle(
155
- 'dito-container--drop-target',
156
- enabled
157
- )
154
+ upload
155
+ .closest('.dito-container')
156
+ .classList.toggle('dito-drop-target', enabled)
158
157
  }
159
158
  if (!enabled) {
160
159
  uploads = []
@@ -166,7 +165,7 @@ export default DitoComponent.component('DitoRoot', {
166
165
  if (enabled) {
167
166
  toggleDropTargetClass(true)
168
167
  } else {
169
- setTimeout(() => toggleDropTargetClass(false), 250)
168
+ setTimeout(() => toggleDropTargetClass(false), 150)
170
169
  }
171
170
  }
172
171
 
@@ -486,14 +485,33 @@ function addRoutes(router, routes) {
486
485
  backdrop-filter: blur(8px);
487
486
  }
488
487
 
488
+ .dito-drop-target {
489
+ --shadow-alpha: 0.25;
490
+
491
+ background: $content-color-background;
492
+ border-radius: $border-radius;
493
+ z-index: $drag-overlay-z-index + 1;
494
+ filter: drop-shadow(0 4px 8px rgba(0, 0, 0, var(--shadow-alpha)));
495
+ }
496
+
489
497
  .dito-drag-enter-active,
490
498
  .dito-drag-leave-active {
491
- transition: opacity 0.25s, backdrop-filter 0.25s;
499
+ $duration: 0.15s;
500
+
501
+ transition: opacity $duration, backdrop-filter $duration;
502
+
503
+ ~ * .dito-drop-target {
504
+ transition: filter $duration;
505
+ }
492
506
  }
493
507
 
494
508
  .dito-drag-enter-from,
495
509
  .dito-drag-leave-to {
496
510
  opacity: 0;
497
511
  backdrop-filter: blur(0);
512
+
513
+ ~ * .dito-drop-target {
514
+ --shadow-alpha: 0;
515
+ }
498
516
  }
499
517
  </style>
@@ -3,7 +3,10 @@ slot(name="before")
3
3
  .dito-schema(
4
4
  v-bind="$attrs"
5
5
  )
6
- .dito-schema-content(:class="{ 'dito-scroll': scrollable }")
6
+ .dito-schema-content(
7
+ ref="content"
8
+ :class="{ 'dito-scroll': scrollable }"
9
+ )
7
10
  Teleport(
8
11
  to=".dito-header__menu"
9
12
  :disabled="!headerInMenu"
@@ -280,6 +283,10 @@ export default DitoComponent.component('DitoSchema', {
280
283
  return this.everyComponent(it => it.isValidated)
281
284
  },
282
285
 
286
+ hasErrors() {
287
+ return this.someComponent(it => it.hasErrors)
288
+ },
289
+
283
290
  hasData() {
284
291
  return !!this.data
285
292
  },
@@ -334,6 +341,9 @@ export default DitoComponent.component('DitoSchema', {
334
341
  handler(hash) {
335
342
  if (this.hasTabs) {
336
343
  this.currentTab = hash?.slice(1) || null
344
+ if (this.hasErrors) {
345
+ this.repositionErrors()
346
+ }
337
347
  }
338
348
  }
339
349
  },
@@ -453,9 +463,18 @@ export default DitoComponent.component('DitoSchema', {
453
463
  }
454
464
  },
455
465
 
466
+ repositionErrors() {
467
+ // Force repositioning of error tooltips, as otherwise they
468
+ // sometimes don't show up in the right place initially when
469
+ // changing tabs
470
+ const scrollParent = this.$refs.content.closest('.dito-scroll')
471
+ scrollParent.scrollTop++
472
+ scrollParent.scrollTop--
473
+ },
474
+
456
475
  focus() {
457
- this.parentSchemaComponent?.focus()
458
476
  this.opened = true
477
+ return this.parentSchemaComponent?.focus()
459
478
  },
460
479
 
461
480
  validateAll(match, notify = true) {
@@ -485,7 +504,7 @@ export default DitoComponent.component('DitoSchema', {
485
504
  if (!component.validate(notify)) {
486
505
  // Focus first error field
487
506
  if (notify && first) {
488
- component.focus()
507
+ component.scrollIntoView()
489
508
  }
490
509
  first = false
491
510
  isValid = false
@@ -0,0 +1,198 @@
1
+ <template lang="pug">
2
+ .dito-upload-file
3
+ .dito-thumbnail(
4
+ v-if="thumbnail"
5
+ :class="`dito-thumbnail--${thumbnail}`"
6
+ )
7
+ .dito-thumbnail__inner
8
+ img(
9
+ v-if="source"
10
+ :src="source"
11
+ crossorigin="anonymous"
12
+ )
13
+ .dito-thumbnail__type(
14
+ v-else
15
+ )
16
+ span {{ type }}
17
+ span {{ file.name }}
18
+ </template>
19
+
20
+ <script>
21
+ import DitoComponent from '../DitoComponent.js'
22
+
23
+ // @vue/component
24
+ export default DitoComponent.component('DitoUploadFile', {
25
+ props: {
26
+ file: { type: Object, required: true },
27
+ thumbnail: { type: String, default: null },
28
+ thumbnailUrl: { type: String, default: null }
29
+ },
30
+
31
+ data() {
32
+ return {
33
+ uploadUrl: null
34
+ }
35
+ },
36
+
37
+ computed: {
38
+ type() {
39
+ return (
40
+ TYPES[this.file.type] ||
41
+ this.file.name.split('.').pop().toUpperCase()
42
+ )
43
+ },
44
+
45
+ source() {
46
+ return this.uploadUrl || this.thumbnailUrl
47
+ }
48
+ },
49
+
50
+ watch: {
51
+ 'file.upload.file': {
52
+ immediate: true,
53
+ handler(file) {
54
+ if (file && this.thumbnail) {
55
+ const reader = new FileReader()
56
+ reader.onload = () => {
57
+ this.uploadUrl = reader.result
58
+ }
59
+ reader.readAsDataURL(file)
60
+ } else {
61
+ this.uploadUrl = null
62
+ }
63
+ }
64
+ }
65
+ }
66
+ })
67
+
68
+ const TYPES = {
69
+ 'text/plain': 'TXT',
70
+ 'text/html': 'HTML',
71
+ 'text/css': 'CSS',
72
+ 'text/javascript': 'JS',
73
+ 'image/jpeg': 'JPG',
74
+ 'image/png': 'PNG',
75
+ 'image/gif': 'GIF',
76
+ 'image/svg+xml': 'SVG',
77
+ 'movie/mp4': 'MP4',
78
+ 'audio/mpeg': 'MP3',
79
+ 'application/json': 'JSON',
80
+ 'application/xml': 'XML',
81
+ 'application/pdf': 'PDF',
82
+ 'application/zip': 'ZIP'
83
+ }
84
+ </script>
85
+
86
+ <style lang="scss">
87
+ @use 'sass:math';
88
+ @import '../styles/_imports';
89
+
90
+ .dito-upload-file {
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: flex-start;
94
+ }
95
+
96
+ .dito-thumbnail {
97
+ $self: &;
98
+
99
+ // Small size by default
100
+ --max-size: #{1 * $input-height};
101
+ --corner-size: calc(var(--max-size) / 5);
102
+ --shadow-size: 1px;
103
+ --min-size: calc(2 * var(--corner-size));
104
+ --margin: 0em;
105
+ --drop-shadow: drop-shadow(
106
+ 0 calc(var(--shadow-size) * 0.75) var(--shadow-size) #{rgba(
107
+ $color-black,
108
+ 0.4
109
+ )}
110
+ );
111
+
112
+ position: relative;
113
+ margin: var(--margin);
114
+ margin-right: 0.5em;
115
+ filter: var(--drop-shadow);
116
+
117
+ &--small {
118
+ --max-size: #{1 * $input-height};
119
+ --margin: 0em;
120
+ --shadow-size: 1px;
121
+ }
122
+
123
+ &--medium {
124
+ --max-size: #{2 * $input-height};
125
+ --margin: 0.25em;
126
+ --shadow-size: 1.5px;
127
+ }
128
+
129
+ &--large {
130
+ --max-size: #{4 * $input-height};
131
+ --margin: 0.5em;
132
+ --shadow-size: 2.5px;
133
+ }
134
+
135
+ &__inner {
136
+ background: #ffffff;
137
+ clip-path: polygon(
138
+ 0 0,
139
+ calc(100% - var(--corner-size)) 0,
140
+ 100% var(--corner-size),
141
+ 100% 100%,
142
+ 0 100%
143
+ );
144
+
145
+ &::after {
146
+ content: '';
147
+ position: absolute;
148
+ top: 0;
149
+ right: 0;
150
+ width: var(--corner-size);
151
+ height: var(--corner-size);
152
+ background: linear-gradient(45deg, #ffffff, #eeeeee 40%, #dddddd 50%);
153
+ filter: var(--drop-shadow);
154
+ }
155
+ }
156
+
157
+ &__type {
158
+ --font-size: var(--corner-size);
159
+
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: center;
163
+ min-width: var(--min-size);
164
+ min-height: var(--max-size);
165
+ aspect-ratio: 3 / 4;
166
+
167
+ span {
168
+ --color: #{$color-grey};
169
+
170
+ font-size: min(var(--font-size), #{1.25 * $font-size});
171
+ color: var(--color);
172
+
173
+ #{$self}:not(#{$self}--small) & {
174
+ padding: 0 calc(var(--font-size) / 4);
175
+ border-radius: calc(var(--font-size) / 4);
176
+ background: var(--color);
177
+ color: #ffffff;
178
+ }
179
+
180
+ #{$self}--medium & {
181
+ --color: #{$color-light};
182
+ }
183
+
184
+ #{$self}--large & {
185
+ --color: #{$color-lighter};
186
+ }
187
+ }
188
+ }
189
+
190
+ img {
191
+ display: block;
192
+ min-width: var(--min-size);
193
+ min-height: var(--min-size);
194
+ max-width: var(--max-size);
195
+ max-height: var(--max-size);
196
+ }
197
+ }
198
+ </style>
@@ -31,5 +31,6 @@ export { default as DitoPagination } from './DitoPagination.vue'
31
31
  export { default as DitoTreeItem } from './DitoTreeItem.vue'
32
32
  export { default as DitoTableHead } from './DitoTableHead.vue'
33
33
  export { default as DitoTableCell } from './DitoTableCell.vue'
34
+ export { default as DitoUploadFile } from './DitoUploadFile.vue'
34
35
  export { default as DitoDraggable } from './DitoDraggable.vue'
35
36
  export { default as DitoVNode } from './DitoVNode.vue'
@@ -192,19 +192,36 @@ export default {
192
192
  return value
193
193
  },
194
194
 
195
- getChildStore(key) {
196
- return this.getStore(key) || this.setStore(key, reactive({}))
195
+ getChildStore(key, index) {
196
+ // When storing, temporary ids change to permanent ones and thus the key
197
+ // can change, so we need to store the index as well, to be able to find
198
+ // the store again after the item was saved.
199
+ const store = (
200
+ this.getStore(key) ||
201
+ index != null && this.getStore(index) ||
202
+ this.setStore(key, reactive({}))
203
+ )
204
+ if (index != null) {
205
+ this.setStore(index, store)
206
+ }
207
+ return store
197
208
  },
198
209
 
199
210
  getSchemaValue(
200
211
  keyOrDataPath,
201
- { type, schema = this.schema, callback = true, default: def } = {}
212
+ {
213
+ type,
214
+ default: def,
215
+ schema = this.schema,
216
+ context = this.context,
217
+ callback = true
218
+ } = {}
202
219
  ) {
203
220
  return getSchemaValue(keyOrDataPath, {
204
221
  type,
205
222
  schema,
223
+ context,
206
224
  callback,
207
- context: this.context,
208
225
  default: isFunction(def) ? () => def.call(this) : def
209
226
  })
210
227
  },
@@ -365,7 +382,9 @@ export default {
365
382
  }
366
383
  // See: https://stackoverflow.com/a/49917066/1163708
367
384
  const a = document.createElement('a')
368
- a.href = this.api.getApiUrl(options)
385
+ a.href = options.url?.startsWith('blob:')
386
+ ? options.url
387
+ : this.api.getApiUrl(options)
369
388
  a.download = options.filename ?? null
370
389
  const { body } = document
371
390
  body.appendChild(a)
@@ -3,7 +3,7 @@ import ValidationMixin from './ValidationMixin.js'
3
3
  import { getSchemaAccessor } from '../utils/accessor.js'
4
4
  import { computeValue } from '../utils/schema.js'
5
5
  import { getItem, getParentItem } from '../utils/data.js'
6
- import { asArray, camelize } from '@ditojs/utils'
6
+ import { camelize } from '@ditojs/utils'
7
7
 
8
8
  // @vue/component
9
9
  export default {
@@ -238,22 +238,30 @@ export default {
238
238
  },
239
239
 
240
240
  // @overridable
241
- focusElement() {
242
- const [element = this.$el] = asArray(this.$refs.element)
243
- this.$nextTick(() => {
244
- element.focus?.()
245
- // If the element is disabled, `focus()` will likely not have the
246
- // desired effect. Use `scrollIntoView()` if available:
247
- if (!element.focus || this.disabled) {
248
- ;(element.$el || element).scrollIntoView?.()
249
- }
241
+ async scrollIntoView() {
242
+ await this.focusSchema()
243
+ const { element = this } = this.$refs
244
+ ;(element.$el || element).scrollIntoView?.({
245
+ behavior: 'smooth',
246
+ block: 'center'
250
247
  })
251
248
  },
252
249
 
253
- focus() {
250
+ // @overridable
251
+ async focusElement() {
252
+ await this.focusSchema()
253
+ const { element = this } = this.$refs
254
+ ;(element.$el || element).focus?.()
255
+ },
256
+
257
+ async focusSchema() {
254
258
  // Also focus this component's schema and panel in case it's a tab.
255
- this.schemaComponent.focus()
256
- this.tabComponent?.focus()
259
+ await this.schemaComponent.focus()
260
+ await this.tabComponent?.focus()
261
+ },
262
+
263
+ focus() {
264
+ this.scrollIntoView()
257
265
  this.focusElement()
258
266
  },
259
267
 
@@ -15,6 +15,12 @@ export default {
15
15
  }
16
16
  },
17
17
 
18
+ computed: {
19
+ hasErrors() {
20
+ return !!this.errors
21
+ }
22
+ },
23
+
18
24
  methods: {
19
25
  resetValidation() {
20
26
  this.isTouched = false
@@ -60,6 +66,8 @@ export default {
60
66
 
61
67
  markTouched() {
62
68
  this.isTouched = true
69
+ // Clear currently displayed errors when focusing input.
70
+ this.clearErrors()
63
71
  },
64
72
 
65
73
  markDirty() {
@@ -87,7 +95,7 @@ export default {
87
95
  this.addError(message, true)
88
96
  }
89
97
  if (focus) {
90
- this.focus()
98
+ this.scrollIntoView()
91
99
  }
92
100
  return true
93
101
  },
@@ -6,11 +6,7 @@ export default {
6
6
 
7
7
  computed: {
8
8
  errors() {
9
- return this.schemaComponents.reduce(
10
- (result, { errors }) =>
11
- errors && result ? result.concat(errors) : errors,
12
- null
13
- )
9
+ return this.schemaComponents.flatMap(({ errors }) => errors || [])
14
10
  },
15
11
 
16
12
  isTouched() {
@@ -1,5 +1,3 @@
1
- @import 'tippy.js/animations/shift-away-subtle.css';
2
-
3
1
  .dito-info {
4
2
  --size: calc(1em * var(--line-height));
5
3
 
@@ -19,17 +17,3 @@
19
17
  width: var(--size);
20
18
  }
21
19
  }
22
-
23
- .tippy-box[data-theme~='info'] {
24
- background-color: $color-active;
25
- color: $color-white;
26
- filter: drop-shadow(0 2px 4px $color-shadow);
27
-
28
- > .tippy-arrow::before {
29
- color: $color-active;
30
- }
31
-
32
- .tippy-content {
33
- white-space: pre-line;
34
- }
35
- }
@@ -0,0 +1,39 @@
1
+ @use 'sass:color';
2
+ @import 'tippy.js/animations/shift-away-subtle.css';
3
+
4
+ .tippy-box {
5
+ &[data-theme] {
6
+ --color: #{$color-active};
7
+
8
+ font-size: unset;
9
+ line-height: unset;
10
+ background-color: var(--color);
11
+ color: $color-white;
12
+ filter: drop-shadow(0 2px 4px $color-shadow);
13
+
14
+ .tippy-content {
15
+ white-space: pre-line;
16
+ padding: ($input-padding-ver + 2 * $border-width)
17
+ ($input-padding-hor + $border-width);
18
+ }
19
+
20
+ > .tippy-arrow::before {
21
+ color: var(--color);
22
+ }
23
+ }
24
+
25
+ &[data-theme~='info'] {
26
+ --color: #{color.adjust($color-active, $lightness: 5%)};
27
+ }
28
+
29
+ &[data-theme~='error'] {
30
+ --color: #{color.adjust($color-error, $lightness: 5%)};
31
+
32
+ cursor: pointer;
33
+
34
+ > .tippy-arrow {
35
+ transform: unset !important;
36
+ left: 16px !important;
37
+ }
38
+ }
39
+ }