@ditojs/admin 2.100.0 → 3.0.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/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +3421 -2662
- package/dist/dito-admin.umd.js +6 -6
- package/package.json +81 -45
- package/src/DitoComponent.js +1 -1
- package/src/DitoContext.js +51 -0
- package/src/DitoTypeComponent.js +6 -1
- package/src/components/DitoAffixes.vue +6 -3
- package/src/components/DitoContainer.vue +19 -10
- package/src/components/DitoCreateButton.vue +11 -2
- package/src/components/DitoDialog.vue +21 -2
- package/src/components/DitoForm.vue +156 -83
- package/src/components/DitoLabel.vue +3 -2
- package/src/components/DitoPane.vue +55 -11
- package/src/components/DitoPanel.vue +33 -5
- package/src/components/DitoPanels.vue +2 -1
- package/src/components/DitoRoot.vue +13 -6
- package/src/components/DitoSchema.vue +278 -145
- package/src/components/DitoSchemaInlined.vue +1 -1
- package/src/components/DitoTabs.vue +16 -18
- package/src/components/DitoTrail.vue +5 -4
- package/src/components/DitoTreeItem.vue +17 -6
- package/src/components/DitoView.vue +25 -2
- package/src/mixins/DitoMixin.js +42 -7
- package/src/mixins/ItemMixin.js +3 -6
- package/src/mixins/NumberMixin.js +47 -31
- package/src/mixins/OptionsMixin.js +122 -71
- package/src/mixins/ResourceMixin.js +5 -2
- package/src/mixins/RouteMixin.js +39 -1
- package/src/mixins/SortableMixin.js +0 -13
- package/src/mixins/SourceMixin.js +142 -95
- package/src/mixins/TextMixin.js +1 -1
- package/src/mixins/TypeMixin.js +37 -22
- package/src/mixins/ValidationMixin.js +9 -27
- package/src/mixins/ValidatorMixin.js +5 -7
- package/src/mixins/ValueMixin.js +5 -2
- package/src/types/DitoTypeButton.vue +1 -1
- package/src/types/DitoTypeCheckbox.vue +1 -1
- package/src/types/DitoTypeCheckboxes.vue +1 -1
- package/src/types/DitoTypeCode.vue +8 -1
- package/src/types/DitoTypeColor.vue +20 -34
- package/src/types/DitoTypeComponent.vue +1 -1
- package/src/types/DitoTypeComputed.vue +4 -27
- package/src/types/DitoTypeDate.vue +16 -5
- package/src/types/DitoTypeLabel.vue +1 -4
- package/src/types/DitoTypeList.vue +9 -5
- package/src/types/DitoTypeMarkup.vue +25 -13
- package/src/types/DitoTypeMultiselect.vue +26 -6
- package/src/types/DitoTypeNumber.vue +1 -1
- package/src/types/DitoTypeObject.vue +10 -7
- package/src/types/DitoTypePanel.vue +19 -6
- package/src/types/DitoTypeProgress.vue +1 -1
- package/src/types/DitoTypeRadio.vue +1 -1
- package/src/types/DitoTypeSection.vue +32 -4
- package/src/types/DitoTypeSelect.vue +1 -1
- package/src/types/DitoTypeSlider.vue +1 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +13 -15
- package/src/types/DitoTypeTextarea.vue +1 -1
- package/src/types/DitoTypeTreeList.vue +12 -7
- package/src/types/DitoTypeUpload.vue +1 -2
- package/src/utils/DataSchemaResolver.js +141 -0
- package/src/utils/DataSchemaResolver.test.js +132 -0
- package/src/utils/FormModel.js +556 -0
- package/src/utils/FormModel.test.js +808 -0
- package/src/utils/data.js +20 -0
- package/src/utils/data.test.js +28 -0
- package/src/utils/filter.js +3 -2
- package/src/utils/options.js +3 -0
- package/src/utils/schema/data.js +617 -0
- package/src/utils/schema/data.test.js +471 -0
- package/src/utils/schema/lookup.js +266 -0
- package/src/utils/schema/lookup.test.js +61 -0
- package/src/utils/schema/setup.js +354 -0
- package/src/utils/schema/structure.js +172 -0
- package/src/utils/schema/structure.test.js +57 -0
- package/src/utils/schema/types.js +34 -0
- package/src/utils/schema/validation.js +142 -0
- package/src/utils/schema/validation.test.js +395 -0
- package/src/utils/units.js +70 -3
- package/src/utils/units.test.js +71 -0
- package/types/index.d.ts +149 -59
- package/src/mixins/DataMixin.js +0 -131
- package/src/utils/schema.js +0 -1121
|
@@ -28,7 +28,6 @@ slot(name="prepend")
|
|
|
28
28
|
v-if="hasLabel"
|
|
29
29
|
:label="label"
|
|
30
30
|
:info="info"
|
|
31
|
-
:dataPath="dataPath"
|
|
32
31
|
:collapsible="collapsible"
|
|
33
32
|
:collapsed="!opened"
|
|
34
33
|
@open="onOpen"
|
|
@@ -40,8 +39,7 @@ slot(name="prepend")
|
|
|
40
39
|
DitoTabs(
|
|
41
40
|
v-if="opened"
|
|
42
41
|
v-model="selectedTab"
|
|
43
|
-
:tabs="
|
|
44
|
-
:dataPath="dataPath"
|
|
42
|
+
:tabs="renderedTabs"
|
|
45
43
|
)
|
|
46
44
|
DitoClipboard(
|
|
47
45
|
v-if="clipboard"
|
|
@@ -62,8 +60,10 @@ slot(name="prepend")
|
|
|
62
60
|
v-for="(tabSchema, tab) in tabs"
|
|
63
61
|
:key="tab"
|
|
64
62
|
)
|
|
65
|
-
//- TODO: Switch to v-if instead of v-show,
|
|
66
|
-
//-
|
|
63
|
+
//- TODO: Switch to v-if instead of v-show, now that the dirty state
|
|
64
|
+
//- of forms is based on their data (views still use the dirty flags
|
|
65
|
+
//- of their components). `navigateToComponent()` then needs to
|
|
66
|
+
//- select the tab that displays the component.
|
|
67
67
|
DitoPane.dito-pane__tab(
|
|
68
68
|
v-show="selectedTab === tab"
|
|
69
69
|
ref="tabs"
|
|
@@ -111,7 +111,6 @@ import {
|
|
|
111
111
|
isObject,
|
|
112
112
|
isArray,
|
|
113
113
|
isFunction,
|
|
114
|
-
isRegExp,
|
|
115
114
|
equals,
|
|
116
115
|
parseDataPath,
|
|
117
116
|
normalizeDataPath,
|
|
@@ -122,14 +121,17 @@ import DitoComponent from '../DitoComponent.js'
|
|
|
122
121
|
import ContextMixin from '../mixins/ContextMixin.js'
|
|
123
122
|
import ItemMixin from '../mixins/ItemMixin.js'
|
|
124
123
|
import { appendDataPath } from '../utils/data.js'
|
|
124
|
+
import { isPanel } from '../utils/schema/structure.js'
|
|
125
125
|
import {
|
|
126
126
|
getNamedSchemas,
|
|
127
127
|
getPanelEntries,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
isEmptySchema
|
|
129
|
+
} from '../utils/schema/lookup.js'
|
|
130
|
+
import { initializeData, processData } from '../utils/schema/data.js'
|
|
131
|
+
import {
|
|
132
|
+
getDataValidationErrors,
|
|
133
|
+
getMatchingValidationErrors
|
|
134
|
+
} from '../utils/schema/validation.js'
|
|
133
135
|
import { getSchemaAccessor, getStoreAccessor } from '../utils/accessor.js'
|
|
134
136
|
|
|
135
137
|
// @vue/component
|
|
@@ -185,6 +187,7 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
185
187
|
),
|
|
186
188
|
selectedTab: null,
|
|
187
189
|
componentsRegistry: {},
|
|
190
|
+
unnestedComponentsRegistry: {},
|
|
188
191
|
panesRegistry: {},
|
|
189
192
|
panelsRegistry: {},
|
|
190
193
|
scrollPositions: {}
|
|
@@ -210,17 +213,33 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
210
213
|
},
|
|
211
214
|
|
|
212
215
|
panelEntries() {
|
|
213
|
-
return getPanelEntries(
|
|
216
|
+
return getPanelEntries(
|
|
217
|
+
this.schema.panels,
|
|
218
|
+
this.dataPath,
|
|
219
|
+
this.componentPath
|
|
220
|
+
)
|
|
214
221
|
},
|
|
215
222
|
|
|
216
223
|
tabs() {
|
|
217
224
|
return getNamedSchemas(this.schema.tabs)
|
|
218
225
|
},
|
|
219
226
|
|
|
227
|
+
renderedTabs() {
|
|
228
|
+
// Evaluate the tabs' `if` with the schema's context, and only once there
|
|
229
|
+
// is data to evaluate it with, as their components do too.
|
|
230
|
+
return this.tabs && this.data
|
|
231
|
+
? Object.fromEntries(
|
|
232
|
+
Object.entries(this.tabs).filter(([, tab]) =>
|
|
233
|
+
this.shouldRenderSchema(tab)
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
: this.tabs
|
|
237
|
+
},
|
|
238
|
+
|
|
220
239
|
defaultTab() {
|
|
221
240
|
let first = null
|
|
222
|
-
if (this.
|
|
223
|
-
const tabs = Object.values(this.
|
|
241
|
+
if (this.renderedTabs) {
|
|
242
|
+
const tabs = Object.values(this.renderedTabs)
|
|
224
243
|
for (const { name, defaultTab } of tabs) {
|
|
225
244
|
if (isFunction(defaultTab) ? defaultTab(this.context) : defaultTab) {
|
|
226
245
|
return name
|
|
@@ -251,13 +270,11 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
251
270
|
|
|
252
271
|
// @override
|
|
253
272
|
processedData() {
|
|
254
|
-
// TODO: Fix side-effects
|
|
255
273
|
return this.processData({ target: 'server', schemaOnly: true })
|
|
256
274
|
},
|
|
257
275
|
|
|
258
276
|
clipboardData: {
|
|
259
277
|
get() {
|
|
260
|
-
// TODO: Fix side-effects
|
|
261
278
|
return this.processData({ target: 'clipboard', schemaOnly: true })
|
|
262
279
|
},
|
|
263
280
|
|
|
@@ -276,10 +293,6 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
276
293
|
)
|
|
277
294
|
},
|
|
278
295
|
|
|
279
|
-
isNested() {
|
|
280
|
-
return isNested(this.schema)
|
|
281
|
-
},
|
|
282
|
-
|
|
283
296
|
isDirty() {
|
|
284
297
|
return this.someComponent(it => it.isDirty)
|
|
285
298
|
},
|
|
@@ -313,7 +326,8 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
313
326
|
},
|
|
314
327
|
|
|
315
328
|
isTopLevelSchema() {
|
|
316
|
-
|
|
329
|
+
// Panels are displayed in the sidebar, inlined schemas in their parents.
|
|
330
|
+
return !isPanel(this.schema) && !this.inlined
|
|
317
331
|
},
|
|
318
332
|
|
|
319
333
|
hasTopLevelTabs() {
|
|
@@ -335,6 +349,10 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
335
349
|
return Object.values(this.componentsRegistry)
|
|
336
350
|
},
|
|
337
351
|
|
|
352
|
+
unnestedComponents() {
|
|
353
|
+
return Object.values(this.unnestedComponentsRegistry)
|
|
354
|
+
},
|
|
355
|
+
|
|
338
356
|
panes() {
|
|
339
357
|
return Object.values(this.panesRegistry)
|
|
340
358
|
},
|
|
@@ -396,9 +414,11 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
396
414
|
})
|
|
397
415
|
}
|
|
398
416
|
if (this.hasTopLevelTabs) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
417
|
+
// Without a selected tab, e.g. while a nested form is open, keep the
|
|
418
|
+
// route without hash. Fall back to the default tab only for tabs that
|
|
419
|
+
// aren't rendered.
|
|
420
|
+
const tab =
|
|
421
|
+
!newTab || newTab in this.renderedTabs ? newTab : this.defaultTab
|
|
402
422
|
this.$router.replace({
|
|
403
423
|
query: this.$route.query,
|
|
404
424
|
hash: tab ? `#${tab}` : null
|
|
@@ -487,16 +507,7 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
487
507
|
|
|
488
508
|
onOpen(open) {
|
|
489
509
|
this.emitEvent('open', { context: { open } })
|
|
490
|
-
|
|
491
|
-
// validation will not be executed once it's closed.
|
|
492
|
-
|
|
493
|
-
// TODO: Move validation out of components, to schema, just like
|
|
494
|
-
// processing, and use `showValidationErrors()` for the resulting errors,
|
|
495
|
-
// then remove this requirement, since we can validate closed forms and
|
|
496
|
-
// schemas then.
|
|
497
|
-
if (!this.opened || open || this.validateAll()) {
|
|
498
|
-
this.opened = open
|
|
499
|
-
}
|
|
510
|
+
this.opened = open
|
|
500
511
|
},
|
|
501
512
|
|
|
502
513
|
onChange() {
|
|
@@ -532,43 +543,30 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
532
543
|
},
|
|
533
544
|
|
|
534
545
|
validateAll(match, notify = true) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
: [match]
|
|
548
|
-
}
|
|
546
|
+
// Validate the data, not the mounted components, so that fields in
|
|
547
|
+
// collapsed schemas and closed nested forms are validated too.
|
|
548
|
+
const dataValidationErrors = this.hasData
|
|
549
|
+
? getDataValidationErrors(this.schema, this.data, {
|
|
550
|
+
dataPath: this.dataPath,
|
|
551
|
+
component: this,
|
|
552
|
+
rootData: this.rootData
|
|
553
|
+
})
|
|
554
|
+
: null
|
|
555
|
+
const matchingValidationErrors = match
|
|
556
|
+
? getMatchingValidationErrors(dataValidationErrors, match)
|
|
557
|
+
: dataValidationErrors
|
|
549
558
|
if (notify) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if (!component.validate(notify)) {
|
|
559
|
-
// Focus first error field
|
|
560
|
-
if (notify && first) {
|
|
561
|
-
component.scrollIntoView()
|
|
562
|
-
}
|
|
563
|
-
first = false
|
|
564
|
-
isValid = false
|
|
565
|
-
}
|
|
559
|
+
if (matchingValidationErrors) {
|
|
560
|
+
this._showValidationErrorsByFullDataPath(
|
|
561
|
+
matchingValidationErrors,
|
|
562
|
+
true,
|
|
563
|
+
true
|
|
564
|
+
).catch(console.error)
|
|
565
|
+
} else {
|
|
566
|
+
this.clearErrors()
|
|
566
567
|
}
|
|
567
568
|
}
|
|
568
|
-
|
|
569
|
-
this.notifyErrors()
|
|
570
|
-
}
|
|
571
|
-
return isValid
|
|
569
|
+
return !matchingValidationErrors
|
|
572
570
|
},
|
|
573
571
|
|
|
574
572
|
verifyAll(match) {
|
|
@@ -576,75 +574,162 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
576
574
|
},
|
|
577
575
|
|
|
578
576
|
async showValidationErrors(errors, focus, first = true) {
|
|
577
|
+
// Convert the data paths from JavaScript property access notation to
|
|
578
|
+
// our own form of relative JSON pointers. If the schema is a data-root,
|
|
579
|
+
// prefix its own dataPath to all errors, since the data that it sends
|
|
580
|
+
// and validates will be unprefixed.
|
|
581
|
+
const errorsByFullDataPath = Object.fromEntries(
|
|
582
|
+
Object.entries(errors).map(([dataPath, errs]) => [
|
|
583
|
+
normalizeDataPath(
|
|
584
|
+
this.hasOwnData
|
|
585
|
+
? appendDataPath(this.dataPath, dataPath)
|
|
586
|
+
: dataPath
|
|
587
|
+
),
|
|
588
|
+
errs
|
|
589
|
+
])
|
|
590
|
+
)
|
|
591
|
+
return this._showValidationErrorsByFullDataPath(
|
|
592
|
+
errorsByFullDataPath,
|
|
593
|
+
focus,
|
|
594
|
+
first
|
|
595
|
+
)
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
// Finds all components that display the value at `dataPath` and passes
|
|
599
|
+
// them to `onComplete()`, revealing them first if needed: Collapsed
|
|
600
|
+
// schemas open, and the nearest components that display a part of the
|
|
601
|
+
// data path are asked to navigate to the rest, e.g. sections open and
|
|
602
|
+
// sources open their inlined items or navigate to nested forms, see
|
|
603
|
+
// `SourceMixin.navigateToComponent()`. Once components are found, the
|
|
604
|
+
// other ones are only revealed in inlined schemas
|
|
605
|
+
// (`shouldRevealInlinedOnly`), so that there's only one navigation to a
|
|
606
|
+
// nested form. Returns `true` if components were found and `onComplete()`
|
|
607
|
+
// accepted them.
|
|
608
|
+
async navigateToComponent(
|
|
609
|
+
dataPath,
|
|
610
|
+
onComplete,
|
|
611
|
+
{ shouldRevealInlinedOnly = false } = {}
|
|
612
|
+
) {
|
|
613
|
+
const { opened } = this
|
|
614
|
+
if (!opened) {
|
|
615
|
+
this.opened = true
|
|
616
|
+
await this.$nextTick()
|
|
617
|
+
}
|
|
618
|
+
const acceptedComponents = []
|
|
619
|
+
const completeWithComponents = components => {
|
|
620
|
+
const isAccepted = onComplete?.(components) ?? true
|
|
621
|
+
if (isAccepted) {
|
|
622
|
+
acceptedComponents.push(...components)
|
|
623
|
+
}
|
|
624
|
+
return isAccepted
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const navigateViaNestedComponents = async nestedComponents => {
|
|
628
|
+
for (const nestedComponent of nestedComponents) {
|
|
629
|
+
// Skip components that contain accepted components already.
|
|
630
|
+
const componentPathPrefix = `${nestedComponent.componentPath}/`
|
|
631
|
+
const hasAcceptedComponents = acceptedComponents.some(
|
|
632
|
+
acceptedComponent =>
|
|
633
|
+
acceptedComponent.componentPath.startsWith(componentPathPrefix)
|
|
634
|
+
)
|
|
635
|
+
if (!hasAcceptedComponents) {
|
|
636
|
+
await nestedComponent.navigateToComponent?.(
|
|
637
|
+
dataPath,
|
|
638
|
+
completeWithComponents,
|
|
639
|
+
{
|
|
640
|
+
shouldRevealInlinedOnly: (
|
|
641
|
+
shouldRevealInlinedOnly || acceptedComponents.length > 0
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const displayingComponents = this.getComponentsByDataPath(dataPath)
|
|
650
|
+
if (displayingComponents.length > 0) {
|
|
651
|
+
completeWithComponents(displayingComponents)
|
|
652
|
+
}
|
|
653
|
+
// Walk up the data path and ask the nested components that display a
|
|
654
|
+
// part of it to navigate to the rest of it:
|
|
655
|
+
const dataPathParts = parseDataPath(dataPath)
|
|
656
|
+
const schemaDataPathLength = parseDataPath(this.dataPath).length
|
|
657
|
+
while (dataPathParts.length > schemaDataPathLength + 1) {
|
|
658
|
+
dataPathParts.pop()
|
|
659
|
+
await navigateViaNestedComponents(
|
|
660
|
+
this.getComponentsByDataPath(dataPathParts)
|
|
661
|
+
)
|
|
662
|
+
}
|
|
663
|
+
// Unnested components share the data path of the schema, so they
|
|
664
|
+
// decide by schema whether they can navigate to the data path:
|
|
665
|
+
await navigateViaNestedComponents(this.unnestedComponents)
|
|
666
|
+
const hasFoundComponents = acceptedComponents.length > 0
|
|
667
|
+
if (!hasFoundComponents) {
|
|
668
|
+
this.opened = opened
|
|
669
|
+
}
|
|
670
|
+
return hasFoundComponents
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
// Shows `errorsByFullDataPath`, keyed by normalized data paths including
|
|
674
|
+
// the schema's own data path, see `showValidationErrors()`. Errors are
|
|
675
|
+
// shown on all components that display their data path, and the first one
|
|
676
|
+
// in document order is focused.
|
|
677
|
+
async _showValidationErrorsByFullDataPath(
|
|
678
|
+
errorsByFullDataPath,
|
|
679
|
+
focus,
|
|
680
|
+
first
|
|
681
|
+
) {
|
|
579
682
|
this.clearErrors()
|
|
580
683
|
const unmatched = []
|
|
581
684
|
const wasFirst = first
|
|
582
|
-
for (const [dataPath, errs] of Object.entries(
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (component.showValidationErrors(errs, first && focus)) {
|
|
596
|
-
found = true
|
|
597
|
-
first = false
|
|
598
|
-
break
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
if (!found) {
|
|
602
|
-
// Couldn't find a component in an active form for the given dataPath.
|
|
603
|
-
// See if we can find a component serving a part of the dataPath,
|
|
604
|
-
// and take it from there:
|
|
605
|
-
const property = dataPathParts.pop()
|
|
606
|
-
while (dataPathParts.length > 0) {
|
|
607
|
-
const components = this.getComponentsByDataPath(dataPathParts)
|
|
608
|
-
for (const component of components) {
|
|
609
|
-
const navigated = await component.navigateToComponent?.(
|
|
610
|
-
fullDataPath,
|
|
611
|
-
subComponents => {
|
|
612
|
-
let found = false
|
|
613
|
-
for (const component of subComponents) {
|
|
614
|
-
const matched = Object.fromEntries(
|
|
615
|
-
Object.entries(errors).filter(
|
|
616
|
-
([dataPath]) =>
|
|
617
|
-
normalizeDataPath(dataPath).startsWith(
|
|
618
|
-
component.dataPath
|
|
619
|
-
)
|
|
620
|
-
)
|
|
621
|
-
)
|
|
622
|
-
if (
|
|
623
|
-
Object.keys(matched).length > 0 &&
|
|
624
|
-
component.showValidationErrors(matched, first && focus)
|
|
625
|
-
) {
|
|
626
|
-
found = true
|
|
627
|
-
first = false
|
|
628
|
-
break
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
return found
|
|
632
|
-
}
|
|
633
|
-
)
|
|
634
|
-
if (navigated) {
|
|
635
|
-
// Found a nested form to display at least parts fo the errors.
|
|
636
|
-
// We can't show all errors at once, so we're done. Don't call
|
|
637
|
-
// `notifyErrors()` yet, as we can only display it once
|
|
638
|
-
// `showValidationErrors()` was called from `DitoForm.mounted()`
|
|
639
|
-
return
|
|
685
|
+
for (const [dataPath, errs] of Object.entries(errorsByFullDataPath)) {
|
|
686
|
+
let nestedForm = null
|
|
687
|
+
const displayingComponents = []
|
|
688
|
+
const hasFoundComponents = await this.navigateToComponent(
|
|
689
|
+
dataPath,
|
|
690
|
+
foundComponents => {
|
|
691
|
+
for (const component of foundComponents) {
|
|
692
|
+
if (component.isForm) {
|
|
693
|
+
// A nested form was navigated to, display the errors that are
|
|
694
|
+
// part of its data there.
|
|
695
|
+
nestedForm = component
|
|
696
|
+
} else {
|
|
697
|
+
displayingComponents.push(component)
|
|
640
698
|
}
|
|
641
699
|
}
|
|
642
|
-
|
|
643
|
-
|
|
700
|
+
return true
|
|
701
|
+
}
|
|
702
|
+
)
|
|
703
|
+
// Focus the first component in document order, unless a nested form
|
|
704
|
+
// displays the errors:
|
|
705
|
+
sortInDocumentOrder(displayingComponents).forEach(
|
|
706
|
+
(component, index) => {
|
|
707
|
+
component.showValidationErrors(
|
|
708
|
+
errs,
|
|
709
|
+
first && focus && !nestedForm && index === 0
|
|
710
|
+
)
|
|
644
711
|
}
|
|
712
|
+
)
|
|
713
|
+
if (nestedForm) {
|
|
714
|
+
const nestedFormErrors = Object.fromEntries(
|
|
715
|
+
Object.entries(errorsByFullDataPath).filter(
|
|
716
|
+
([errorDataPath]) => (
|
|
717
|
+
errorDataPath === nestedForm.dataPath ||
|
|
718
|
+
errorDataPath.startsWith(`${nestedForm.dataPath}/`)
|
|
719
|
+
)
|
|
720
|
+
)
|
|
721
|
+
)
|
|
722
|
+
// The nested form notifies the errors itself, as it can't display
|
|
723
|
+
// the remaining errors of this schema, so we're done here.
|
|
724
|
+
return nestedForm.showValidationErrors(
|
|
725
|
+
nestedFormErrors,
|
|
726
|
+
first && focus
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
if (!hasFoundComponents) {
|
|
645
730
|
// When the error can't be matched, add it to a list of unmatched
|
|
646
731
|
// errors with decent message, to report at the end.
|
|
647
|
-
const field = labelize(
|
|
732
|
+
const field = labelize(parseDataPath(dataPath).pop())
|
|
648
733
|
for (const err of errs) {
|
|
649
734
|
const prefix = field
|
|
650
735
|
? `The field ${field}`
|
|
@@ -671,12 +756,13 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
671
756
|
resetData() {
|
|
672
757
|
// We can't set `this.data = ...` because it's a property, but we can set
|
|
673
758
|
// all known properties on it to the values returned by
|
|
674
|
-
// `
|
|
759
|
+
// `initializeData()`, as they are all reactive already from the starts:
|
|
675
760
|
// eslint-disable-next-line vue/no-mutating-props
|
|
676
|
-
Object.assign(this.data,
|
|
761
|
+
Object.assign(this.data, initializeData(this.dataSchema, {}, this))
|
|
677
762
|
this.clearErrors()
|
|
678
763
|
},
|
|
679
764
|
|
|
765
|
+
// Merges the values of `data` into the schema's data, and returns it.
|
|
680
766
|
setData(data) {
|
|
681
767
|
for (const name in data) {
|
|
682
768
|
if (name in this.data) {
|
|
@@ -689,6 +775,7 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
689
775
|
}
|
|
690
776
|
}
|
|
691
777
|
}
|
|
778
|
+
return this.data
|
|
692
779
|
},
|
|
693
780
|
|
|
694
781
|
filterData(data) {
|
|
@@ -733,26 +820,50 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
733
820
|
this.$schemaParentComponent()?._registerSchemaComponent(this, add)
|
|
734
821
|
},
|
|
735
822
|
|
|
736
|
-
_registerComponent(component, add) {
|
|
737
|
-
this._registerEntry(
|
|
823
|
+
_registerComponent(component, add, componentPath) {
|
|
824
|
+
this._registerEntry(
|
|
825
|
+
this.componentsRegistry,
|
|
826
|
+
component,
|
|
827
|
+
add,
|
|
828
|
+
componentPath
|
|
829
|
+
)
|
|
738
830
|
// Only register with the parent if schema shares data with it.
|
|
739
|
-
this.parentSchemaComponent?._registerComponent(
|
|
831
|
+
this.parentSchemaComponent?._registerComponent(
|
|
832
|
+
component,
|
|
833
|
+
add,
|
|
834
|
+
componentPath
|
|
835
|
+
)
|
|
836
|
+
},
|
|
837
|
+
|
|
838
|
+
_registerUnnestedComponent(component, add, componentPath) {
|
|
839
|
+
// Unnested components are only registered with their own schema, as
|
|
840
|
+
// they share its data path, see `navigateToComponent()`.
|
|
841
|
+
this._registerEntry(
|
|
842
|
+
this.unnestedComponentsRegistry,
|
|
843
|
+
component,
|
|
844
|
+
add,
|
|
845
|
+
componentPath
|
|
846
|
+
)
|
|
740
847
|
},
|
|
741
848
|
|
|
742
|
-
_registerPane(pane, add) {
|
|
743
|
-
this._registerEntry(this.panesRegistry, pane, add)
|
|
849
|
+
_registerPane(pane, add, componentPath) {
|
|
850
|
+
this._registerEntry(this.panesRegistry, pane, add, componentPath)
|
|
744
851
|
},
|
|
745
852
|
|
|
746
|
-
_registerPanel(panel, add) {
|
|
747
|
-
this._registerEntry(this.panelsRegistry, panel, add)
|
|
853
|
+
_registerPanel(panel, add, componentPath) {
|
|
854
|
+
this._registerEntry(this.panelsRegistry, panel, add, componentPath)
|
|
748
855
|
},
|
|
749
856
|
|
|
750
|
-
|
|
751
|
-
|
|
857
|
+
// Registries are keyed by component path. Component paths change when list
|
|
858
|
+
// 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) {
|
|
752
861
|
if (add) {
|
|
753
|
-
registry[
|
|
754
|
-
} else {
|
|
755
|
-
|
|
862
|
+
registry[componentPath] = entry
|
|
863
|
+
} else if (registry[componentPath] === entry) {
|
|
864
|
+
// Unless another entry registered under the path already, e.g. when
|
|
865
|
+
// list items swap places.
|
|
866
|
+
delete registry[componentPath]
|
|
756
867
|
}
|
|
757
868
|
},
|
|
758
869
|
|
|
@@ -774,14 +885,36 @@ export default DitoComponent.component('DitoSchema', {
|
|
|
774
885
|
},
|
|
775
886
|
|
|
776
887
|
_getEntriesByDataPath(entriesByDataPath, dataPath) {
|
|
777
|
-
return
|
|
888
|
+
return sortInDocumentOrder(
|
|
889
|
+
entriesByDataPath[normalizeDataPath(dataPath)] || []
|
|
890
|
+
)
|
|
778
891
|
},
|
|
779
892
|
|
|
780
893
|
_getEntriesByName(entriesByDataPath, name) {
|
|
781
|
-
return
|
|
894
|
+
return sortInDocumentOrder(
|
|
895
|
+
entriesByDataPath[appendDataPath(this.dataPath, name)] || []
|
|
896
|
+
)
|
|
782
897
|
}
|
|
783
898
|
}
|
|
784
899
|
})
|
|
900
|
+
|
|
901
|
+
// Sorts components that display the same data, e.g. in different tabs, in the
|
|
902
|
+
// document order of their elements, which is the order of their schemas.
|
|
903
|
+
function sortInDocumentOrder(components) {
|
|
904
|
+
return components.length > 1
|
|
905
|
+
? [...components].sort(compareByDocumentPosition)
|
|
906
|
+
: components
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function compareByDocumentPosition(component1, component2) {
|
|
910
|
+
const node1 = component1.$el
|
|
911
|
+
const node2 = component2.$el
|
|
912
|
+
if (!node1 || !node2 || node1 === node2) {
|
|
913
|
+
return 0
|
|
914
|
+
}
|
|
915
|
+
const position = node1.compareDocumentPosition(node2)
|
|
916
|
+
return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
|
|
917
|
+
}
|
|
785
918
|
</script>
|
|
786
919
|
|
|
787
920
|
<style lang="scss">
|
|
@@ -41,7 +41,7 @@ DitoSchema.dito-schema-inlined(
|
|
|
41
41
|
|
|
42
42
|
<script>
|
|
43
43
|
import DitoComponent from '../DitoComponent.js'
|
|
44
|
-
import { isCompact } from '../utils/schema.js'
|
|
44
|
+
import { isCompact } from '../utils/schema/structure.js'
|
|
45
45
|
// @vue/component
|
|
46
46
|
export default DitoComponent.component('DitoSchemaInlined', {
|
|
47
47
|
emits: ['delete'],
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
2
|
.dito-tabs(role="tablist")
|
|
3
|
-
|
|
3
|
+
a.dito-tabs__link(
|
|
4
4
|
v-for="(tabSchema, key) in tabs"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
:class="{ 'dito-tabs__link--active': modelValue === key }"
|
|
15
|
-
@click="$emit('update:modelValue', key)"
|
|
16
|
-
) {{ getLabel(tabSchema, key) }}
|
|
5
|
+
:id="getTabId(key)"
|
|
6
|
+
:key="key"
|
|
7
|
+
role="tab"
|
|
8
|
+
:aria-selected="modelValue === key"
|
|
9
|
+
:aria-controls="getTabPanelId(key)"
|
|
10
|
+
:tabindex="modelValue === key ? 0 : -1"
|
|
11
|
+
:class="{ 'dito-tabs__link--active': modelValue === key }"
|
|
12
|
+
@click="$emit('update:modelValue', key)"
|
|
13
|
+
) {{ getLabel(tabSchema, key) }}
|
|
17
14
|
</template>
|
|
18
15
|
|
|
19
16
|
<script>
|
|
@@ -24,17 +21,18 @@ export default DitoComponent.component('DitoTabs', {
|
|
|
24
21
|
emits: ['update:modelValue'],
|
|
25
22
|
props: {
|
|
26
23
|
tabs: { type: Object, default: null },
|
|
27
|
-
modelValue: { type: String, default: null }
|
|
28
|
-
dataPath: { type: String, default: '' }
|
|
24
|
+
modelValue: { type: String, default: null }
|
|
29
25
|
},
|
|
30
26
|
|
|
31
27
|
methods: {
|
|
28
|
+
// Tabs are identified through the component path of their schema, like in
|
|
29
|
+
// `DitoPane.tabId` and `tabPanelId`.
|
|
32
30
|
getTabId(key) {
|
|
33
|
-
return `${this.
|
|
31
|
+
return `${this.schemaComponent.componentPath}-tab-${key}`
|
|
34
32
|
},
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
return `${this.
|
|
34
|
+
getTabPanelId(key) {
|
|
35
|
+
return `${this.schemaComponent.componentPath}-tabpanel-${key}`
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
})
|