@ditojs/admin 2.7.5 → 2.8.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.
@@ -1,6 +1,7 @@
1
1
  <template lang="pug">
2
2
  slot(name="before")
3
3
  .dito-schema(
4
+ :class="{ 'dito-scroll-parent': scrollable, 'dito-schema--open': opened }"
4
5
  v-bind="$attrs"
5
6
  )
6
7
  Teleport(
@@ -14,64 +15,62 @@ slot(name="before")
14
15
  :store="store"
15
16
  :disabled="disabled"
16
17
  )
17
- .dito-schema-content(
18
- ref="content"
19
- :class="{ 'dito-scroll': scrollable }"
18
+ Teleport(
19
+ v-if="hasHeader"
20
+ :to="headerTeleport"
21
+ :disabled="!headerTeleport"
20
22
  )
21
- Teleport(
22
- v-if="hasLabel || hasTabs || clipboard"
23
- to=".dito-header__teleport"
24
- :disabled="!headerInMenu"
25
- )
26
- .dito-schema-header(
27
- :class="{ 'dito-schema-header--menu': headerInMenu }"
23
+ .dito-schema-header
24
+ DitoLabel(
25
+ v-if="hasLabel"
26
+ :label="label"
27
+ :dataPath="dataPath"
28
+ :collapsible="collapsible"
29
+ :collapsed="!opened"
30
+ @open="onOpen"
31
+ )
32
+ Transition(
33
+ v-if="tabs"
34
+ name="dito-fade"
28
35
  )
29
- DitoLabel(
30
- v-if="hasLabel"
31
- :label="label"
32
- :dataPath="dataPath"
33
- :collapsible="collapsible"
34
- :collapsed="!opened"
35
- @expand="onExpand"
36
- )
37
- //- Pass edit-buttons through to dito-label's own edit-buttons slot:
38
- template(
39
- v-if="inlined"
40
- #edit-buttons
41
- )
42
- slot(name="edit-buttons")
43
36
  DitoTabs(
44
- v-if="tabs"
37
+ v-if="opened"
38
+ v-model="selectedTab"
45
39
  :tabs="tabs"
46
- :selectedTab="selectedTab"
47
- )
48
- DitoClipboard(
49
- :clipboard="clipboard"
50
- :dataPath="dataPath"
51
- :data="data"
52
40
  )
53
- template(
54
- v-if="hasTabs"
55
- )
56
- DitoPane.dito-pane-tab(
57
- v-for="(tabSchema, tab) in tabs"
58
- ref="tabs"
59
- :key="tab"
60
- :visible="selectedTab === tab"
61
- :tab="tab"
62
- :schema="tabSchema"
41
+ DitoClipboard(
42
+ :clipboard="clipboard"
63
43
  :dataPath="dataPath"
64
44
  :data="data"
65
- :meta="meta"
66
- :store="store"
67
- :single="!inlined && !hasMainPane"
68
- :disabled="disabled"
69
- :generateLabels="generateLabels"
70
- :accumulatedBasis="accumulatedBasis"
71
45
  )
72
- TransitionHeight(:enabled="inlined")
73
- DitoPane.dito-pane-main(
74
- v-if="hasMainPane && opened"
46
+ slot(name="edit-buttons")
47
+ TransitionHeight(:enabled="inlined")
48
+ .dito-schema-content(
49
+ v-if="opened"
50
+ ref="content"
51
+ :class="{ 'dito-scroll': scrollable }"
52
+ )
53
+ template(
54
+ v-if="hasTabs"
55
+ )
56
+ DitoPane.dito-pane__tab(
57
+ v-for="(tabSchema, tab) in tabs"
58
+ v-show="selectedTab === tab"
59
+ ref="tabs"
60
+ :key="tab"
61
+ :tab="tab"
62
+ :schema="tabSchema"
63
+ :dataPath="dataPath"
64
+ :data="data"
65
+ :meta="meta"
66
+ :store="store"
67
+ :single="!inlined && !hasMainPane"
68
+ :disabled="disabled"
69
+ :generateLabels="generateLabels"
70
+ :accumulatedBasis="accumulatedBasis"
71
+ )
72
+ DitoPane.dito-pane__main(
73
+ v-if="hasMainPane"
75
74
  ref="components"
76
75
  :schema="schema"
77
76
  :dataPath="dataPath"
@@ -83,17 +82,14 @@ slot(name="before")
83
82
  :generateLabels="generateLabels"
84
83
  :accumulatedBasis="accumulatedBasis"
85
84
  )
86
- slot(
87
- v-if="!inlined && isPopulated"
88
- name="buttons"
89
- )
90
- template(
91
- v-if="inlined"
85
+ slot(
86
+ v-if="!inlined && isPopulated"
87
+ name="buttons"
88
+ )
89
+ slot(
90
+ v-if="inlined && !hasHeader"
91
+ name="edit-buttons"
92
92
  )
93
- slot(
94
- v-if="!hasLabel"
95
- name="edit-buttons"
96
- )
97
93
  slot(name="after")
98
94
  </template>
99
95
 
@@ -103,6 +99,7 @@ import {
103
99
  isArray,
104
100
  isFunction,
105
101
  isRegExp,
102
+ equals,
106
103
  parseDataPath,
107
104
  normalizeDataPath,
108
105
  labelize
@@ -148,8 +145,8 @@ export default DitoComponent.component('DitoSchema', {
148
145
  collapsible: { type: Boolean, default: false },
149
146
  scrollable: { type: Boolean, default: false },
150
147
  hasOwnData: { type: Boolean, default: false },
151
- headerInMenu: { type: Boolean, default: false },
152
148
  generateLabels: { type: Boolean, default: false },
149
+ labelNode: { type: HTMLElement, default: null },
153
150
  accumulatedBasis: { type: Number, default: 1 }
154
151
  },
155
152
 
@@ -190,25 +187,25 @@ export default DitoComponent.component('DitoSchema', {
190
187
  return getNamedSchemas(this.schema.tabs)
191
188
  },
192
189
 
193
- selectedTab() {
194
- return this.currentTab || this.defaultTab?.name || null
190
+ selectedTab: {
191
+ get() {
192
+ return this.currentTab || this.defaultTab || null
193
+ },
194
+
195
+ set(selectedTab) {
196
+ this.currentTab = selectedTab
197
+ }
195
198
  },
196
199
 
197
200
  defaultTab() {
198
201
  let first = null
199
202
  if (this.tabs) {
200
- for (const tab of Object.values(this.tabs)) {
201
- const { defaultTab } = tab
202
- if (
203
- isFunction(defaultTab)
204
- ? defaultTab(this.context)
205
- : defaultTab
206
- ) {
207
- return tab
208
- }
209
- if (!first) {
210
- first = tab
203
+ const tabs = Object.values(this.tabs).filter(this.shouldRenderSchema)
204
+ for (const { name, defaultTab } of tabs) {
205
+ if (isFunction(defaultTab) ? defaultTab(this.context) : defaultTab) {
206
+ return name
211
207
  }
208
+ first ??= name
212
209
  }
213
210
  }
214
211
  return first
@@ -218,6 +215,16 @@ export default DitoComponent.component('DitoSchema', {
218
215
  return this.schema?.clipboard
219
216
  },
220
217
 
218
+ hasHeader() {
219
+ return this.hasLabel || this.hasTabs || !!this.clipboard
220
+ },
221
+
222
+ headerTeleport() {
223
+ return this.isRootSchema
224
+ ? '.dito-header__teleport'
225
+ : this.labelNode
226
+ },
227
+
221
228
  parentData() {
222
229
  const data = getParentItem(this.rootData, this.dataPath, false)
223
230
  return data !== this.data ? data : null
@@ -266,6 +273,11 @@ export default DitoComponent.component('DitoSchema', {
266
273
  )
267
274
  },
268
275
 
276
+ isRootSchema() {
277
+ // Section schemas can share the root dataPath but they are inlined.
278
+ return this.dataPath === '' && !this.inlined
279
+ },
280
+
269
281
  isDirty() {
270
282
  return this.someComponent(it => it.isDirty)
271
283
  },
@@ -295,7 +307,11 @@ export default DitoComponent.component('DitoSchema', {
295
307
  },
296
308
 
297
309
  hasTabs() {
298
- return !this.inlined && !!this.tabs
310
+ return !!this.tabs
311
+ },
312
+
313
+ hasMainTabs() {
314
+ return this.hasTabs && this.isRootSchema
299
315
  },
300
316
 
301
317
  hasMainPane() {
@@ -348,27 +364,21 @@ export default DitoComponent.component('DitoSchema', {
348
364
  // Remember the current path to know if tab changes should still be
349
365
  // handled, but remove the trailing `/create` or `/:id` from it so that
350
366
  // tabs informs that stay open after creation still work.
351
- if (this.hasTabs) {
367
+ if (this.hasMainTabs) {
352
368
  this.currentTab = hash?.slice(1) || null
353
- if (this.hasErrors) {
354
- this.repositionErrors()
355
- }
356
369
  }
357
370
  }
358
371
  },
359
372
 
360
373
  'selectedTab'(selectedTab) {
361
- if (this.hasTabs) {
362
- let tab = null
363
- if (selectedTab !== this.currentTab) {
364
- // Any tab change needs to be reflected in the router also.
365
- tab = selectedTab
366
- } else if (!this.shouldRenderSchema(this.tabs[selectedTab])) {
367
- tab = this.defaultTab?.name
368
- }
369
- if (tab) {
370
- this.$router.replace({ hash: `#${tab}` })
371
- }
374
+ if (this.hasMainTabs) {
375
+ const tab = this.shouldRenderSchema(this.tabs[selectedTab])
376
+ ? selectedTab
377
+ : this.defaultTab
378
+ this.$router.replace({ hash: tab ? `#${tab}` : null })
379
+ }
380
+ if (this.hasErrors) {
381
+ this.repositionErrors()
372
382
  }
373
383
  }
374
384
  },
@@ -441,11 +451,8 @@ export default DitoComponent.component('DitoSchema', {
441
451
  return this.isPopulated && this.components.every(callback)
442
452
  },
443
453
 
444
- onExpand(expand) {
445
- this.emitEvent('expand', {
446
- // TODO: Actually expose this on DitoContext?
447
- context: { expand }
448
- })
454
+ onOpen(open) {
455
+ this.emitEvent('open', { context: { open } })
449
456
  // Prevent closing the schema with invalid data, since the in-component
450
457
  // validation will not be executed once it's closed.
451
458
 
@@ -453,8 +460,8 @@ export default DitoComponent.component('DitoSchema', {
453
460
  // processing, and use `showValidationErrors()` for the resulting errors,
454
461
  // then remove this requirement, since we can validate closed forms and
455
462
  // schemas then.
456
- if (!this.opened || expand || this.validateAll()) {
457
- this.opened = expand
463
+ if (!this.opened || open || this.validateAll()) {
464
+ this.opened = open
458
465
  }
459
466
  },
460
467
 
@@ -630,9 +637,12 @@ export default DitoComponent.component('DitoSchema', {
630
637
  for (const name in data) {
631
638
  if (name in this.data) {
632
639
  // eslint-disable-next-line vue/no-mutating-props
633
- this.data[name] = data[name]
634
- for (const component of this.getComponentsByName(name)) {
635
- component.markDirty()
640
+ if (!equals(this.data[name], data[name])) {
641
+ // eslint-disable-next-line vue/no-mutating-props
642
+ this.data[name] = data[name]
643
+ for (const component of this.getComponentsByName(name)) {
644
+ component.markDirty()
645
+ }
636
646
  }
637
647
  }
638
648
  }
@@ -733,10 +743,22 @@ export default DitoComponent.component('DitoSchema', {
733
743
 
734
744
  .dito-schema {
735
745
  box-sizing: border-box;
736
- // To display edit buttons next to schema:
737
- display: flex;
738
- align-items: stretch;
739
- min-height: 100%;
746
+
747
+ > .dito-schema-header + .dito-schema-content > .dito-pane {
748
+ margin-top: $form-spacing-half;
749
+ }
750
+
751
+ &:has(> .dito-schema-content + .dito-edit-buttons) {
752
+ // Display the edit buttons to the right of the schema:
753
+ display: flex;
754
+ flex-direction: row;
755
+ align-items: stretch;
756
+
757
+ > .dito-edit-buttons {
758
+ flex: 1 0 0%;
759
+ margin-left: $form-spacing;
760
+ }
761
+ }
740
762
 
741
763
  > .dito-schema-content {
742
764
  flex: 0 1 100%;
@@ -751,37 +773,6 @@ export default DitoComponent.component('DitoSchema', {
751
773
  > *:only-child {
752
774
  grid-row-end: none;
753
775
  }
754
-
755
- &.dito-scroll:has(.dito-pane:last-child)::after {
756
- // Eat up negative margin of the last child to prevent overscroll.
757
- content: '';
758
- }
759
- }
760
-
761
- > .dito-buttons {
762
- flex: 1 1 0%;
763
- }
764
-
765
- > .dito-buttons {
766
- margin-left: $form-spacing;
767
- }
768
-
769
- // Display a ruler between tabbed components and towards the .dito-buttons
770
- .dito-pane-tab + .dito-pane-main {
771
- &::before {
772
- // Use a pseudo element to display a ruler with proper margins
773
- display: block;
774
- content: '';
775
- width: 100%;
776
- border-bottom: $border-style;
777
- // Add removed $form-spacing again to the ruler
778
- margin: $content-padding $form-spacing-half $form-spacing-half;
779
- }
780
- }
781
-
782
- .dito-pane-main + .dito-buttons-main {
783
- // Needed forms with sticky main buttons.
784
- margin-bottom: 0;
785
776
  }
786
777
  }
787
778
 
@@ -789,41 +780,22 @@ export default DitoComponent.component('DitoSchema', {
789
780
  display: flex;
790
781
  justify-content: space-between;
791
782
 
792
- .dito-tabs,
793
- .dito-clipboard {
794
- display: flex;
795
- align-self: flex-end;
783
+ .dito-header & {
784
+ // When teleported into main header.
785
+ align-items: flex-end;
796
786
  }
797
787
 
798
- .dito-clipboard {
799
- &:only-child {
800
- margin-left: auto;
801
- }
788
+ .dito-label & {
789
+ // When teleported into container label.
790
+ flex: 1;
791
+ }
802
792
 
803
- .dito-button {
804
- margin: 0 0 $tab-margin $tab-margin;
805
- }
793
+ > .dito-label {
794
+ margin-bottom: 0;
806
795
  }
807
796
 
808
- &--menu {
809
- // Align the tabs on top of to the header menu.
810
- position: absolute;
811
- height: $header-height;
812
- padding: 0 $header-padding-hor;
813
- top: 0;
814
- left: 0;
815
- right: 0;
816
- z-index: $z-index-header;
817
- // Turn off pointer events so that DitoTrail keeps receiving events...
818
- pointer-events: none;
819
- // ...but allow interaction with the tabs and buttons (e.g. clipboard)
820
- // layered on top of DitoTrail.
821
- .dito-tabs,
822
- .dito-buttons {
823
- pointer-events: auto;
824
- line-height: $header-line-height;
825
- font-size: $header-font-size;
826
- }
797
+ > .dito-buttons {
798
+ margin-left: var(--button-margin, 0);
827
799
  }
828
800
  }
829
801
  </style>
@@ -12,6 +12,7 @@ DitoSchema.dito-schema-inlined(
12
12
  :collapsed="collapsed"
13
13
  :collapsible="collapsible"
14
14
  :generateLabels="!isCompact"
15
+ :labelNode="labelNode"
15
16
  :accumulatedBasis="accumulatedBasis"
16
17
  )
17
18
  //- Render dito-edit-buttons for inlined schemas separately from all
@@ -54,6 +55,7 @@ export default DitoComponent.component('DitoSchemaInlined', {
54
55
  editable: { type: Boolean, default: false },
55
56
  deletable: { type: Boolean, default: false },
56
57
  editPath: { type: String, default: null },
58
+ labelNode: { type: HTMLElement, default: null },
57
59
  accumulatedBasis: { type: Number, default: null }
58
60
  },
59
61
 
@@ -73,25 +75,26 @@ export default DitoComponent.component('DitoSchemaInlined', {
73
75
  @import '../styles/_imports';
74
76
 
75
77
  .dito-schema-inlined {
78
+ // Use grid layout for two reasons: For `TransitionHeight` to work smoothly,
79
+ // and to align the header above the content when the header is not teleported
80
+ // outside of the schema.
81
+ display: grid;
82
+ grid-template-rows: min-content;
83
+ grid-template-columns: 100%;
84
+
76
85
  > .dito-schema-content {
77
86
  > .dito-schema-header {
78
- // Change spacing so .dito-label covers the full .dito-schema-header.
79
- margin: -$form-spacing;
87
+ justify-content: space-between;
88
+ position: relative;
80
89
 
81
90
  .dito-label {
82
- // Add removed $form-spacing again.
83
- margin: $form-spacing;
84
91
  width: 100%;
85
- box-sizing: content-box;
92
+ margin: 0;
86
93
  // Prevent collapsing to min-height when alone in
87
94
  // .dito-schema-content, due to grid-template-rows: min-content
88
95
  min-height: $input-height;
89
96
  }
90
97
  }
91
-
92
- > .dito-pane {
93
- padding: 0;
94
- }
95
98
  }
96
99
  }
97
100
  </style>
@@ -3,11 +3,11 @@
3
3
  template(
4
4
  v-for="(tabSchema, key) in tabs"
5
5
  )
6
- RouterLink.dito-link(
6
+ a.dito-link(
7
7
  v-if="shouldRenderSchema(tabSchema)"
8
8
  :key="key"
9
- :to="{ hash: `#${key}` }"
10
- :class="{ 'dito-active': selectedTab === key }"
9
+ :class="{ 'dito-active': modelValue === key }"
10
+ @click="$emit('update:modelValue', key)"
11
11
  ) {{ getLabel(tabSchema, key) }}
12
12
  </template>
13
13
 
@@ -16,45 +16,87 @@ import DitoComponent from '../DitoComponent.js'
16
16
 
17
17
  // @vue/component
18
18
  export default DitoComponent.component('DitoTabs', {
19
+ emits: ['update:modelValue'],
19
20
  props: {
20
21
  tabs: { type: Object, default: null },
21
- selectedTab: { type: String, default: null }
22
+ modelValue: { type: String, default: null }
22
23
  }
23
24
  })
24
25
  </script>
25
26
 
26
27
  <style lang="scss">
28
+ @use 'sass:color';
27
29
  @import '../styles/_imports';
28
30
 
29
- $tab-color-background: $color-lightest;
30
- $tab-color-inactive: $color-light;
31
- $tab-color-active: $color-lightest;
32
- $tab-color-hover: $color-white;
33
-
34
31
  .dito-tabs {
35
- // See: https://codepen.io/tholex/pen/hveBx/
36
- margin-left: auto;
32
+ display: flex;
37
33
 
38
- a {
39
- display: block;
34
+ .dito-link {
40
35
  @include user-select(none);
41
36
 
42
- background: $tab-color-inactive;
43
- padding: $tab-padding-ver $tab-padding-hor;
44
- margin-left: $tab-margin;
45
- border-top-left-radius: $tab-radius;
46
- border-top-right-radius: $tab-radius;
37
+ display: block;
38
+ white-space: nowrap;
47
39
 
48
40
  &:hover {
49
- background: $tab-color-hover;
41
+ background: $color-white;
50
42
  }
51
43
 
52
- &:active {
53
- background: $tab-color-active;
44
+ // When in main header:
45
+ .dito-header & {
46
+ background: $color-light;
47
+ padding: $tab-padding-ver $tab-padding-hor;
48
+ margin-left: $tab-margin;
49
+ border-top-left-radius: $tab-radius;
50
+ border-top-right-radius: $tab-radius;
51
+
52
+ &:active {
53
+ background: $color-lightest;
54
+ }
55
+
56
+ &.dito-active {
57
+ background: $color-lightest;
58
+ }
54
59
  }
55
60
 
56
- &.dito-active {
57
- background: $tab-color-background;
61
+ // When inside a inline schema:
62
+ .dito-schema-inlined &,
63
+ .dito-label & {
64
+ background: $color-lighter;
65
+ border: $border-style;
66
+ padding: $input-padding;
67
+ margin-left: -$border-width;
68
+ white-space: nowrap;
69
+
70
+ &:first-child {
71
+ border-top-left-radius: $tab-radius;
72
+ border-bottom-left-radius: $tab-radius;
73
+ }
74
+
75
+ &:last-child {
76
+ border-top-right-radius: $tab-radius;
77
+ border-bottom-right-radius: $tab-radius;
78
+ }
79
+
80
+ &:active {
81
+ background: $color-lighter;
82
+ }
83
+
84
+ &.dito-active {
85
+ background: $color-active;
86
+ border-color: color.adjust($color-active, $lightness: -10%);
87
+ color: $color-white;
88
+ z-index: 1;
89
+ }
90
+ }
91
+ }
92
+
93
+ .dito-schema & {
94
+ // Push clipboard to the right in the flex layout, see:
95
+ // https://codepen.io/tholex/pen/hveBx/
96
+ margin-left: auto;
97
+
98
+ &:last-child {
99
+ margin-right: auto;
58
100
  }
59
101
  }
60
102
  }
@@ -16,7 +16,7 @@
16
16
  )
17
17
  .dito-chevron(
18
18
  v-if="numEntries"
19
- :class="{ 'dito-opened': opened }"
19
+ :class="{ 'dito-open': opened }"
20
20
  )
21
21
  .dito-tree-label(
22
22
  v-html="label"
@@ -22,7 +22,6 @@ template(
22
22
  :store="getChildStore(name)"
23
23
  :disabled="isLoading"
24
24
  scrollable
25
- headerInMenu
26
25
  )
27
26
  </template>
28
27
 
@@ -52,6 +52,7 @@ export default {
52
52
  data() {
53
53
  return {
54
54
  appState,
55
+ isMounted: false,
55
56
  overrides: null // See accessor.js
56
57
  }
57
58
  },
@@ -171,13 +172,13 @@ export default {
171
172
  }
172
173
  },
173
174
 
175
+ mounted() {
176
+ this.isMounted = true
177
+ },
178
+
174
179
  beforeCreate() {
175
180
  const uid = nextUid++
176
- Object.defineProperty(this, '$uid', {
177
- get() {
178
- return uid
179
- }
180
- })
181
+ Object.defineProperty(this, '$uid', { get: () => uid })
181
182
  },
182
183
 
183
184
  methods: {
@@ -134,7 +134,11 @@ export default {
134
134
  text = `${formLabel} ${hadLabel ? `'${text}'` : text}`
135
135
  }
136
136
  }
137
- return asObject ? { text, prefix, suffix } : text
137
+ return asObject
138
+ ? text || prefix || suffix
139
+ ? { text, prefix, suffix }
140
+ : null
141
+ : text
138
142
  }
139
143
  }
140
144
  }
@@ -118,6 +118,11 @@ export default {
118
118
  return this.processedData
119
119
  },
120
120
 
121
+ labelNode() {
122
+ const node = this.isMounted ? this.$el.previousElementSibling : null
123
+ return node?.matches('.dito-label') ? node : null
124
+ },
125
+
121
126
  visible: getSchemaAccessor('visible', {
122
127
  type: Boolean,
123
128
  default() {