@ditojs/admin 2.3.1 → 2.4.0

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.3.1",
3
+ "version": "2.4.0",
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.3.1",
37
- "@ditojs/utils": "^2.3.0",
36
+ "@ditojs/ui": "^2.4.0",
37
+ "@ditojs/utils": "^2.4.0",
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.3.0",
77
+ "@ditojs/build": "^2.4.0",
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": "1b5f5121a2e8e6d3741f0bb2b81d38c2343810d6",
86
+ "gitHead": "11402f86e31da1b8ebd0a6ea5c984d3abf12d7be",
87
87
  "scripts": {
88
88
  "build": "vite build",
89
89
  "watch": "yarn build --mode 'development' --watch",
@@ -248,7 +248,7 @@ export default DitoComponent.component('DitoContainer', {
248
248
  --justify: flex-end;
249
249
  }
250
250
 
251
- &:not(#{$self}--only-in-row) {
251
+ &:not(#{$self}--alone-in-row) {
252
252
  // Now only apply alignment if there are neighbouring components no the
253
253
  // same row that also align.
254
254
  // Look ahead:
@@ -260,6 +260,12 @@ export default DitoComponent.component('DitoContainer', {
260
260
  }
261
261
  }
262
262
 
263
+ &--drop-target {
264
+ background: $content-color-background;
265
+ border-radius: $border-radius;
266
+ z-index: $drag-overlay-z-index + 1;
267
+ }
268
+
263
269
  &--omit-padding {
264
270
  padding: 0;
265
271
 
@@ -179,6 +179,7 @@ export default DitoComponent.component('DitoDialog', {
179
179
  background: rgba(0, 0, 0, 0.2);
180
180
 
181
181
  &__focus-trap {
182
+ display: flex;
182
183
  max-height: 100%;
183
184
  }
184
185
 
@@ -20,7 +20,7 @@ nav.dito-header
20
20
  v-if="isLoading"
21
21
  )
22
22
  //- Teleport target for `.dito-schema-header`:
23
- .dito-menu
23
+ .dito-header__menu
24
24
  slot
25
25
  </template>
26
26
 
@@ -77,15 +77,16 @@ export default DitoComponent.component('DitoHeader', {
77
77
  @import '../styles/_imports';
78
78
 
79
79
  .dito-header {
80
+ position: relative;
80
81
  background: $color-black;
81
- font-size: $menu-font-size;
82
- line-height: $menu-line-height;
83
- z-index: $menu-z-index;
82
+ font-size: $header-font-size;
83
+ line-height: $header-line-height;
84
+ z-index: $header-z-index;
84
85
  @include user-select(none);
85
86
 
86
87
  span {
87
88
  display: inline-block;
88
- padding: $menu-padding;
89
+ padding: $header-padding;
89
90
  color: $color-white;
90
91
  }
91
92
 
@@ -0,0 +1,177 @@
1
+ <template lang="pug">
2
+ ul.dito-menu(
3
+ v-resize="onResize"
4
+ :style="{ '--width': width ? `${width}px` : null }"
5
+ )
6
+ li(
7
+ v-for="item in items"
8
+ )
9
+ template(
10
+ v-if="shouldRenderSchema(item)"
11
+ )
12
+ a.dito-link(
13
+ :href="getItemHref(item)"
14
+ :class="getItemClass(item, 'dito-sub-menu-link')"
15
+ @click.prevent.stop="onClickItem(item)"
16
+ ) {{ getLabel(item) }}
17
+ DitoMenu(
18
+ v-if="item.items"
19
+ :class="getItemClass(item, 'dito-sub-menu')"
20
+ :items="item.items"
21
+ )
22
+ </template>
23
+
24
+ <script>
25
+ import DitoComponent from '../DitoComponent.js'
26
+
27
+ // @vue/component
28
+ export default DitoComponent.component('DitoMenu', {
29
+ props: {
30
+ items: {
31
+ type: [Object, Array],
32
+ default: () => []
33
+ }
34
+ },
35
+
36
+ data() {
37
+ return {
38
+ width: 0
39
+ }
40
+ },
41
+
42
+ methods: {
43
+ onResize({ contentRect: { width } }) {
44
+ if (width) {
45
+ this.width = width
46
+ }
47
+ },
48
+
49
+ getItemClass(item, subMenuClass) {
50
+ return {
51
+ [subMenuClass]: !!item.items,
52
+ 'dito-active': this.isActiveItem(item)
53
+ }
54
+ },
55
+
56
+ getItemPath(item) {
57
+ return item?.path
58
+ ? `/${item.path}`
59
+ : item.items
60
+ ? this.getItemPath(Object.values(item.items)[0])
61
+ : null
62
+ },
63
+
64
+ getItemHref(item) {
65
+ const path = this.getItemPath(item)
66
+ return path ? this.$router.resolve(path).href : null
67
+ },
68
+
69
+ isActiveItem(item) {
70
+ return (
71
+ this.$route.path.startsWith(this.getItemPath(item)) ||
72
+ item.items && Object.values(item.items).some(this.isActiveItem)
73
+ )
74
+ },
75
+
76
+ onClickItem(item) {
77
+ const path = this.getItemPath(item)
78
+ if (path) {
79
+ this.$router.push({ path, force: true })
80
+ }
81
+ }
82
+ }
83
+ })
84
+ </script>
85
+
86
+ <style lang="scss">
87
+ @use 'sass:color';
88
+ @import '../styles/_imports';
89
+
90
+ .dito-menu {
91
+ $item-height: $menu-font-size + 2 * $menu-padding-ver;
92
+
93
+ border-right: $border-style;
94
+ padding: 0 $menu-spacing;
95
+
96
+ li {
97
+ &:has(.dito-sub-menu:not(.dito-active)) {
98
+ // Pop-out sub-menus on hover:
99
+ &:hover {
100
+ .dito-sub-menu-link {
101
+ background: $color-lightest;
102
+ }
103
+
104
+ .dito-sub-menu {
105
+ display: block;
106
+ position: absolute;
107
+ width: var(--width);
108
+ z-index: $header-z-index;
109
+ transform: translateX(calc(var(--width) + 2 * $menu-spacing))
110
+ translateY(-$item-height);
111
+
112
+ li:first-child {
113
+ .dito-link {
114
+ margin-top: 0;
115
+ }
116
+ }
117
+
118
+ &::before {
119
+ // Fill the gap to not loose the hover when moving over it.
120
+ content: '';
121
+ position: absolute;
122
+ top: 0;
123
+ left: -2 * $menu-spacing;
124
+ width: 2 * $menu-spacing;
125
+ height: $item-height;
126
+ opacity: 0;
127
+ }
128
+ }
129
+
130
+ // .dito-sub-menu-link,
131
+ .dito-sub-menu {
132
+ box-shadow: $shadow-window;
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ .dito-link {
139
+ display: block;
140
+ padding: $menu-padding;
141
+ line-height: $menu-line-height;
142
+ border-radius: $border-radius;
143
+ margin-top: $menu-spacing;
144
+
145
+ &:focus:not(:active, .dito-active) {
146
+ box-shadow: $shadow-focus;
147
+ }
148
+
149
+ &:hover {
150
+ background: rgba(255, 255, 255, 0.5);
151
+ }
152
+
153
+ &.dito-active {
154
+ color: $color-white;
155
+ background: $color-active;
156
+ }
157
+ }
158
+
159
+ .dito-sub-menu-link {
160
+ &.dito-active {
161
+ background: color.adjust($color-active, $alpha: -0.3);
162
+ }
163
+ }
164
+
165
+ .dito-sub-menu {
166
+ display: none;
167
+ border-right: 0;
168
+ padding: 0;
169
+ border-radius: $border-radius;
170
+ background: $color-lightest;
171
+
172
+ &.dito-active {
173
+ display: block;
174
+ }
175
+ }
176
+ }
177
+ </style>
@@ -5,6 +5,10 @@
5
5
  position="top right"
6
6
  classes="dito-notification"
7
7
  )
8
+ Transition(name="dito-drag")
9
+ .dito-drag-overlay(
10
+ v-if="isDraggingFiles"
11
+ )
8
12
  TransitionGroup(name="dito-dialog")
9
13
  DitoDialog(
10
14
  v-for="(dialog, key) in dialogs"
@@ -16,7 +20,7 @@
16
20
  :settings="dialog.settings"
17
21
  @remove="removeDialog(key)"
18
22
  )
19
- DitoMenu
23
+ DitoSidebar
20
24
  main.dito-page.dito-scroll-parent
21
25
  DitoHeader(
22
26
  :spinner="options.spinner"
@@ -43,7 +47,7 @@ import DitoDialog from './DitoDialog.vue'
43
47
  import DomMixin from '../mixins/DomMixin.js'
44
48
  import {
45
49
  processView,
46
- resolveSchemas,
50
+ resolveViews,
47
51
  processSchemaComponents
48
52
  } from '../utils/schema.js'
49
53
 
@@ -69,7 +73,8 @@ export default DitoComponent.component('DitoRoot', {
69
73
  removeRoutes: null,
70
74
  dialogs: {},
71
75
  allowLogin: false,
72
- loadingCount: 0
76
+ loadingCount: 0,
77
+ isDraggingFiles: false
73
78
  }
74
79
  },
75
80
 
@@ -91,6 +96,8 @@ export default DitoComponent.component('DitoRoot', {
91
96
  },
92
97
 
93
98
  async mounted() {
99
+ this.setupDragAndDrop()
100
+
94
101
  tippyDelegate(this.$el, {
95
102
  target: '.dito-info',
96
103
  onCreate(instance) {
@@ -103,6 +110,7 @@ export default DitoComponent.component('DitoRoot', {
103
110
  })
104
111
  }
105
112
  })
113
+
106
114
  // Clear the label marked as active on all mouse and keyboard events, except
107
115
  // the ones that DitoLabel itself intercepts.
108
116
  this.domOn(document, {
@@ -118,6 +126,7 @@ export default DitoComponent.component('DitoRoot', {
118
126
  }
119
127
  }
120
128
  })
129
+
121
130
  try {
122
131
  this.allowLogin = false
123
132
  if (await this.fetchUser()) {
@@ -132,6 +141,81 @@ export default DitoComponent.component('DitoRoot', {
132
141
  },
133
142
 
134
143
  methods: {
144
+ setupDragAndDrop() {
145
+ // This code only happens the visual effects around dragging and dropping
146
+ // files into a `DitoTypeUpload` component. The actual uploading is
147
+ // handled by the `DitoTypeUpload` component itself.
148
+
149
+ let dragCount = 0
150
+ let uploads = []
151
+
152
+ const toggleDropTargetClass = enabled => {
153
+ for (const upload of uploads) {
154
+ upload.closest('.dito-container').classList.toggle(
155
+ 'dito-container--drop-target',
156
+ enabled
157
+ )
158
+ }
159
+ if (!enabled) {
160
+ uploads = []
161
+ }
162
+ }
163
+
164
+ const setDraggingFiles = enabled => {
165
+ this.isDraggingFiles = enabled
166
+ if (enabled) {
167
+ toggleDropTargetClass(true)
168
+ } else {
169
+ setTimeout(() => toggleDropTargetClass(false), 250)
170
+ }
171
+ }
172
+
173
+ this.domOn(document, {
174
+ dragenter: event => {
175
+ if (!dragCount && event.dataTransfer) {
176
+ uploads = document.querySelectorAll('.dito-upload')
177
+ const hasUploads = uploads.length > 0
178
+ event.dataTransfer.effectAllowed = hasUploads ? 'copy' : 'none'
179
+ if (hasUploads) {
180
+ setDraggingFiles(true)
181
+ } else {
182
+ event.preventDefault()
183
+ event.stopPropagation()
184
+ return
185
+ }
186
+ }
187
+ dragCount++
188
+ },
189
+
190
+ dragleave: event => {
191
+ dragCount--
192
+ if (!dragCount && event.dataTransfer) {
193
+ setDraggingFiles(false)
194
+ }
195
+ },
196
+
197
+ dragover: event => {
198
+ if (event.dataTransfer) {
199
+ const canDrop = event.target.closest(
200
+ '.dito-container:has(.dito-upload)'
201
+ )
202
+ event.dataTransfer.dropEffect = canDrop ? 'copy' : 'none'
203
+ if (!canDrop) {
204
+ event.preventDefault()
205
+ event.stopPropagation()
206
+ }
207
+ }
208
+ },
209
+
210
+ drop: event => {
211
+ dragCount = 0
212
+ if (event.dataTransfer) {
213
+ setDraggingFiles(false)
214
+ }
215
+ }
216
+ })
217
+ },
218
+
135
219
  notify({ type = 'info', title, text } = {}) {
136
220
  title ||= (
137
221
  {
@@ -328,7 +412,7 @@ export default DitoComponent.component('DitoRoot', {
328
412
 
329
413
  async resolveViews() {
330
414
  try {
331
- this.resolvedViews = await resolveSchemas(this.unresolvedViews)
415
+ this.resolvedViews = await resolveViews(this.unresolvedViews)
332
416
  } catch (error) {
333
417
  if (!error.request) {
334
418
  console.error(error)
@@ -350,7 +434,7 @@ export default DitoComponent.component('DitoRoot', {
350
434
  path: '/',
351
435
  components: {}
352
436
  },
353
- ...routes
437
+ ...routes.flat()
354
438
  ])
355
439
  this.$router.replace(fullPath)
356
440
  }
@@ -389,4 +473,27 @@ function addRoutes(router, routes) {
389
473
  background: $content-color-background;
390
474
  }
391
475
  }
476
+
477
+ .dito-drag-overlay {
478
+ position: fixed;
479
+ top: 0;
480
+ left: 0;
481
+ z-index: $drag-overlay-z-index;
482
+ width: 100%;
483
+ height: 100%;
484
+ background: rgba(0, 0, 0, 0.25);
485
+ pointer-events: none;
486
+ backdrop-filter: blur(8px);
487
+ }
488
+
489
+ .dito-drag-enter-active,
490
+ .dito-drag-leave-active {
491
+ transition: opacity 0.25s, backdrop-filter 0.25s;
492
+ }
493
+
494
+ .dito-drag-enter-from,
495
+ .dito-drag-leave-to {
496
+ opacity: 0;
497
+ backdrop-filter: blur(0);
498
+ }
392
499
  </style>
@@ -5,7 +5,7 @@ slot(name="before")
5
5
  )
6
6
  .dito-schema-content(:class="{ 'dito-scroll': scrollable }")
7
7
  Teleport(
8
- to=".dito-menu"
8
+ to=".dito-header__menu"
9
9
  :disabled="!headerInMenu"
10
10
  )
11
11
  .dito-schema-header(
@@ -118,6 +118,7 @@ import { getStoreAccessor } from '../utils/accessor.js'
118
118
  export default DitoComponent.component('DitoSchema', {
119
119
  mixins: [ItemMixin],
120
120
  components: { TransitionHeight },
121
+ inheritAttrs: false,
121
122
 
122
123
  provide() {
123
124
  return {
@@ -787,13 +788,13 @@ export default DitoComponent.component('DitoSchema', {
787
788
  &--menu {
788
789
  // Align the tabs on top of to the header menu.
789
790
  position: absolute;
790
- height: $menu-height;
791
- padding: 0 $menu-padding-hor;
791
+ height: $header-height;
792
+ padding: 0 $header-padding-hor;
792
793
  max-width: $content-width;
793
794
  top: 0;
794
795
  left: 0;
795
796
  right: 0;
796
- z-index: $menu-z-index;
797
+ z-index: $header-z-index;
797
798
  // Turn off pointer events so that DitoTrail keeps receiving events...
798
799
  pointer-events: none;
799
800
  // ...but allow interaction with the tabs and buttons (e.g. clipboard)
@@ -801,8 +802,8 @@ export default DitoComponent.component('DitoSchema', {
801
802
  .dito-tabs,
802
803
  .dito-buttons {
803
804
  pointer-events: auto;
804
- line-height: $menu-line-height;
805
- font-size: $menu-font-size;
805
+ line-height: $header-line-height;
806
+ font-size: $header-font-size;
806
807
  }
807
808
  }
808
809
  }
@@ -1,78 +1,40 @@
1
1
  <template lang="pug">
2
2
  nav.dito-sidebar.dito-scroll-parent
3
- h1 {{ appState.title }}
4
- ul.dito-scroll
5
- li(
6
- v-for="view in views"
7
- )
8
- RouterLink(
9
- v-if="shouldRenderSchema(view)"
10
- v-slot="{ isActive, href, route }"
11
- custom
12
- :to="`/${view.path}`"
13
- )
14
- a.dito-link(
15
- :href="href"
16
- :class="{ 'dito-active': isActive }"
17
- @click.prevent="onClickLink(route)"
18
- ) {{ getLabel(view) }}
3
+ h1
4
+ RouterLink.dito-link(to="/") {{ appState.title }}
5
+ DitoMenu.dito-scroll(:items="views")
19
6
  </template>
20
7
 
21
8
  <script>
22
9
  import DitoComponent from '../DitoComponent.js'
23
10
 
24
11
  // @vue/component
25
- export default DitoComponent.component('DitoMenu', {
26
- methods: {
27
- onClickLink(route) {
28
- this.$router.push({ ...route, force: true })
29
- }
30
- }
31
- })
12
+ export default DitoComponent.component('DitoSidebar', {})
32
13
  </script>
33
14
 
34
15
  <style lang="scss">
35
16
  @import '../styles/_imports';
36
17
 
37
18
  .dito-sidebar {
19
+ @include user-select(none);
20
+
38
21
  flex: initial;
39
22
  font-size: $menu-font-size;
40
23
  white-space: nowrap;
41
- @include user-select(none);
42
-
43
- ul {
44
- background: $color-lighter;
45
- border-right: $border-style;
46
- }
24
+ background: $color-lighter;
47
25
 
48
- a,
49
26
  h1 {
50
27
  display: block;
51
- }
52
-
53
- h1 {
54
- padding: $menu-padding;
55
- line-height: $menu-line-height;
28
+ line-height: $header-line-height;
56
29
  font-weight: bold;
57
30
  background: $color-darker;
58
31
  border-right: $border-width solid $color-darkest;
59
32
  color: $color-white;
60
- }
61
-
62
- .dito-link {
63
- padding: $menu-padding;
64
- line-height: $menu-line-height;
65
33
 
66
- &.dito-active {
67
- color: $color-white;
68
- background: $color-active;
34
+ .dito-link {
35
+ display: block;
36
+ padding: $header-padding;
69
37
  }
70
38
  }
71
39
  }
72
-
73
- .dito-link {
74
- &:focus:not(:active, .dito-active) {
75
- box-shadow: $shadow-focus;
76
- }
77
- }
78
40
  </style>
@@ -94,6 +94,7 @@ export default DitoComponent.component('DitoTableHead', {
94
94
 
95
95
  .dito-button {
96
96
  // Convention: Nested spans handle padding, see below
97
+ display: block; // Override default inline-flex positioning.
97
98
  padding: 0;
98
99
  width: 100%;
99
100
  text-align: inherit;
@@ -4,6 +4,7 @@
4
4
  // convention is in order of encountered hierarchy in the DOM.
5
5
 
6
6
  export { default as DitoRoot } from './DitoRoot.vue'
7
+ export { default as DitoMenu } from './DitoMenu.vue'
7
8
  export { default as DitoSidebar } from './DitoSidebar.vue'
8
9
  export { default as DitoHeader } from './DitoHeader.vue'
9
10
  export { default as DitoAccount } from './DitoAccount.vue'
@@ -239,17 +239,15 @@ export default {
239
239
 
240
240
  // @overridable
241
241
  focusElement() {
242
- const [element] = asArray(this.$refs.element)
243
- if (element) {
244
- this.$nextTick(() => {
245
- element.focus()
246
- // If the element is disabled, `focus()` will likely not have the
247
- // desired effect. Use `scrollIntoView()` if available:
248
- if (this.disabled) {
249
- ;(element.$el || element).scrollIntoView?.()
250
- }
251
- })
252
- }
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
+ }
250
+ })
253
251
  },
254
252
 
255
253
  focus() {
@@ -115,7 +115,7 @@
115
115
  &.dito-buttons-large {
116
116
  --button-margin: 3px;
117
117
 
118
- font-size: $menu-font-size;
118
+ font-size: $header-font-size;
119
119
  flex-flow: row wrap;
120
120
  justify-content: center;
121
121
  padding-bottom: $content-padding;
@@ -149,9 +149,9 @@
149
149
  // For now, nothing for these:
150
150
  // .dito-button-create:empty::before,
151
151
  // .dito-button-add:empty::before
152
- // Special .dito-button-add-upload without :empty:
153
- .dito-button-add-upload::before {
154
- @extend %icon-add;
152
+
153
+ .dito-button-upload:empty::before {
154
+ @extend %icon-upload;
155
155
  }
156
156
 
157
157
  .dito-button-delete:empty::before,
@@ -9,7 +9,6 @@
9
9
  // To make vertical scrolling in .dito-scroll work:
10
10
  flex: 1;
11
11
  display: flex;
12
- position: relative;
13
12
  flex-flow: column;
14
13
  overflow: hidden;
15
14
  }