@ditojs/admin 3.2.6 → 3.3.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.
Files changed (112) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +6012 -5827
  3. package/dist/dito-admin.umd.js +3 -7
  4. package/package.json +18 -17
  5. package/src/DitoAdmin.js +44 -24
  6. package/src/DitoContext.js +28 -2
  7. package/src/DitoTypeComponent.js +1 -0
  8. package/src/appState.js +6 -3
  9. package/src/components/DitoAccount.vue +35 -27
  10. package/src/components/DitoAffixes.vue +9 -2
  11. package/src/components/DitoClipboard.vue +59 -93
  12. package/src/components/DitoConfirmMessage.vue +18 -0
  13. package/src/components/DitoContainer.vue +56 -18
  14. package/src/components/DitoCreateButton.vue +36 -84
  15. package/src/components/DitoDialog.vue +20 -15
  16. package/src/components/DitoDragHandle.vue +44 -0
  17. package/src/components/DitoDraggable.vue +43 -16
  18. package/src/components/DitoEditButtons.vue +48 -24
  19. package/src/components/DitoErrors.vue +9 -56
  20. package/src/components/DitoForm.vue +82 -42
  21. package/src/components/DitoInputAffixes.vue +50 -0
  22. package/src/components/DitoLabel.vue +1 -15
  23. package/src/components/DitoMenu.vue +45 -52
  24. package/src/components/DitoNotifications.vue +38 -14
  25. package/src/components/DitoOptionList.vue +81 -0
  26. package/src/components/DitoOptionsEditButtons.vue +57 -0
  27. package/src/components/DitoPagination.vue +1 -8
  28. package/src/components/DitoPane.vue +51 -39
  29. package/src/components/DitoPanel.vue +12 -26
  30. package/src/components/DitoPanels.vue +11 -5
  31. package/src/components/DitoResizeHandle.vue +47 -0
  32. package/src/components/DitoRoot.vue +113 -320
  33. package/src/components/DitoSchema.vue +72 -63
  34. package/src/components/DitoSchemaInlined.vue +8 -9
  35. package/src/components/DitoScopes.vue +14 -18
  36. package/src/components/DitoSidebar.vue +4 -29
  37. package/src/components/DitoSourceItem.vue +114 -0
  38. package/src/components/DitoTableCell.vue +4 -0
  39. package/src/components/DitoTableHead.vue +15 -17
  40. package/src/components/DitoTabs.vue +25 -0
  41. package/src/components/DitoTrail.vue +25 -25
  42. package/src/components/DitoTreeItem.vue +102 -141
  43. package/src/components/DitoUploadFile.vue +5 -2
  44. package/src/components/DitoView.vue +33 -24
  45. package/src/index.js +4 -1
  46. package/src/mixins/DitoMixin.js +77 -110
  47. package/src/mixins/DomMixin.js +12 -14
  48. package/src/mixins/EmitterMixin.js +3 -2
  49. package/src/mixins/ItemMixin.js +38 -94
  50. package/src/mixins/OptionsMixin.js +38 -25
  51. package/src/mixins/ResourceMixin.js +182 -111
  52. package/src/mixins/RouteMixin.js +61 -44
  53. package/src/mixins/SchemaParentMixin.js +4 -1
  54. package/src/mixins/SourceMixin.js +118 -100
  55. package/src/mixins/TypeMixin.js +42 -40
  56. package/src/mixins/ValidationMixin.js +16 -4
  57. package/src/mixins/ValidatorMixin.js +1 -1
  58. package/src/styles/_settings.scss +4 -0
  59. package/src/styles/_tippy.scss +0 -11
  60. package/src/styles/style.scss +0 -1
  61. package/src/types/DitoTypeButton.vue +11 -14
  62. package/src/types/DitoTypeCheckboxes.vue +35 -17
  63. package/src/types/DitoTypeCode.vue +19 -41
  64. package/src/types/DitoTypeColor.vue +67 -19
  65. package/src/types/DitoTypeDate.vue +11 -14
  66. package/src/types/DitoTypeList.vue +50 -62
  67. package/src/types/DitoTypeMarkup.vue +41 -54
  68. package/src/types/DitoTypeMultiselect.vue +82 -52
  69. package/src/types/DitoTypeNumber.vue +12 -15
  70. package/src/types/DitoTypeObject.vue +4 -38
  71. package/src/types/DitoTypeRadio.vue +19 -6
  72. package/src/types/DitoTypeSection.vue +23 -9
  73. package/src/types/DitoTypeSelect.vue +39 -23
  74. package/src/types/DitoTypeSlider.vue +9 -1
  75. package/src/types/DitoTypeSwitch.vue +1 -1
  76. package/src/types/DitoTypeText.vue +11 -14
  77. package/src/types/DitoTypeTreeList.vue +46 -8
  78. package/src/types/DitoTypeUpload.vue +190 -67
  79. package/src/utils/DataModel.js +368 -146
  80. package/src/utils/FileDropTargets.js +97 -0
  81. package/src/utils/ItemStores.js +31 -0
  82. package/src/utils/ListQuery.js +115 -0
  83. package/src/utils/LoadingTracker.js +76 -0
  84. package/src/utils/SchemaGraph.js +3 -2
  85. package/src/utils/Session.js +151 -0
  86. package/src/utils/ViewRegistry.js +85 -0
  87. package/src/utils/accessor.js +12 -4
  88. package/src/utils/affixes.js +33 -0
  89. package/src/utils/changeOnceEdited.js +33 -0
  90. package/src/utils/clipboard.js +55 -0
  91. package/src/utils/data.js +10 -5
  92. package/src/utils/dialogs.js +59 -0
  93. package/src/utils/dragResize.js +61 -0
  94. package/src/utils/filter.js +36 -2
  95. package/src/utils/html.js +9 -0
  96. package/src/utils/list.js +10 -0
  97. package/src/utils/options.js +4 -0
  98. package/src/utils/registration.js +18 -0
  99. package/src/utils/request.js +108 -13
  100. package/src/utils/route.js +6 -17
  101. package/src/utils/schema/data.js +29 -2
  102. package/src/utils/schema/events.js +19 -0
  103. package/src/utils/schema/item.js +114 -0
  104. package/src/utils/schema/lookup.js +39 -14
  105. package/src/utils/schema/setup.js +5 -5
  106. package/src/utils/schema/validation.js +16 -2
  107. package/src/utils/uid.js +105 -8
  108. package/src/validators/_decimals.js +16 -5
  109. package/types/index.d.ts +168 -41
  110. package/src/mixins/LoadingMixin.js +0 -23
  111. package/src/mixins/PulldownMixin.js +0 -72
  112. package/src/styles/_pulldown.scss +0 -38
@@ -18,8 +18,8 @@ slot(name="prepend")
18
18
  )
19
19
  Teleport(
20
20
  v-if="hasHeader"
21
- :to="headerTeleport"
22
- :disabled="!headerTeleport"
21
+ to=".dito-header__teleport"
22
+ :disabled="!isTopLevelSchema"
23
23
  )
24
24
  .dito-schema-header(
25
25
  v-if="active"
@@ -38,13 +38,17 @@ slot(name="prepend")
38
38
  )
39
39
  DitoTabs(
40
40
  v-if="opened"
41
- v-model="selectedTab"
41
+ :modelValue="selectedTab"
42
42
  :tabs="renderedTabs"
43
+ @update:modelValue="selectTab"
43
44
  )
44
45
  DitoClipboard(
45
46
  v-if="clipboard"
46
47
  :clipboard="clipboard"
47
48
  :schema="schema"
49
+ :hasData="hasData"
50
+ :getData="getDataForClipboard"
51
+ @paste="setData"
48
52
  )
49
53
  slot(name="edit-buttons")
50
54
  TransitionHeight(:enabled="inlined")
@@ -65,7 +69,7 @@ slot(name="prepend")
65
69
  //- of their components). `navigateToComponent()` then needs to
66
70
  //- select the tab that displays the component.
67
71
  DitoPane.dito-pane__tab(
68
- v-show="selectedTab === tab"
72
+ v-show="isTabSelected(tab)"
69
73
  ref="tabs"
70
74
  :tab="tab"
71
75
  :schema="tabSchema"
@@ -78,7 +82,6 @@ slot(name="prepend")
78
82
  :disabled="disabled"
79
83
  :compact="compact"
80
84
  :generateLabels="generateLabels"
81
- :accumulatedBasis="accumulatedBasis"
82
85
  )
83
86
  DitoPane.dito-pane__main(
84
87
  v-if="hasMainPane"
@@ -93,7 +96,6 @@ slot(name="prepend")
93
96
  :disabled="disabled"
94
97
  :compact="compact"
95
98
  :generateLabels="generateLabels"
96
- :accumulatedBasis="accumulatedBasis"
97
99
  )
98
100
  slot(
99
101
  v-if="!inlined && isPopulated"
@@ -118,13 +120,14 @@ import {
118
120
  } from '@ditojs/utils'
119
121
  import { TransitionHeight } from '@ditojs/ui/src'
120
122
  import DitoComponent from '../DitoComponent.js'
123
+ import DitoContext from '../DitoContext.js'
121
124
  import ContextMixin from '../mixins/ContextMixin.js'
122
- import ItemMixin from '../mixins/ItemMixin.js'
123
125
  import { appendDataPath } from '../utils/data.js'
124
126
  import { isPanel } from '../utils/schema/structure.js'
125
127
  import {
126
128
  getNamedSchemas,
127
129
  getPanelEntries,
130
+ getItemFormSchema,
128
131
  isEmptySchema
129
132
  } from '../utils/schema/lookup.js'
130
133
  import { initializeData, processData } from '../utils/schema/data.js'
@@ -136,13 +139,17 @@ import { getSchemaAccessor, getStoreAccessor } from '../utils/accessor.js'
136
139
 
137
140
  // @vue/component
138
141
  export default DitoComponent.component('DitoSchema', {
139
- mixins: [ContextMixin, ItemMixin],
142
+ mixins: [ContextMixin],
140
143
  components: { TransitionHeight },
141
144
  inheritAttrs: false,
142
145
 
143
146
  provide() {
144
147
  return {
145
- $schemaComponent: () => this
148
+ $schemaComponent: () => this,
149
+ // Inlined schemas share the width of their container, see
150
+ // `DitoContainer.combinedBasis`, while all others take the full width,
151
+ // e.g. panels in the sidebar.
152
+ ...(!this.inlined && { $accumulatedBasis: () => 1 })
146
153
  }
147
154
  },
148
155
 
@@ -171,21 +178,25 @@ export default DitoComponent.component('DitoSchema', {
171
178
  collapsible: { type: Boolean, default: false },
172
179
  scrollable: { type: Boolean, default: false },
173
180
  hasOwnData: { type: Boolean, default: false },
174
- generateLabels: { type: Boolean, default: false },
175
- labelNode: { type: HTMLElement, default: null },
176
- accumulatedBasis: { type: Number, default: 1 }
181
+ generateLabels: { type: Boolean, default: false }
177
182
  },
178
183
 
179
184
  data() {
180
185
  const { data } = this.schema
181
186
  return {
182
- // Allow schema to provide more data through `schema.data`, vue-style:
187
+ // Allow schema to provide more data through `schema.data`, vue-style.
188
+ // `data()` runs before the computed properties are set up, so create the
189
+ // context here rather than using the `context` computed property:
183
190
  ...(
184
191
  data && isFunction(data)
185
- ? data(this.context)
192
+ ? data(new DitoContext(this, { nested: false }))
186
193
  : data
187
194
  ),
188
195
  selectedTab: null,
196
+ // Whether the selected tab still follows `defaultTab`, until the route
197
+ // or the user select a tab, or until the data that `defaultTab()` and
198
+ // the tabs' `if` are evaluated with is there, see `selectDefaultTab()`.
199
+ shouldFollowDefaultTab: true,
189
200
  componentsRegistry: {},
190
201
  unnestedComponentsRegistry: {},
191
202
  panesRegistry: {},
@@ -213,10 +224,13 @@ export default DitoComponent.component('DitoSchema', {
213
224
  },
214
225
 
215
226
  panelEntries() {
227
+ // Panels of schemas inlined in tabs are only shown with their tab, as
228
+ // the panels of components in tabs, see `DitoContainer.panelEntries`.
216
229
  return getPanelEntries(
217
230
  this.schema.panels,
218
231
  this.dataPath,
219
- this.componentPath
232
+ this.componentPath,
233
+ this.tabComponent?.panelTab ?? null
220
234
  )
221
235
  },
222
236
 
@@ -262,34 +276,14 @@ export default DitoComponent.component('DitoSchema', {
262
276
  return this.hasLabel || this.hasTabs || !!this.clipboard
263
277
  },
264
278
 
265
- headerTeleport() {
266
- return this.isTopLevelSchema
267
- ? '.dito-header__teleport'
268
- : this.labelNode
269
- },
270
-
271
279
  // @override
272
280
  processedData() {
273
281
  return this.processData({ target: 'server', schemaOnly: true })
274
282
  },
275
283
 
276
- clipboardData: {
277
- get() {
278
- return this.processData({ target: 'clipboard', schemaOnly: true })
279
- },
280
-
281
- set(data) {
282
- this.setData(data)
283
- }
284
- },
285
-
286
- clipboardItem() {
287
- return this.clipboardData
288
- },
289
-
290
284
  formLabel() {
291
285
  return this.getLabel(
292
- this.getItemFormSchema(this.sourceSchema, this.data, this.context)
286
+ getItemFormSchema(this.sourceSchema, this.data, this.context)
293
287
  )
294
288
  },
295
289
 
@@ -396,15 +390,22 @@ export default DitoComponent.component('DitoSchema', {
396
390
  // https://github.com/vuejs/vue-router/issues/3393#issuecomment-1158470149
397
391
  flush: 'post',
398
392
  handler(routeTab) {
399
- // Remember the current path to know if tab changes should still be
400
- // handled, but remove the trailing `/create` or `/:id` from it so that
401
- // tabs informs that stay open after creation still work.
402
- if (this.hasTopLevelTabs) {
403
- this.selectedTab = routeTab
393
+ // Changes of the hash to the selected tab are the route following the
394
+ // selection, see `selectedTab()`, not a selection by the route.
395
+ if (this.hasTopLevelTabs && routeTab !== this.selectedTab) {
396
+ this.selectTab(routeTab)
404
397
  }
405
398
  }
406
399
  },
407
400
 
401
+ hasData(hasData) {
402
+ // `defaultTab()` and the tabs' `if` may depend on data that is only
403
+ // available after mounting, e.g. the item of a form once it's loaded.
404
+ if (hasData && this.shouldFollowDefaultTab) {
405
+ this.selectDefaultTab()
406
+ }
407
+ },
408
+
408
409
  selectedTab(newTab, oldTab) {
409
410
  if (this.scrollable) {
410
411
  const { content } = this.$refs
@@ -424,9 +425,6 @@ export default DitoComponent.component('DitoSchema', {
424
425
  hash: tab ? `#${tab}` : null
425
426
  })
426
427
  }
427
- if (this.hasErrors) {
428
- this.repositionErrors()
429
- }
430
428
  }
431
429
  },
432
430
 
@@ -438,7 +436,11 @@ export default DitoComponent.component('DitoSchema', {
438
436
  },
439
437
 
440
438
  mounted() {
441
- this.selectedTab = this.routeTab || this.defaultTab
439
+ if (this.routeTab) {
440
+ this.selectTab(this.routeTab)
441
+ } else {
442
+ this.selectDefaultTab()
443
+ }
442
444
  },
443
445
 
444
446
  unmounted() {
@@ -505,6 +507,23 @@ export default DitoComponent.component('DitoSchema', {
505
507
  return this.isPopulated && this.components.every(callback)
506
508
  },
507
509
 
510
+ // Selects the tab chosen by the route or the user.
511
+ selectTab(tab) {
512
+ this.selectedTab = tab
513
+ this.shouldFollowDefaultTab = false
514
+ },
515
+
516
+ // Selects `defaultTab`, and keeps following it until there is data, but
517
+ // not after that, so that editing the data doesn't switch the tab.
518
+ selectDefaultTab() {
519
+ this.selectedTab = this.defaultTab
520
+ this.shouldFollowDefaultTab = !this.hasData
521
+ },
522
+
523
+ isTabSelected(tab) {
524
+ return this.selectedTab === tab
525
+ },
526
+
508
527
  onOpen(open) {
509
528
  this.emitEvent('open', { context: { open } })
510
529
  this.opened = open
@@ -526,17 +545,6 @@ export default DitoComponent.component('DitoSchema', {
526
545
  }
527
546
  },
528
547
 
529
- repositionErrors() {
530
- // Fire a fake scroll event to force the repositioning of error tooltips,
531
- // as otherwise they sometimes don't show up in the right place initially
532
- // when changing tabs.
533
- const scrollContainer = this.$refs.content.closest('.dito-scroll')
534
- const dispatch = () => scrollContainer.dispatchEvent(new Event('scroll'))
535
- dispatch()
536
- // This is required to handle `&--label-vertical` based layout changes.
537
- setTimeout(dispatch, 0)
538
- },
539
-
540
548
  focus() {
541
549
  this.opened = true
542
550
  return this.parentSchemaComponent?.focus()
@@ -797,6 +805,12 @@ export default DitoComponent.component('DitoSchema', {
797
805
  return { localData, foreignData }
798
806
  },
799
807
 
808
+ // Called by `DitoClipboard` only when copying, as processing all data on
809
+ // each change would be wasteful.
810
+ getDataForClipboard() {
811
+ return this.processData({ target: 'clipboard', schemaOnly: true })
812
+ },
813
+
800
814
  processData({ target = 'clipboard', schemaOnly = true } = {}) {
801
815
  return processData(
802
816
  this.dataSchema,
@@ -856,8 +870,8 @@ export default DitoComponent.component('DitoSchema', {
856
870
 
857
871
  // Registries are keyed by component path. Component paths change when list
858
872
  // items move, and the entries then register again under their new path,
859
- // see `TypeMixin`, `DitoPane` and `DitoPanel`.
860
- _registerEntry(registry, entry, add, componentPath = entry.componentPath) {
873
+ // see `trackRegistration()`.
874
+ _registerEntry(registry, entry, add, componentPath) {
861
875
  if (add) {
862
876
  registry[componentPath] = entry
863
877
  } else if (registry[componentPath] === entry) {
@@ -966,11 +980,6 @@ function compareByDocumentPosition(component1, component2) {
966
980
  align-items: flex-end;
967
981
  }
968
982
 
969
- .dito-label & {
970
- // When teleported into container label.
971
- flex: 1;
972
- }
973
-
974
983
  > .dito-label {
975
984
  margin-bottom: 0;
976
985
  }
@@ -14,15 +14,16 @@ DitoSchema.dito-schema-inlined(
14
14
  :collapsed="collapsed"
15
15
  :collapsible="collapsible"
16
16
  :generateLabels="!isCompact"
17
- :labelNode="labelNode"
18
- :accumulatedBasis="accumulatedBasis"
19
17
  )
20
18
  //- Render dito-edit-buttons for inlined schemas separately from all
21
19
  //- others in `TypeList` as a scope, for better handling of layout.
20
+ //- `sourceSchema` is the schema of the list or object that holds the item,
21
+ //- passed through `meta.schema`, while `schema` is the item's form.
22
22
  template(#edit-buttons)
23
23
  DitoEditButtons(
24
24
  v-if="deletable || draggable || editable || insertIndex != null"
25
- :schema="schema"
25
+ :schema="sourceSchema"
26
+ :formSchema="schema"
26
27
  :dataPath="dataPath"
27
28
  :data="data"
28
29
  :meta="meta"
@@ -36,6 +37,7 @@ DitoSchema.dito-schema-inlined(
36
37
  :createPath="createPath"
37
38
  :insertIndex="insertIndex"
38
39
  @delete="$emit('delete')"
40
+ @move="delta => $emit('move', delta)"
39
41
  )
40
42
  </template>
41
43
 
@@ -44,7 +46,7 @@ import DitoComponent from '../DitoComponent.js'
44
46
  import { isCompact } from '../utils/schema/structure.js'
45
47
  // @vue/component
46
48
  export default DitoComponent.component('DitoSchemaInlined', {
47
- emits: ['delete'],
49
+ emits: ['delete', 'move'],
48
50
 
49
51
  props: {
50
52
  schema: { type: Object, required: true },
@@ -63,9 +65,7 @@ export default DitoComponent.component('DitoSchemaInlined', {
63
65
  deletable: { type: Boolean, default: false },
64
66
  editPath: { type: String, default: null },
65
67
  createPath: { type: String, default: null },
66
- insertIndex: { type: Number, default: null },
67
- labelNode: { type: HTMLElement, default: null },
68
- accumulatedBasis: { type: Number, default: null }
68
+ insertIndex: { type: Number, default: null }
69
69
  },
70
70
 
71
71
  computed: {
@@ -85,8 +85,7 @@ export default DitoComponent.component('DitoSchemaInlined', {
85
85
 
86
86
  .dito-schema-inlined {
87
87
  // Use grid layout for two reasons: For `TransitionHeight` to work smoothly,
88
- // and to align the header above the content when the header is not teleported
89
- // outside of the schema.
88
+ // and to align the header above the content.
90
89
  display: grid;
91
90
  grid-template-rows: min-content;
92
91
  grid-template-columns: 100%;
@@ -1,38 +1,34 @@
1
1
  <template lang="pug">
2
2
  .dito-scopes
3
- RouterLink(
3
+ DitoButton(
4
4
  v-for="(scope, key) in scopes"
5
5
  :key="key"
6
- v-slot="{ navigate }"
7
- :to="getScopeLink(scope)"
8
- custom
9
- )
10
- button.dito-button(
11
- type="button"
12
- :class="{ 'dito-button--selected': scope.name === query.scope }"
13
- :aria-pressed="scope.name === query.scope"
14
- :title="scope.hint || getLabel(scope)"
15
- @click="navigate"
16
- ) {{ getLabel(scope) }}
6
+ :class="{ 'dito-button--selected': scope.name === query.scope }"
7
+ :aria-pressed="scope.name === query.scope"
8
+ :title="scope.hint || getLabel(scope)"
9
+ @click="selectScope(scope)"
10
+ ) {{ getLabel(scope) }}
17
11
  </template>
18
12
 
19
13
  <script>
20
14
  import DitoComponent from '../DitoComponent.js'
15
+ import { DitoButton } from '@ditojs/ui/src'
21
16
 
22
17
  // @vue/component
23
18
  export default DitoComponent.component('DitoScopes', {
19
+ components: { DitoButton },
20
+
24
21
  props: {
25
22
  query: { type: Object, required: true },
26
23
  scopes: { type: Object, required: true }
27
24
  },
28
25
 
29
26
  methods: {
30
- getScopeLink(scope) {
31
- const query = { ...this.query, scope: scope.name }
32
- if (query.page) {
33
- query.page = 0
34
- }
35
- return this.getQueryLink(query)
27
+ selectScope(scope) {
28
+ this.sourceComponent.listQuery.update(
29
+ { scope: scope.name },
30
+ { resetPage: true }
31
+ )
36
32
  }
37
33
  }
38
34
  })
@@ -1,8 +1,5 @@
1
1
  <template lang="pug">
2
- aside.dito-sidebar.dito-scroll-parent(
3
- v-resize="onResizeSidebar"
4
- :class="classes"
5
- )
2
+ aside.dito-sidebar.dito-scroll-parent
6
3
  nav.dito-header
7
4
  slot
8
5
  .dito-sidebar__teleport.dito-scroll
@@ -12,37 +9,15 @@ aside.dito-sidebar.dito-scroll-parent(
12
9
  import DitoComponent from '../DitoComponent.js'
13
10
 
14
11
  // @vue/component
15
- export default DitoComponent.component('DitoSidebar', {
16
- data() {
17
- return {
18
- sidebarWidth: 0
19
- }
20
- },
21
-
22
- computed: {
23
- classes() {
24
- const prefix = 'dito-sidebar'
25
- // NOTE: Keep synced with $sidebar-max-width in SCSS:
26
- const sidebarWidth = 360
27
- return {
28
- [`${prefix}--width-99`]: this.sidebarWidth < sidebarWidth,
29
- [`${prefix}--width-60`]: this.sidebarWidth <= sidebarWidth * 0.6
30
- }
31
- }
32
- },
33
-
34
- methods: {
35
- onResizeSidebar({ contentRect: { width } }) {
36
- this.sidebarWidth = width
37
- }
38
- }
39
- })
12
+ export default DitoComponent.component('DitoSidebar', {})
40
13
  </script>
41
14
 
42
15
  <style lang="scss">
43
16
  @import '../styles/_imports';
44
17
 
45
18
  .dito-sidebar {
19
+ // For the layout of `DitoContainer` in narrow sidebars:
20
+ container: dito-sidebar / inline-size;
46
21
  flex: 0 1 $sidebar-max-width;
47
22
  max-width: $sidebar-max-width;
48
23
  min-width: $sidebar-min-width;
@@ -0,0 +1,114 @@
1
+ <template lang="pug">
2
+ //- Renders an item of a list or object source: Inlined items render their
3
+ //- form, others the source's `component`, `render()` or the item's label.
4
+ DitoSchemaInlined(
5
+ v-if="isInlined"
6
+ :label="inlinedLabel"
7
+ :schema="formSchema"
8
+ :dataPath="dataPath"
9
+ :data="data"
10
+ :meta="meta"
11
+ :store="store"
12
+ :disabled="disabled"
13
+ :collapsed="collapsed"
14
+ :collapsible="collapsible"
15
+ :deletable="deletable"
16
+ :draggable="draggable"
17
+ :editable="editable"
18
+ :editPath="editPath"
19
+ :createPath="createPath"
20
+ :insertIndex="insertIndex"
21
+ @delete="$emit('delete')"
22
+ @move="delta => $emit('move', delta)"
23
+ )
24
+ component(
25
+ v-else-if="schema.component"
26
+ :is="schema.component"
27
+ :dataPath="dataPath"
28
+ :data="data"
29
+ :nested="false"
30
+ )
31
+ span(
32
+ v-else-if="sourceComponent.render"
33
+ v-html="renderedItem"
34
+ )
35
+ span(
36
+ v-else
37
+ v-html="itemLabel"
38
+ )
39
+ </template>
40
+
41
+ <script>
42
+ import DitoComponent from '../DitoComponent.js'
43
+ import DitoContext from '../DitoContext.js'
44
+ import { isInlined, isListSource } from '../utils/schema/structure.js'
45
+ import { getItemFormSchema } from '../utils/schema/lookup.js'
46
+
47
+ // @vue/component
48
+ export default DitoComponent.component('DitoSourceItem', {
49
+ emits: ['delete', 'move'],
50
+
51
+ props: {
52
+ // The schema of the list or object source that holds the item.
53
+ schema: { type: Object, required: true },
54
+ dataPath: { type: String, required: true },
55
+ data: { type: Object, required: true },
56
+ // The index of the item in a list source, `null` in object sources.
57
+ index: { type: Number, default: null },
58
+ meta: { type: Object, required: true },
59
+ store: { type: Object, required: true },
60
+ disabled: { type: Boolean, required: true },
61
+ // The remaining props only apply to inlined items, see `DitoSchemaInlined`:
62
+ collapsed: { type: Boolean, default: false },
63
+ collapsible: { type: Boolean, default: false },
64
+ draggable: { type: Boolean, default: false },
65
+ editable: { type: Boolean, default: false },
66
+ deletable: { type: Boolean, default: false },
67
+ editPath: { type: String, default: null },
68
+ createPath: { type: String, default: null },
69
+ insertIndex: { type: Number, default: null }
70
+ },
71
+
72
+ computed: {
73
+ isInlined() {
74
+ return isInlined(this.schema)
75
+ },
76
+
77
+ formSchema() {
78
+ return getItemFormSchema(
79
+ this.schema,
80
+ this.data,
81
+ this.sourceComponent.context
82
+ )
83
+ },
84
+
85
+ inlinedLabel() {
86
+ // List items always show their label, objects only when collapsible.
87
+ return isListSource(this.schema) || this.collapsible
88
+ ? this.sourceComponent.getItemLabel(this.schema, this.data, {
89
+ index: this.index,
90
+ asObject: true
91
+ })
92
+ : null
93
+ },
94
+
95
+ itemLabel() {
96
+ return this.sourceComponent.getItemLabel(this.schema, this.data, {
97
+ index: this.index
98
+ })
99
+ },
100
+
101
+ renderedItem() {
102
+ const { sourceComponent } = this
103
+ return sourceComponent.render(
104
+ new DitoContext(sourceComponent, {
105
+ data: this.data,
106
+ value: this.data,
107
+ index: this.index,
108
+ dataPath: this.dataPath
109
+ })
110
+ )
111
+ }
112
+ }
113
+ })
114
+ </script>
@@ -33,8 +33,12 @@ export default DitoComponent.component('DitoTableCell', {
33
33
  mixins: [ContextMixin],
34
34
 
35
35
  props: {
36
+ // The schema of the cell, e.g. a column of a list or a property of a tree
37
+ // item, whose `name` is the key of its value in `data`.
36
38
  cell: { type: Object, required: true },
39
+ // The schema of the list or tree that holds the item.
37
40
  schema: { type: Object, required: true },
41
+ // The data path and data of the item, see `renderCell()`.
38
42
  dataPath: { type: String, required: true },
39
43
  data: { type: Object, required: true },
40
44
  meta: { type: Object, required: true },
@@ -8,21 +8,15 @@ thead.dito-table-head
8
8
  v-if="shouldRenderSchema(column)"
9
9
  scope="col"
10
10
  :class="getColumnClass(column)"
11
+ :aria-sort="column.sortable ? getSortState(column) : null"
11
12
  )
12
- RouterLink(
13
+ DitoButton(
13
14
  v-if="column.sortable"
14
- v-slot="{ navigate }"
15
- :to="getSortLink(column)"
16
- custom
15
+ :class="getSortClass(column)"
16
+ @click="sortByColumn(column)"
17
17
  )
18
- button.dito-button(
19
- type="button"
20
- :class="getSortClass(column)"
21
- :aria-sort="getSortState(column)"
22
- @click="navigate"
23
- )
24
- .dito-button__order-arrows
25
- span {{ getLabel(column) }}
18
+ .dito-button__order-arrows
19
+ span {{ getLabel(column) }}
26
20
  span(
27
21
  v-else
28
22
  ) {{ getLabel(column) }}
@@ -35,10 +29,13 @@ thead.dito-table-head
35
29
 
36
30
  <script>
37
31
  import DitoComponent from '../DitoComponent.js'
32
+ import { DitoButton } from '@ditojs/ui/src'
38
33
  import { hyphenate } from '@ditojs/utils'
39
34
 
40
35
  // @vue/component
41
36
  export default DitoComponent.component('DitoTableHead', {
37
+ components: { DitoButton },
38
+
42
39
  props: {
43
40
  query: { type: Object, required: true },
44
41
  columns: { type: Object, required: true },
@@ -74,16 +71,17 @@ export default DitoComponent.component('DitoTableHead', {
74
71
  )
75
72
  },
76
73
 
77
- getSortLink(column) {
74
+ sortByColumn(column) {
78
75
  // Toggle order if the same column is clicked again.
79
76
  const order =
80
77
  this.sort.name === column.name && this.sort.order === 'asc'
81
78
  ? 'desc'
82
79
  : 'asc'
83
- return this.getQueryLink({
84
- ...this.query,
85
- order: `${column.name} ${order}`
86
- })
80
+ // Sorting resets the page, as the sorted list has other pages.
81
+ this.sourceComponent.listQuery.update(
82
+ { order: `${column.name} ${order}` },
83
+ { resetPage: true }
84
+ )
87
85
  }
88
86
  }
89
87
  })
@@ -10,6 +10,7 @@
10
10
  :tabindex="modelValue === key ? 0 : -1"
11
11
  :class="{ 'dito-tabs__link--active': modelValue === key }"
12
12
  @click="$emit('update:modelValue', key)"
13
+ @keydown="onKeyDown($event, key)"
13
14
  ) {{ getLabel(tabSchema, key) }}
14
15
  </template>
15
16
 
@@ -33,6 +34,30 @@ export default DitoComponent.component('DitoTabs', {
33
34
 
34
35
  getTabPanelId(key) {
35
36
  return `${this.schemaComponent.componentPath}-tabpanel-${key}`
37
+ },
38
+
39
+ // Supports the keyboard interaction of tabs with manual activation: The
40
+ // arrow keys, Home and End move the focus to other tabs, Enter and Space
41
+ // select the focused tab, so that moving through the tabs doesn't switch
42
+ // their panels and the route hash on each key press.
43
+ onKeyDown(event, key) {
44
+ const keys = Object.keys(this.tabs)
45
+ const index = keys.indexOf(key)
46
+ if (event.key === 'Enter' || event.key === ' ') {
47
+ event.preventDefault()
48
+ this.$emit('update:modelValue', key)
49
+ return
50
+ }
51
+ const focusIndex = {
52
+ ArrowLeft: (index - 1 + keys.length) % keys.length,
53
+ ArrowRight: (index + 1) % keys.length,
54
+ Home: 0,
55
+ End: keys.length - 1
56
+ }[event.key]
57
+ if (focusIndex !== undefined) {
58
+ event.preventDefault()
59
+ document.getElementById(this.getTabId(keys[focusIndex]))?.focus()
60
+ }
36
61
  }
37
62
  }
38
63
  })