@ditojs/admin 3.2.7 → 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 +6011 -5826
  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 +24 -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
@@ -4,7 +4,8 @@ import ResourceMixin from './ResourceMixin.js'
4
4
  import SchemaParentMixin from '../mixins/SchemaParentMixin.js'
5
5
  import { getSchemaAccessor, getStoreAccessor } from '../utils/accessor.js'
6
6
  import { getMemberResource } from '../utils/resource.js'
7
- import { replaceRoute } from '../utils/route.js'
7
+ import { ListQuery } from '../utils/ListQuery.js'
8
+ import { ItemStores } from '../utils/ItemStores.js'
8
9
  import {
9
10
  isCompact,
10
11
  isInlined,
@@ -21,6 +22,7 @@ import {
21
22
  getViewPath
22
23
  } from '../utils/schema/lookup.js'
23
24
  import { updateOrder } from '../utils/schema/data.js'
25
+ import { confirmAndRemove } from '../utils/dialogs.js'
24
26
  import {
25
27
  isObject,
26
28
  isString,
@@ -30,6 +32,7 @@ import {
30
32
  parseDataPath,
31
33
  normalizeDataPath
32
34
  } from '@ditojs/utils'
35
+ import { markRaw } from 'vue'
33
36
 
34
37
  // @vue/component
35
38
  export default {
@@ -47,7 +50,16 @@ export default {
47
50
 
48
51
  data() {
49
52
  return {
50
- wrappedPrimitives: null
53
+ wrappedPrimitives: null,
54
+ listQuery: markRaw(
55
+ new ListQuery({
56
+ router: this.$router,
57
+ getRoute: () => this.$route,
58
+ getSourceStore: () => this.store,
59
+ getDefaultQuery: () => this.defaultQuery
60
+ })
61
+ ),
62
+ itemStores: ItemStores.getFromStore(this.store)
51
63
  }
52
64
  },
53
65
 
@@ -157,45 +169,28 @@ export default {
157
169
  return this.routeComponent.getChildPath(this.schema.path)
158
170
  },
159
171
 
172
+ // The defaults of the query, see `ListQuery`.
160
173
  defaultQuery() {
161
- const { defaultOrder: order } = this
162
- return order ? { order } : {}
174
+ return {
175
+ scope: this.defaultScope?.name,
176
+ page: this.schema.page,
177
+ order: this.defaultOrder
178
+ }
163
179
  },
164
180
 
165
- query: getStoreAccessor('query', {
166
- get(query) {
167
- return {
168
- ...this.defaultQuery,
169
- ...query
170
- }
181
+ query: {
182
+ get() {
183
+ return this.listQuery.query
171
184
  },
172
185
 
186
+ // Navigates to the query asynchronously, see `ListQuery.update()`. The
187
+ // stored query and thus the getter only change once the route did, when
188
+ // the `$route` watcher loads the list. Loads requested meanwhile wait
189
+ // for the navigation, see `requestData()`.
173
190
  set(query) {
174
- // Always keep the displayed query parameters in sync with the stored
175
- // ones. Use scope and page from the list schema as defaults, but allow
176
- // the route query parameters to override them.
177
- const {
178
- scope = this.defaultScope?.name,
179
- page = this.schema.page,
180
- type
181
- } = this.query
182
- // Preserve / merge currently stored values, including any custom query
183
- // parameters added by creatable.query
184
- query = {
185
- ...this.query,
186
- ...(scope != null && { scope }),
187
- ...(page != null && { page }),
188
- ...(type != null && { type }),
189
- ...query
190
- }
191
- if (!equals(query, this.$route.query)) {
192
- // Change the route query parameters, but don't trigger a route
193
- // change, as that would cause the list to reload.
194
- replaceRoute({ query })
195
- }
196
- return query // Let getStoreAccessor() do the actual setting
191
+ this.listQuery.update(query)
197
192
  }
198
- }),
193
+ },
199
194
 
200
195
  total: getStoreAccessor('total'),
201
196
 
@@ -355,25 +350,14 @@ export default {
355
350
  // https://github.com/vuejs/vue-router/issues/3393#issuecomment-1158470149
356
351
  flush: 'post',
357
352
  handler(to, from) {
358
- if (this.providesData) {
359
- if (
360
- from.path === to.path &&
361
- from.hash === to.hash
362
- ) {
363
- // Paths and hashes remain the same, so only queries have changed.
364
- // Update filter and reload data without clearing.
365
- this.query = to.query
366
- this.loadData(false)
367
- } else if (
368
- this.meta.reload &&
369
- from.path !== to.path &&
370
- from.path.startsWith(to.path)
371
- ) {
372
- // Reload the source when navigating back to a parent-route after
373
- // changing data in a child-route.
374
- this.meta.reload = false
375
- this.loadData(false)
376
- }
353
+ if (
354
+ this.providesData &&
355
+ // Only the query changed, see `ListQuery`:
356
+ from.path === to.path &&
357
+ from.hash === to.hash &&
358
+ this.listQuery.setFromRoute(to.query)
359
+ ) {
360
+ this.loadData(false)
377
361
  }
378
362
  }
379
363
  },
@@ -417,10 +401,26 @@ export default {
417
401
 
418
402
  methods: {
419
403
  setupData() {
420
- this.query = this.$route.query
404
+ this.listQuery.syncWithRoute()
421
405
  this.ensureData()
422
406
  },
423
407
 
408
+ // @override ResourceMixin.requestData()
409
+ requestData() {
410
+ // While a navigation to a changed query is pending, the stored query is
411
+ // stale, and the `$route` watcher loads the list with the new query once
412
+ // the route changed. Wait for it instead of loading the stale query, and
413
+ // only load if the navigation failed or was superseded.
414
+ const navigated = this.listQuery.whenNavigated()
415
+ return navigated
416
+ ? navigated.then(hasNavigated => {
417
+ if (!hasNavigated && !this.$.isUnmounted) {
418
+ return this.requestData()
419
+ }
420
+ })
421
+ : ResourceMixin.methods.requestData.call(this)
422
+ },
423
+
424
424
  // @override ResourceMixin.clearData()
425
425
  clearData() {
426
426
  this.total = 0
@@ -450,6 +450,10 @@ export default {
450
450
  return this.value
451
451
  },
452
452
 
453
+ getItemStore(item) {
454
+ return this.itemStores.getStore(this.getItemUid(this.schema, item))
455
+ },
456
+
453
457
  isListResults(data) {
454
458
  // Lists can also be loaded as `{ results, total }`, see `setData()`.
455
459
  return (
@@ -536,57 +540,71 @@ export default {
536
540
  }
537
541
  }
538
542
  if (removed) {
539
- this.removeItemStore(this.schema, item, index)
540
- this.onChange()
543
+ this.itemStores.removeStore(this.getItemUid(this.schema, item))
544
+ // Items of lists with a resource are deleted through it, see
545
+ // `deleteItem()`, leaving no changes to save behind, but still
546
+ // changing the list.
547
+ if (this.isTransient) {
548
+ this.onChange()
549
+ } else {
550
+ this.emitChangeEvent()
551
+ }
541
552
  }
542
553
  },
543
554
 
544
- deleteItem(item, index) {
545
- const label = (
546
- item &&
547
- this.getItemLabel(this.schema, item, {
548
- index,
549
- extended: true
550
- })
551
- )
552
-
553
- const notify = () =>
554
- this.notify({
555
- type: this.isTransient ? 'info' : 'success',
556
- title: 'Successfully Removed',
557
- text: [
558
- `${label} was ${this.verbs.deleted}.`,
559
- this.transientNote
560
- ]
561
- })
562
-
563
- if (
564
- item &&
565
- window.confirm(
566
- `Do you really want to ${this.verbs.delete} ${label}?`
567
- )
568
- ) {
569
- if (this.isTransient) {
570
- this.removeItem(item, index)
571
- notify()
572
- } else {
573
- const itemId = this.getItemId(this.schema, item, index)
555
+ async deleteItem(item, index) {
556
+ if (!item) return
557
+ const label = this.getItemLabel(this.schema, item, {
558
+ index,
559
+ extended: true
560
+ })
561
+ // Look up the item by identity whenever it's removed, as the data may
562
+ // have changed while the dialog was open or the request was pending.
563
+ // Object sources have no index, see `removeItem()`.
564
+ const getCurrentIndex = () => {
565
+ if (this.isObjectSource) {
566
+ return this.objectData === item ? null : -1
567
+ }
568
+ return this.listData.indexOf(item)
569
+ }
570
+ const removeIfPresent = () => {
571
+ const currentIndex = getCurrentIndex()
572
+ const isPresent = currentIndex !== -1
573
+ if (isPresent) {
574
+ this.removeItem(item, currentIndex)
575
+ }
576
+ return isPresent
577
+ }
578
+ await confirmAndRemove(this, {
579
+ label,
580
+ isTransient: this.isTransient,
581
+ remove: async () => {
582
+ if (this.isTransient) {
583
+ return removeIfPresent()
584
+ }
585
+ const currentIndex = getCurrentIndex()
586
+ if (currentIndex === -1) {
587
+ return false
588
+ }
574
589
  const method = 'delete'
575
590
  const resource = getMemberResource(
576
- itemId,
591
+ this.getItemId(this.schema, item, currentIndex),
577
592
  this.getResource({ method })
578
593
  )
579
- if (resource) {
580
- this.handleRequest({ method, resource }, err => {
581
- if (!err) {
582
- this.removeItem(item, index)
583
- notify()
584
- }
585
- this.reloadData()
586
- })
594
+ if (!resource) {
595
+ return false
587
596
  }
597
+ let isDeleted = false
598
+ await this.handleRequest({ method, resource }, err => {
599
+ if (!err) {
600
+ removeIfPresent()
601
+ isDeleted = true
602
+ }
603
+ this.reloadData()
604
+ })
605
+ return isDeleted
588
606
  }
589
- }
607
+ })
590
608
  },
591
609
 
592
610
  getSchemaComponent(index) {
@@ -646,10 +664,10 @@ export default {
646
664
  navigateToRouteComponent(dataPath, onComplete) {
647
665
  return new Promise((resolve, reject) => {
648
666
  const callOnComplete = () => {
649
- // Retrieve the last route component, which will be the component that
650
- // we just navigated to, and pass it on to `onComplete()`
651
- const { routeComponents } = this.appState
652
- const routeComponent = routeComponents[routeComponents.length - 1]
667
+ // Retrieve the route component of the last route level, which is the
668
+ // component that we just navigated to, and pass it to `onComplete()`
669
+ const level = this.$route.matched.length - 1
670
+ const routeComponent = this.appState.routeComponents[level]
653
671
  resolve(onComplete?.([routeComponent]) ?? true)
654
672
  }
655
673
 
@@ -1,7 +1,9 @@
1
1
  import ValueMixin from './ValueMixin.js'
2
2
  import ContextMixin from './ContextMixin.js'
3
3
  import ValidationMixin from './ValidationMixin.js'
4
+ import { getSchemaEventEntries } from '../utils/schema/events.js'
4
5
  import { getSchemaAccessor } from '../utils/accessor.js'
6
+ import { trackRegistration } from '../utils/registration.js'
5
7
  import { getValidations } from '../utils/schema/validation.js'
6
8
  import { asArray, camelize, equals } from '@ditojs/utils'
7
9
 
@@ -22,7 +24,8 @@ export default {
22
24
  label: { type: String, default: null },
23
25
  single: { type: Boolean, default: false },
24
26
  nested: { type: Boolean, default: true },
25
- accumulatedBasis: { type: Number, default: null }
27
+ // The id of the element that displays the errors, see `DitoContainer`.
28
+ errorsElementId: { type: String, default: null }
26
29
  },
27
30
 
28
31
  data() {
@@ -41,11 +44,6 @@ export default {
41
44
  return this.schema.type
42
45
  },
43
46
 
44
- labelNode() {
45
- const node = this.isMounted ? this.$el.previousElementSibling : null
46
- return node?.matches('.dito-label') ? node : null
47
- },
48
-
49
47
  visible: getSchemaAccessor('visible', {
50
48
  type: Boolean,
51
49
  default() {
@@ -107,19 +105,12 @@ export default {
107
105
  default: null
108
106
  }),
109
107
 
110
- inlineInfo() {
111
- // When a label is present, info is shown in the label component.
112
- // Otherwise, we have to show it inline.
113
- return !this.label ? this.info : null
114
- },
115
-
116
108
  events() {
117
109
  const events = this.getEvents()
118
- // Register callbacks for all provides non-recognized events,
119
- // assuming they are native events.
120
- // TODO: Move to vue3-style `on[A-Z]` event handlers naming that aren't
121
- // namespaced in `schema.events` once the transition is complete.
122
- for (const event of Object.keys(this.schema.events || {})) {
110
+ // Register callbacks for all events that the schema handles and that the
111
+ // component doesn't handle itself, assuming they are native events, in
112
+ // both forms, `events: { mouseenter }` and `onMouseenter`.
113
+ for (const { event } of getSchemaEventEntries(this.schema)) {
123
114
  events[`on${camelize(event, true)}`] ??= () => {
124
115
  this.emitEvent(event)
125
116
  }
@@ -133,7 +124,10 @@ export default {
133
124
  const attributes = {
134
125
  ...this.events,
135
126
  // Only text fields support `readonly`, disable all other components.
136
- disabled: this.disabled || (this.readonly && !textField)
127
+ 'disabled': this.disabled || (this.readonly && !textField),
128
+ // Link the displayed errors for assistive technology:
129
+ 'aria-invalid': this.hasErrors || null,
130
+ 'aria-describedby': this.hasErrors ? this.errorsElementId : null
137
131
  }
138
132
 
139
133
  if (nativeField) {
@@ -155,10 +149,6 @@ export default {
155
149
 
156
150
  validations() {
157
151
  return getValidations(this.schema, this.context)
158
- },
159
-
160
- showClearButton() {
161
- return this.clearable && this.value != null
162
152
  }
163
153
  },
164
154
 
@@ -172,23 +162,23 @@ export default {
172
162
  if (this.hasErrors && !equals(value, this.valueWithErrors)) {
173
163
  this.clearErrors()
174
164
  }
175
- },
176
-
177
- componentPath(componentPath, oldComponentPath) {
178
- // Component paths change when list items move, see `_registerEntry()`.
179
- this._registerWithSchemaComponent(false, oldComponentPath)
180
- this._registerWithSchemaComponent(true, componentPath)
181
165
  }
182
166
  },
183
167
 
184
168
  created() {
185
- this._register(true)
169
+ // Provide component to container for schema accessor evaluation.
170
+ this.$emit('update:component', this)
171
+ this._unregister = trackRegistration(
172
+ this,
173
+ this._registerWithSchemaComponent
174
+ )
186
175
  this.setupSchemaFields()
187
176
  this.warnAboutUncomputedValue()
188
177
  },
189
178
 
190
179
  unmounted() {
191
- this._register(false)
180
+ this.$emit('update:component', null)
181
+ this._unregister()
192
182
  },
193
183
 
194
184
  methods: {
@@ -198,7 +188,10 @@ export default {
198
188
  warnAboutUncomputedValue() {
199
189
  if (
200
190
  this.schema.compute &&
201
- !this.dataModel.hasComputedValueEntry(this.componentPath)
191
+ !this.dataModel.hasComputedValueEntry({
192
+ data: this.data,
193
+ name: this.name
194
+ })
202
195
  ) {
203
196
  console.warn(
204
197
  `The value of the component at '${this.componentPath}' isn't ` +
@@ -207,12 +200,6 @@ export default {
207
200
  }
208
201
  },
209
202
 
210
- _register(add) {
211
- // Provide component to container for schema accessor evaluation.
212
- this.$emit('update:component', add ? this : null)
213
- this._registerWithSchemaComponent(add, this.componentPath)
214
- },
215
-
216
203
  _registerWithSchemaComponent(add, componentPath) {
217
204
  // Register unnested type components separately, to prevent them from
218
205
  // overriding parent data paths.
@@ -244,12 +231,20 @@ export default {
244
231
 
245
232
  // @overridable
246
233
  focusElement() {
247
- this.getFocusElement()?.focus?.()
234
+ this._getFocusTarget('focus')?.focus?.()
248
235
  },
249
236
 
250
237
  // @overridable
251
238
  blurElement() {
252
- this.getFocusElement()?.blur?.()
239
+ this._getFocusTarget('blur')?.blur?.()
240
+ },
241
+
242
+ _getFocusTarget(method) {
243
+ // Prefer the `focus()` and `blur()` of components, e.g. `DitoInput`
244
+ // targets its input rather than its root element that
245
+ // `getFocusElement()` returns.
246
+ const element = asArray(this.$refs.element)[0]
247
+ return element?.[method] ? element : this.getFocusElement()
253
248
  },
254
249
 
255
250
  getFocusElement() {
@@ -298,7 +293,14 @@ export default {
298
293
 
299
294
  onChange() {
300
295
  this.markDirty()
301
- this.emitEvent('change', {
296
+ this.emitChangeEvent()
297
+ },
298
+
299
+ // Emits the `change` event without marking the component dirty, e.g. for
300
+ // changes that are already persisted through a resource, see
301
+ // `SourceMixin.removeItem()`.
302
+ emitChangeEvent() {
303
+ return this.emitEvent('change', {
302
304
  context: {
303
305
  // Prevent endless parse recursion:
304
306
  value: this.parsedValue !== undefined ? this.parsedValue : this.value
@@ -50,9 +50,9 @@ export default {
50
50
  },
51
51
 
52
52
  markTouched() {
53
+ // Displayed errors remain while the input is focused, e.g. after
54
+ // `showValidationErrors()` focused it, until new input clears them.
53
55
  this.isTouched = true
54
- // Clear currently displayed errors when focusing input.
55
- this.clearErrors()
56
56
  },
57
57
 
58
58
  markDirty() {
@@ -74,9 +74,21 @@ export default {
74
74
  this.$emit('errors', this.errors)
75
75
  },
76
76
 
77
+ // Removes the last occurrence of the error that `addError()` added.
78
+ removeError(error) {
79
+ const index = this.errors?.lastIndexOf(error) ?? -1
80
+ if (index >= 0) {
81
+ this.errors.splice(index, 1)
82
+ if (this.errors.length === 0) {
83
+ this.errors = null
84
+ }
85
+ this.$emit('errors', this.errors)
86
+ }
87
+ },
88
+
77
89
  showValidationErrors(errors, focus) {
78
90
  // Convert from AJV errors objects to an array of error messages
79
- this.errors = []
91
+ this.clearErrors()
80
92
  if (errors.length === 0) {
81
93
  return false
82
94
  }
@@ -84,7 +96,7 @@ export default {
84
96
  this.addError(message, true)
85
97
  }
86
98
  if (focus) {
87
- this.scrollIntoView()
99
+ this.focus()
88
100
  }
89
101
  return true
90
102
  },
@@ -22,7 +22,7 @@ export default {
22
22
  },
23
23
 
24
24
  isValidated() {
25
- return this.schemaComponents.every(it => it.isValid)
25
+ return this.schemaComponents.every(it => it.isValidated)
26
26
  }
27
27
  },
28
28
 
@@ -29,6 +29,7 @@ $line-height: rounded(
29
29
  $button-order-arrow-size: 4px;
30
30
 
31
31
  // Pulldown
32
+ // Deprecated, remove in 4.0
32
33
  $pulldown-radius: 0.5em;
33
34
  $pulldown-padding-ver: 0.5em;
34
35
  $pulldown-padding-hor: 1em;
@@ -79,6 +80,9 @@ $z-index-header: 2000;
79
80
  $z-index-tippy: 750;
80
81
  $z-index-main-buttons: 500;
81
82
 
83
+ // Drag overlay, see `DitoRoot` and `DitoTypeUpload`
84
+ $drag-overlay-duration: 0.15s;
85
+
82
86
  // Patterns
83
87
  $pattern-transparency-size: ($font-size - $border-width) * $input-height-factor;
84
88
  $pattern-transparency: repeating-conic-gradient(
@@ -31,15 +31,4 @@
31
31
  &[data-theme~='info'] {
32
32
  --color: #{color.adjust($color-active, $lightness: 5%)};
33
33
  }
34
-
35
- &[data-theme~='error'] {
36
- --color: #{color.adjust($color-error, $lightness: 5%)};
37
-
38
- cursor: pointer;
39
-
40
- > .tippy-arrow {
41
- transform: unset !important;
42
- left: 16px !important;
43
- }
44
- }
45
34
  }
@@ -1,7 +1,6 @@
1
1
  @import '_imports';
2
2
  @import '_base';
3
3
  @import '_button';
4
- @import '_pulldown';
5
4
  @import '_layout';
6
5
  @import '_scroll';
7
6
  @import '_table';
@@ -1,12 +1,12 @@
1
1
  <template lang="pug">
2
- DitoButton.dito-button(
2
+ DitoButton(
3
3
  :id="componentPath"
4
4
  ref="element"
5
5
  :type="type"
6
+ :verb="verb"
6
7
  :text="text"
7
- :title="title"
8
- :disabled="disabled"
9
- :class="buttonClass"
8
+ :title="text"
9
+ :aria-busy="isRunning"
10
10
  v-bind="attributes"
11
11
  )
12
12
  template(#prefix)
@@ -37,7 +37,7 @@ import DitoAffixes from '../components/DitoAffixes.vue'
37
37
  import { DitoButton } from '@ditojs/ui/src'
38
38
  import { getSchemaAccessor } from '../utils/accessor.js'
39
39
  import { hasResource } from '../utils/resource.js'
40
- import { labelize, asArray } from '@ditojs/utils'
40
+ import { asArray } from '@ditojs/utils'
41
41
 
42
42
  export default DitoTypeComponent.register(
43
43
  ['button', 'submit'],
@@ -60,18 +60,10 @@ export default DitoTypeComponent.register(
60
60
  return this.verbs[this.name]
61
61
  },
62
62
 
63
- buttonClass() {
64
- return this.verb ? `dito-button--${this.verb}` : null
65
- },
66
-
67
63
  text: getSchemaAccessor('text', {
68
64
  type: String
69
65
  }),
70
66
 
71
- title() {
72
- return this.text || labelize(this.verb)
73
- },
74
-
75
67
  prefixes() {
76
68
  return asArray(this.schema.prefix)
77
69
  },
@@ -98,6 +90,11 @@ export default DitoTypeComponent.register(
98
90
  },
99
91
 
100
92
  async onClick() {
93
+ // Prevent running the action again while it's running, e.g. submits.
94
+ // The button isn't disabled meanwhile, as `disabled` reflects the
95
+ // state of the schema, e.g. of forms that can't be submitted when
96
+ // they're clean, which a running action doesn't change:
97
+ if (this.isRunning) return
101
98
  this.isRunning = true
102
99
  try {
103
100
  const res = await this.emitEvent('click', {
@@ -111,7 +108,7 @@ export default DitoTypeComponent.register(
111
108
  await this.submit()
112
109
  }
113
110
  } catch (error) {
114
- const res = await this.emitEvent('error', { error })
111
+ const res = await this.emitEvent('error', { context: { error } })
115
112
  if (res === undefined) {
116
113
  if (error instanceof AggregateError) {
117
114
  for (const err of error.errors) {