@ditojs/admin 2.2.10 → 2.2.12

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.2.10",
3
+ "version": "2.2.12",
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",
@@ -82,7 +82,7 @@
82
82
  "vite": "^4.3.1"
83
83
  },
84
84
  "types": "types",
85
- "gitHead": "8c6fffbc52edec642c72859dc280f3bb442b3bf1",
85
+ "gitHead": "062d8d229de60f7c81677b927c5c66ce9f2d2337",
86
86
  "scripts": {
87
87
  "build": "vite build",
88
88
  "watch": "yarn build --mode 'development' --watch",
package/src/DitoAdmin.js CHANGED
@@ -13,6 +13,7 @@ import * as components from './components/index.js'
13
13
  import * as types from './types/index.js'
14
14
  import DitoRoot from './components/DitoRoot.vue'
15
15
  import DitoTypeComponent from './DitoTypeComponent.js'
16
+ import ResizeDirective from './directives/resize.js'
16
17
  import { getResource } from './utils/resource.js'
17
18
  import { deprecate } from './utils/deprecate.js'
18
19
  import { formatQuery } from './utils/route.js'
@@ -198,7 +199,7 @@ export default class DitoAdmin {
198
199
  componentName: 'VueNotifications'
199
200
  })
200
201
 
201
- // root.component('vue-modal', VueModal)
202
+ app.directive('resize', ResizeDirective)
202
203
 
203
204
  app.use(
204
205
  createRouter({
@@ -21,6 +21,7 @@ export default {
21
21
  generateLabel: true,
22
22
  excludeValue: false,
23
23
  ignoreMissingValue: null,
24
+ keepAligned: true,
24
25
  omitPadding: false,
25
26
 
26
27
  component: DitoComponent.component,
@@ -31,7 +31,7 @@ import { isString, isNumber } from '@ditojs/utils'
31
31
  import DitoComponent from '../DitoComponent.js'
32
32
  import DitoContext from '../DitoContext.js'
33
33
  import { getSchemaAccessor } from '../utils/accessor.js'
34
- import { getTypeComponent, omitPadding } from '../utils/schema.js'
34
+ import { getTypeComponent, keepAligned, omitPadding } from '../utils/schema.js'
35
35
  import { parseFraction } from '../utils/math.js'
36
36
 
37
37
  // @vue/component
@@ -45,7 +45,9 @@ export default DitoComponent.component('DitoContainer', {
45
45
  single: { type: Boolean, default: false },
46
46
  nested: { type: Boolean, default: true },
47
47
  disabled: { type: Boolean, required: true },
48
- generateLabels: { type: Boolean, default: false }
48
+ generateLabels: { type: Boolean, default: false },
49
+ firstInRow: { type: Boolean, default: false },
50
+ lastInRow: { type: Boolean, default: false }
49
51
  },
50
52
 
51
53
  data() {
@@ -68,25 +70,13 @@ export default DitoComponent.component('DitoContainer', {
68
70
  return (
69
71
  label !== false && (
70
72
  !!label ||
71
- this.generateLabels && (
72
- this.typeComponent?.generateLabel ||
73
- // If the component has no label but isn't full width, render an
74
- // empty label for alignment with other components:
75
- !this.isFullWidth
76
- )
73
+ this.generateLabels && this.typeComponent?.generateLabel
77
74
  )
78
75
  )
79
76
  },
80
77
 
81
78
  label() {
82
- return this.hasLabel
83
- ? this.getLabel(
84
- this.schema,
85
- // Pass an empty string in case we need an empty label, see
86
- // `hasLabel()`:
87
- this.typeComponent?.generateLabel ? this.schema.name : ''
88
- ) || ''
89
- : null
79
+ return this.hasLabel ? this.getLabel(this.schema) : null
90
80
  },
91
81
 
92
82
  labelDataPath() {
@@ -94,13 +84,6 @@ export default DitoComponent.component('DitoContainer', {
94
84
  return this.nested ? this.dataPath : null
95
85
  },
96
86
 
97
- isFullWidth() {
98
- return (
99
- !this.componentBasis.endsWith('%') ||
100
- parseFloat(this.componentBasis) === 100
101
- )
102
- },
103
-
104
87
  componentWidth: getSchemaAccessor('width', {
105
88
  type: [String, Number],
106
89
  default() {
@@ -146,7 +129,11 @@ export default DitoComponent.component('DitoContainer', {
146
129
  return {
147
130
  [`${prefix}--single`]: this.single,
148
131
  [`${prefix}--has-label`]: this.hasLabel,
132
+ [`${prefix}--aligned`]: keepAligned(this.schema),
149
133
  [`${prefix}--omit-padding`]: omitPadding(this.schema),
134
+ [`${prefix}--first-in-row`]: this.firstInRow,
135
+ [`${prefix}--last-in-row`]: this.lastInRow,
136
+ [`${prefix}--alone-in-row`]: this.firstInRow && this.lastInRow,
150
137
  ...(
151
138
  isString(containerClass)
152
139
  ? { [containerClass]: true }
@@ -205,6 +192,8 @@ export default DitoComponent.component('DitoContainer', {
205
192
  @import '../styles/_imports';
206
193
 
207
194
  .dito-container {
195
+ $self: &;
196
+
208
197
  display: flex;
209
198
  flex-flow: column;
210
199
  align-items: flex-start;
@@ -227,6 +216,26 @@ export default DitoComponent.component('DitoContainer', {
227
216
  padding: 0;
228
217
  }
229
218
 
219
+ &--aligned {
220
+ // For components with labels, align the label at the top and the component
221
+ // at the bottom.
222
+ --justify: space-between;
223
+
224
+ &:has(> :only-child) {
225
+ // But if there is no label, still align the component to the bottom.
226
+ --justify: flex-end;
227
+ }
228
+
229
+ // Now only apply alignment if there are neighbouring components no the same
230
+ // row that also align.
231
+ // Look ahead:
232
+ &:not(#{$self}--last-in-row) + #{&}:not(#{$self}--first-in-row),
233
+ // Look behind:
234
+ &:not(#{$self}--last-in-row):has(+ #{&}:not(#{$self}--first-in-row)) {
235
+ justify-content: var(--justify);
236
+ }
237
+ }
238
+
230
239
  &--omit-padding {
231
240
  padding: 0;
232
241
 
@@ -1,5 +1,6 @@
1
1
  <template lang="pug">
2
2
  component.dito-label(
3
+ v-if="text || collapsible"
3
4
  :is="tag"
4
5
  v-bind="attributes"
5
6
  :class="{ 'dito-active': isActive }"
@@ -14,7 +14,7 @@
14
14
  nestedDataPath,
15
15
  nested,
16
16
  store
17
- } in componentSchemas`
17
+ }, index in componentSchemas`
18
18
  )
19
19
  .dito-break(
20
20
  v-if="schema.break === 'before'"
@@ -22,6 +22,7 @@
22
22
  DitoContainer(
23
23
  v-if="shouldRender(schema)"
24
24
  :key="nestedDataPath"
25
+ v-resize="event => onResize(index, event)"
25
26
  :schema="schema"
26
27
  :dataPath="dataPath"
27
28
  :data="data"
@@ -31,6 +32,8 @@
31
32
  :nested="nested"
32
33
  :disabled="disabled"
33
34
  :generateLabels="generateLabels"
35
+ :firstInRow="schema.break === 'before' || isFirstInRow(index)"
36
+ :lastInRow="schema.break === 'after' || isLastInRow(index)"
34
37
  )
35
38
  .dito-break(
36
39
  v-if="schema.break === 'after'"
@@ -63,6 +66,12 @@ export default DitoComponent.component('DitoPane', {
63
66
  generateLabels: { type: Boolean, default: false }
64
67
  },
65
68
 
69
+ data() {
70
+ return {
71
+ positions: []
72
+ }
73
+ },
74
+
66
75
  computed: {
67
76
  tabComponent() {
68
77
  return this.tab ? this : this.$tabComponent()
@@ -135,9 +144,42 @@ export default DitoComponent.component('DitoPane', {
135
144
  if (this.tab) {
136
145
  this.$router.push({ hash: `#${this.tab}` })
137
146
  }
147
+ },
148
+
149
+ onResize(index, { target }) {
150
+ const { y, width, height } = target.getBoundingClientRect()
151
+ this.positions[index] = width > 0 && height > 0 ? y : null
152
+ },
153
+
154
+ isFirstInRow(index) {
155
+ const { positions } = this
156
+ return (
157
+ positions[index] !== null && (
158
+ index === 0 ||
159
+ (findNextPosition(positions, index, -1, Infinity) < positions[index])
160
+ )
161
+ )
162
+ },
163
+
164
+ isLastInRow(index) {
165
+ const { positions } = this
166
+ return (
167
+ positions[index] !== null && (
168
+ index === positions.length - 1 ||
169
+ findNextPosition(positions, index, +1, 0) > positions[index]
170
+ )
171
+ )
138
172
  }
139
173
  }
140
174
  })
175
+
176
+ function findNextPosition(positions, index, step, fallback) {
177
+ for (let i = index + step; i >= 0 && i < positions.length; i += step) {
178
+ const position = positions[i]
179
+ if (position) return position
180
+ }
181
+ return fallback
182
+ }
141
183
  </script>
142
184
 
143
185
  <style lang="scss">
@@ -164,8 +164,8 @@ export default DitoComponent.component('DitoPanel', {
164
164
  @import '../styles/_imports';
165
165
 
166
166
  .dito-panel {
167
- & + & {
168
- margin-top: $content-padding;
167
+ &:not(:last-child) {
168
+ margin-bottom: $content-padding;
169
169
  }
170
170
 
171
171
  &__header {
@@ -0,0 +1,83 @@
1
+ import { asArray } from '@ditojs/utils'
2
+
3
+ export default {
4
+ mounted(node, binding) {
5
+ observeResize(node, binding.value, binding.arg)
6
+ },
7
+
8
+ unmounted(node, binding) {
9
+ unobserveResize(node, binding.value, binding.arg)
10
+ }
11
+ }
12
+
13
+ export function observeResize(node, handler, options) {
14
+ Observer.getObserver(options).observe(node, handler)
15
+ }
16
+
17
+ export function unobserveResize(node, handler, options) {
18
+ Observer.getObserver(options).unobserve(node, handler)
19
+ }
20
+
21
+ export const isResizeSupported = typeof ResizeObserver !== 'undefined'
22
+
23
+ const observers = {}
24
+
25
+ class Observer {
26
+ constructor(key, options) {
27
+ this.key = key
28
+ this.options = options
29
+ this.observer = isResizeSupported
30
+ ? new ResizeObserver(entries => this.handle(entries))
31
+ : null
32
+ this.handlersByNode = new WeakMap()
33
+ this.nodeCount = 0
34
+ }
35
+
36
+ observe(node, handler) {
37
+ let handlers = this.handlersByNode.get(node)
38
+ if (!handlers) {
39
+ handlers = new Set()
40
+ this.handlersByNode.set(node, handlers)
41
+ this.observer?.observe(node, this.options)
42
+ this.nodeCount++
43
+ }
44
+ handlers.add(handler)
45
+ }
46
+
47
+ unobserve(node, handler) {
48
+ const handlers = this.handlersByNode.get(node)
49
+ if (handlers?.delete(handler) && handlers.size === 0) {
50
+ this.handlersByNode.delete(node)
51
+ this.observer?.unobserve(node)
52
+ if (--this.nodeCount === 0) {
53
+ delete observers[this.key]
54
+ }
55
+ }
56
+ }
57
+
58
+ handle(entries) {
59
+ for (const entry of entries) {
60
+ const handlers = this.handlersByNode.get(entry.target)
61
+ if (handlers) {
62
+ const event = {
63
+ target: entry.target,
64
+ contentRect: entry.contentRect,
65
+ // Use `asArray` since Firefox before v92 returns these as objects:
66
+ borderBoxSize: asArray(entry.borderBoxSize),
67
+ contentBoxSize: asArray(entry.contentBoxSize),
68
+ devicePixelContentBoxSize: asArray(entry.devicePixelContentBoxSize)
69
+ }
70
+ for (const handler of handlers) {
71
+ handler(event)
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ static getObserver({ box = 'content-box' } = {}) {
78
+ const options = { box }
79
+ const key = JSON.stringify(options)
80
+ observers[key] ||= new Observer(key, options)
81
+ return observers[key]
82
+ }
83
+ }
@@ -243,7 +243,7 @@ export default {
243
243
  getLabel(schema, name) {
244
244
  return schema
245
245
  ? this.getSchemaValue('label', { type: String, schema }) ||
246
- labelize(name ?? schema.name)
246
+ labelize(name || schema.name)
247
247
  : labelize(name) || ''
248
248
  },
249
249
 
@@ -14,6 +14,7 @@ import CodeFlask from 'codeflask'
14
14
  // @vue/component
15
15
  export default DitoTypeComponent.register('code', {
16
16
  mixins: [DomMixin],
17
+ keepAligned: false,
17
18
 
18
19
  computed: {
19
20
  lines() {
@@ -22,6 +22,7 @@ export default DitoTypeComponent.register('component', {
22
22
  // Override the standard `defaultValue: null` to not set any data for custom
23
23
  // components, unless they provide a default value.
24
24
  defaultValue: () => undefined, // Callback to override `defaultValue: null`
25
+ keepAligned: false,
25
26
  ignoreMissingValue: schema => !('default' in schema),
26
27
 
27
28
  async processSchema(api, schema) {
@@ -11,7 +11,8 @@ import DitoTypeComponent from '../DitoTypeComponent.js'
11
11
  // @vue/component
12
12
  export default DitoTypeComponent.register('label', {
13
13
  excludeValue: true,
14
- generateLabel: false
14
+ generateLabel: false,
15
+ keepAligned: false
15
16
  })
16
17
  </script>
17
18
 
@@ -167,6 +167,7 @@ import { pickBy, equals, hyphenate } from '@ditojs/utils'
167
167
  // @vue/component
168
168
  export default DitoTypeComponent.register('list', {
169
169
  mixins: [SourceMixin, SortableMixin],
170
+ keepAligned: false,
170
171
 
171
172
  getSourceType(type) {
172
173
  // No need for transformation here. See TypeTreeList for details.
@@ -83,6 +83,8 @@ export default DitoTypeComponent.register('markup', {
83
83
  Icon
84
84
  },
85
85
 
86
+ keepAligned: false,
87
+
86
88
  data() {
87
89
  return {
88
90
  editor: null,
@@ -63,6 +63,8 @@ import { resolveSchemaComponent } from '../utils/schema.js'
63
63
  export default DitoTypeComponent.register('object', {
64
64
  mixins: [SourceMixin],
65
65
 
66
+ keepAligned: false,
67
+
66
68
  getSourceType(type) {
67
69
  // No need for transformation here. See TypeTreeList for details.
68
70
  return type
@@ -6,6 +6,7 @@ export default DitoTypeComponent.register('panel', {
6
6
  defaultValue: () => undefined, // Callback to override `defaultValue: null`
7
7
  excludeValue: true,
8
8
  generateLabel: false,
9
+ keepAligned: false,
9
10
  omitPadding: true,
10
11
 
11
12
  getPanelSchema(api, schema) {
@@ -20,6 +20,7 @@ export default DitoTypeComponent.register('section', {
20
20
  ignoreMissingValue: schema => !schema.nested && !('default' in schema),
21
21
  defaultNested: false,
22
22
  generateLabel: false,
23
+ keepAligned: false,
23
24
 
24
25
  computed: {
25
26
  item() {
@@ -19,6 +19,7 @@ export default DitoTypeComponent.register('textarea', {
19
19
  mixins: [TextMixin],
20
20
  nativeField: true,
21
21
  textField: true,
22
+ keepAligned: false,
22
23
 
23
24
  computed: {
24
25
  lines() {
@@ -35,6 +35,8 @@ export default DitoTypeComponent.register(
35
35
  {
36
36
  mixins: [SourceMixin],
37
37
 
38
+ keepAligned: false,
39
+
38
40
  provide() {
39
41
  return { container: this }
40
42
  },
@@ -112,6 +112,8 @@ export default DitoTypeComponent.register('upload', {
112
112
  mixins: [SortableMixin],
113
113
  components: { VueUpload },
114
114
 
115
+ keepAligned: false,
116
+
115
117
  data() {
116
118
  return {
117
119
  uploads: []
@@ -36,6 +36,7 @@ const ditoOptionKeys = [
36
36
  'generateLabel',
37
37
  'excludeValue',
38
38
  'ignoreMissingValue',
39
+ 'keepAligned',
39
40
  'omitPadding',
40
41
  'processValue',
41
42
  'processSchema',
@@ -474,6 +474,10 @@ export function omitPadding(schema) {
474
474
  return !!getTypeOptions(schema)?.omitPadding
475
475
  }
476
476
 
477
+ export function keepAligned(schema) {
478
+ return !!getTypeOptions(schema)?.keepAligned
479
+ }
480
+
477
481
  export function getDefaultValue(schema) {
478
482
  // Support default values both on schema and on component level.
479
483
  // NOTE: At the time of creation, components may not be instantiated, (e.g. if