@radio-garden/ditojs-admin 2.85.2-0.5067ad799

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 (153) hide show
  1. package/README.md +180 -0
  2. package/dist/dito-admin.css +1 -0
  3. package/dist/dito-admin.es.js +12106 -0
  4. package/dist/dito-admin.umd.js +7 -0
  5. package/package.json +96 -0
  6. package/src/DitoAdmin.js +293 -0
  7. package/src/DitoComponent.js +34 -0
  8. package/src/DitoContext.js +318 -0
  9. package/src/DitoTypeComponent.js +42 -0
  10. package/src/DitoUser.js +12 -0
  11. package/src/appState.js +12 -0
  12. package/src/components/DitoAccount.vue +60 -0
  13. package/src/components/DitoAffix.vue +68 -0
  14. package/src/components/DitoAffixes.vue +200 -0
  15. package/src/components/DitoButtons.vue +80 -0
  16. package/src/components/DitoClipboard.vue +186 -0
  17. package/src/components/DitoContainer.vue +374 -0
  18. package/src/components/DitoCreateButton.vue +146 -0
  19. package/src/components/DitoDialog.vue +242 -0
  20. package/src/components/DitoDraggable.vue +117 -0
  21. package/src/components/DitoEditButtons.vue +135 -0
  22. package/src/components/DitoErrors.vue +83 -0
  23. package/src/components/DitoForm.vue +521 -0
  24. package/src/components/DitoFormInner.vue +26 -0
  25. package/src/components/DitoFormNested.vue +17 -0
  26. package/src/components/DitoHeader.vue +84 -0
  27. package/src/components/DitoLabel.vue +200 -0
  28. package/src/components/DitoMenu.vue +186 -0
  29. package/src/components/DitoNavigation.vue +40 -0
  30. package/src/components/DitoNotifications.vue +170 -0
  31. package/src/components/DitoPagination.vue +42 -0
  32. package/src/components/DitoPane.vue +334 -0
  33. package/src/components/DitoPanel.vue +256 -0
  34. package/src/components/DitoPanels.vue +61 -0
  35. package/src/components/DitoRoot.vue +524 -0
  36. package/src/components/DitoSchema.vue +846 -0
  37. package/src/components/DitoSchemaInlined.vue +97 -0
  38. package/src/components/DitoScopes.vue +76 -0
  39. package/src/components/DitoSidebar.vue +50 -0
  40. package/src/components/DitoSpinner.vue +95 -0
  41. package/src/components/DitoTableCell.vue +64 -0
  42. package/src/components/DitoTableHead.vue +121 -0
  43. package/src/components/DitoTabs.vue +103 -0
  44. package/src/components/DitoTrail.vue +124 -0
  45. package/src/components/DitoTreeItem.vue +420 -0
  46. package/src/components/DitoUploadFile.vue +199 -0
  47. package/src/components/DitoVNode.vue +14 -0
  48. package/src/components/DitoView.vue +143 -0
  49. package/src/components/index.js +42 -0
  50. package/src/directives/resize.js +83 -0
  51. package/src/index.js +1 -0
  52. package/src/mixins/ContextMixin.js +68 -0
  53. package/src/mixins/DataMixin.js +131 -0
  54. package/src/mixins/DitoMixin.js +591 -0
  55. package/src/mixins/DomMixin.js +29 -0
  56. package/src/mixins/EmitterMixin.js +158 -0
  57. package/src/mixins/ItemMixin.js +144 -0
  58. package/src/mixins/LoadingMixin.js +23 -0
  59. package/src/mixins/NumberMixin.js +118 -0
  60. package/src/mixins/OptionsMixin.js +304 -0
  61. package/src/mixins/PulldownMixin.js +63 -0
  62. package/src/mixins/ResourceMixin.js +398 -0
  63. package/src/mixins/RouteMixin.js +190 -0
  64. package/src/mixins/SchemaParentMixin.js +33 -0
  65. package/src/mixins/SortableMixin.js +49 -0
  66. package/src/mixins/SourceMixin.js +734 -0
  67. package/src/mixins/TextMixin.js +26 -0
  68. package/src/mixins/TypeMixin.js +280 -0
  69. package/src/mixins/ValidationMixin.js +119 -0
  70. package/src/mixins/ValidatorMixin.js +57 -0
  71. package/src/mixins/ValueMixin.js +31 -0
  72. package/src/styles/_base.scss +17 -0
  73. package/src/styles/_button.scss +191 -0
  74. package/src/styles/_imports.scss +3 -0
  75. package/src/styles/_info.scss +19 -0
  76. package/src/styles/_layout.scss +19 -0
  77. package/src/styles/_pulldown.scss +38 -0
  78. package/src/styles/_scroll.scss +13 -0
  79. package/src/styles/_settings.scss +88 -0
  80. package/src/styles/_table.scss +223 -0
  81. package/src/styles/_tippy.scss +45 -0
  82. package/src/styles/style.scss +9 -0
  83. package/src/types/DitoTypeButton.vue +143 -0
  84. package/src/types/DitoTypeCheckbox.vue +27 -0
  85. package/src/types/DitoTypeCheckboxes.vue +65 -0
  86. package/src/types/DitoTypeCode.vue +199 -0
  87. package/src/types/DitoTypeColor.vue +272 -0
  88. package/src/types/DitoTypeComponent.vue +31 -0
  89. package/src/types/DitoTypeComputed.vue +50 -0
  90. package/src/types/DitoTypeDate.vue +99 -0
  91. package/src/types/DitoTypeLabel.vue +23 -0
  92. package/src/types/DitoTypeList.vue +364 -0
  93. package/src/types/DitoTypeMarkup.vue +700 -0
  94. package/src/types/DitoTypeMultiselect.vue +522 -0
  95. package/src/types/DitoTypeNumber.vue +66 -0
  96. package/src/types/DitoTypeObject.vue +136 -0
  97. package/src/types/DitoTypePanel.vue +18 -0
  98. package/src/types/DitoTypeProgress.vue +40 -0
  99. package/src/types/DitoTypeRadio.vue +45 -0
  100. package/src/types/DitoTypeSection.vue +80 -0
  101. package/src/types/DitoTypeSelect.vue +133 -0
  102. package/src/types/DitoTypeSlider.vue +66 -0
  103. package/src/types/DitoTypeSpacer.vue +11 -0
  104. package/src/types/DitoTypeSwitch.vue +40 -0
  105. package/src/types/DitoTypeText.vue +101 -0
  106. package/src/types/DitoTypeTextarea.vue +48 -0
  107. package/src/types/DitoTypeTreeList.vue +193 -0
  108. package/src/types/DitoTypeUpload.vue +503 -0
  109. package/src/types/index.js +30 -0
  110. package/src/utils/SchemaGraph.js +147 -0
  111. package/src/utils/accessor.js +75 -0
  112. package/src/utils/agent.js +47 -0
  113. package/src/utils/data.js +92 -0
  114. package/src/utils/filter.js +266 -0
  115. package/src/utils/math.js +14 -0
  116. package/src/utils/options.js +48 -0
  117. package/src/utils/path.js +5 -0
  118. package/src/utils/resource.js +44 -0
  119. package/src/utils/route.js +53 -0
  120. package/src/utils/schema.js +1121 -0
  121. package/src/utils/type.js +81 -0
  122. package/src/utils/uid.js +15 -0
  123. package/src/utils/units.js +5 -0
  124. package/src/validators/_creditcard.js +6 -0
  125. package/src/validators/_decimals.js +11 -0
  126. package/src/validators/_domain.js +6 -0
  127. package/src/validators/_email.js +6 -0
  128. package/src/validators/_hostname.js +6 -0
  129. package/src/validators/_integer.js +6 -0
  130. package/src/validators/_max.js +6 -0
  131. package/src/validators/_min.js +6 -0
  132. package/src/validators/_password.js +5 -0
  133. package/src/validators/_range.js +6 -0
  134. package/src/validators/_required.js +9 -0
  135. package/src/validators/_url.js +6 -0
  136. package/src/validators/index.js +12 -0
  137. package/src/verbs.js +17 -0
  138. package/types/index.d.ts +3298 -0
  139. package/types/tests/admin.test-d.ts +27 -0
  140. package/types/tests/component-buttons.test-d.ts +44 -0
  141. package/types/tests/component-list.test-d.ts +159 -0
  142. package/types/tests/component-misc.test-d.ts +137 -0
  143. package/types/tests/component-object.test-d.ts +69 -0
  144. package/types/tests/component-section.test-d.ts +174 -0
  145. package/types/tests/component-select.test-d.ts +107 -0
  146. package/types/tests/components.test-d.ts +81 -0
  147. package/types/tests/context.test-d.ts +31 -0
  148. package/types/tests/fixtures.ts +24 -0
  149. package/types/tests/form.test-d.ts +109 -0
  150. package/types/tests/instance.test-d.ts +20 -0
  151. package/types/tests/schema-features.test-d.ts +402 -0
  152. package/types/tests/variance.test-d.ts +125 -0
  153. package/types/tests/view.test-d.ts +146 -0
@@ -0,0 +1,846 @@
1
+ <template lang="pug">
2
+ slot(name="prepend")
3
+ .dito-schema(
4
+ :class="{ 'dito-scroll-parent': scrollable, 'dito-schema--open': opened }"
5
+ v-bind="$attrs"
6
+ )
7
+ Teleport(
8
+ v-if="isPopulated && panelEntries.length > 0"
9
+ to=".dito-sidebar__teleport"
10
+ )
11
+ DitoPanels(
12
+ v-if="active"
13
+ :panels="panelEntries"
14
+ :data="data"
15
+ :meta="meta"
16
+ :store="store"
17
+ :disabled="disabled"
18
+ )
19
+ Teleport(
20
+ v-if="hasHeader"
21
+ :to="headerTeleport"
22
+ :disabled="!headerTeleport"
23
+ )
24
+ .dito-schema-header(
25
+ v-if="active"
26
+ )
27
+ DitoLabel(
28
+ v-if="hasLabel"
29
+ :label="label"
30
+ :info="info"
31
+ :dataPath="dataPath"
32
+ :collapsible="collapsible"
33
+ :collapsed="!opened"
34
+ @open="onOpen"
35
+ )
36
+ Transition(
37
+ v-if="tabs"
38
+ name="dito-fade"
39
+ )
40
+ DitoTabs(
41
+ v-if="opened"
42
+ v-model="selectedTab"
43
+ :tabs="tabs"
44
+ )
45
+ DitoClipboard(
46
+ v-if="clipboard"
47
+ :clipboard="clipboard"
48
+ :schema="schema"
49
+ )
50
+ slot(name="edit-buttons")
51
+ TransitionHeight(:enabled="inlined")
52
+ .dito-schema-content(
53
+ v-if="opened"
54
+ ref="content"
55
+ :class="{ 'dito-scroll': scrollable }"
56
+ )
57
+ template(
58
+ v-if="hasTabs"
59
+ )
60
+ template(
61
+ v-for="(tabSchema, tab) in tabs"
62
+ :key="tab"
63
+ )
64
+ //- TODO: Switch to v-if instead of v-show, once validation is
65
+ //- decoupled from components.
66
+ DitoPane.dito-pane__tab(
67
+ v-show="selectedTab === tab"
68
+ ref="tabs"
69
+ :tab="tab"
70
+ :schema="tabSchema"
71
+ :dataPath="dataPath"
72
+ :data="data"
73
+ :meta="meta"
74
+ :store="store"
75
+ :padding="padding"
76
+ :single="single && !inlined && !hasMainPane"
77
+ :disabled="disabled"
78
+ :compact="compact"
79
+ :generateLabels="generateLabels"
80
+ :accumulatedBasis="accumulatedBasis"
81
+ )
82
+ DitoPane.dito-pane__main(
83
+ v-if="hasMainPane"
84
+ ref="components"
85
+ :schema="schema"
86
+ :dataPath="dataPath"
87
+ :data="data"
88
+ :meta="meta"
89
+ :store="store"
90
+ :padding="padding"
91
+ :single="single && !inlined && !hasTabs"
92
+ :disabled="disabled"
93
+ :compact="compact"
94
+ :generateLabels="generateLabels"
95
+ :accumulatedBasis="accumulatedBasis"
96
+ )
97
+ slot(
98
+ v-if="!inlined && isPopulated"
99
+ name="buttons"
100
+ )
101
+ slot(
102
+ v-if="inlined && !hasHeader"
103
+ name="edit-buttons"
104
+ )
105
+ slot(name="append")
106
+ </template>
107
+
108
+ <script>
109
+ import {
110
+ isObject,
111
+ isArray,
112
+ isFunction,
113
+ isRegExp,
114
+ equals,
115
+ parseDataPath,
116
+ normalizeDataPath,
117
+ labelize
118
+ } from '@ditojs/utils'
119
+ import { TransitionHeight } from '@ditojs/ui/src'
120
+ import DitoComponent from '../DitoComponent.js'
121
+ import ContextMixin from '../mixins/ContextMixin.js'
122
+ import ItemMixin from '../mixins/ItemMixin.js'
123
+ import { appendDataPath } from '../utils/data.js'
124
+ import {
125
+ getNamedSchemas,
126
+ getPanelEntries,
127
+ setDefaultValues,
128
+ processData,
129
+ isEmptySchema,
130
+ isNested
131
+ } from '../utils/schema.js'
132
+ import { getSchemaAccessor, getStoreAccessor } from '../utils/accessor.js'
133
+
134
+ // @vue/component
135
+ export default DitoComponent.component('DitoSchema', {
136
+ mixins: [ContextMixin, ItemMixin],
137
+ components: { TransitionHeight },
138
+ inheritAttrs: false,
139
+
140
+ provide() {
141
+ return {
142
+ $schemaComponent: () => this
143
+ }
144
+ },
145
+
146
+ inject: [
147
+ '$schemaParentComponent'
148
+ ],
149
+
150
+ props: {
151
+ schema: { type: Object, required: true },
152
+ // `dataSchema` is only provided for panels, where the panel schema
153
+ // is different from the data schema for panels without own data.
154
+ dataSchema: { type: Object, default: props => props.schema },
155
+ dataPath: { type: String, default: '' },
156
+ data: { type: Object, default: null },
157
+ meta: { type: Object, default: () => ({}) },
158
+ store: { type: Object, default: () => ({}) },
159
+ label: { type: [String, Object], default: null },
160
+ info: { type: String, default: null },
161
+ single: { type: Boolean, default: false },
162
+ padding: { type: String, default: null },
163
+ active: { type: Boolean, default: true },
164
+ inlined: { type: Boolean, default: false },
165
+ disabled: { type: Boolean, default: false },
166
+ compact: { type: Boolean, default: false },
167
+ collapsed: { type: Boolean, default: false },
168
+ collapsible: { type: Boolean, default: false },
169
+ scrollable: { type: Boolean, default: false },
170
+ hasOwnData: { type: Boolean, default: false },
171
+ generateLabels: { type: Boolean, default: false },
172
+ labelNode: { type: HTMLElement, default: null },
173
+ accumulatedBasis: { type: Number, default: 1 }
174
+ },
175
+
176
+ data() {
177
+ const { data } = this.schema
178
+ return {
179
+ // Allow schema to provide more data through `schema.data`, vue-style:
180
+ ...(
181
+ data && isFunction(data)
182
+ ? data(this.context)
183
+ : data
184
+ ),
185
+ selectedTab: null,
186
+ componentsRegistry: {},
187
+ panesRegistry: {},
188
+ panelsRegistry: {},
189
+ scrollPositions: {}
190
+ }
191
+ },
192
+
193
+ computed: {
194
+ nested() {
195
+ // For `ContextMixin`:
196
+ return false
197
+ },
198
+
199
+ schemaComponent() {
200
+ // Override DitoMixin's schemaComponent() which uses the injected value.
201
+ return this
202
+ },
203
+
204
+ parentSchemaComponent() {
205
+ // Don't return the actual parent schema is this schema handles its own
206
+ // data. This prevents delegating events to the parent, and registering
207
+ // components with the parent that would cause it to set isDirty flags.
208
+ return this.hasOwnData ? null : this.parentComponent.schemaComponent
209
+ },
210
+
211
+ panelEntries() {
212
+ return getPanelEntries(this.schema.panels, this.dataPath)
213
+ },
214
+
215
+ tabs() {
216
+ return getNamedSchemas(this.schema.tabs)
217
+ },
218
+
219
+ defaultTab() {
220
+ let first = null
221
+ if (this.tabs) {
222
+ const tabs = Object.values(this.tabs).filter(this.shouldRenderSchema)
223
+ for (const { name, defaultTab } of tabs) {
224
+ if (isFunction(defaultTab) ? defaultTab(this.context) : defaultTab) {
225
+ return name
226
+ }
227
+ first ??= name
228
+ }
229
+ }
230
+ return first
231
+ },
232
+
233
+ routeTab() {
234
+ return this.$route.hash?.slice(1) || null
235
+ },
236
+
237
+ clipboard() {
238
+ return this.schema?.clipboard ?? null
239
+ },
240
+
241
+ hasHeader() {
242
+ return this.hasLabel || this.hasTabs || !!this.clipboard
243
+ },
244
+
245
+ headerTeleport() {
246
+ return this.isTopLevelSchema
247
+ ? '.dito-header__teleport'
248
+ : this.labelNode
249
+ },
250
+
251
+ // @override
252
+ processedData() {
253
+ // TODO: Fix side-effects
254
+ return this.processData({ target: 'server', schemaOnly: true })
255
+ },
256
+
257
+ clipboardData: {
258
+ get() {
259
+ // TODO: Fix side-effects
260
+ return this.processData({ target: 'clipboard', schemaOnly: true })
261
+ },
262
+
263
+ set(data) {
264
+ this.setData(data)
265
+ }
266
+ },
267
+
268
+ clipboardItem() {
269
+ return this.clipboardData
270
+ },
271
+
272
+ formLabel() {
273
+ return this.getLabel(
274
+ this.getItemFormSchema(this.sourceSchema, this.data, this.context)
275
+ )
276
+ },
277
+
278
+ isNested() {
279
+ return isNested(this.schema)
280
+ },
281
+
282
+ isDirty() {
283
+ return this.someComponent(it => it.isDirty)
284
+ },
285
+
286
+ isTouched() {
287
+ return this.someComponent(it => it.isTouched)
288
+ },
289
+
290
+ isValid() {
291
+ return this.everyComponent(it => it.isValid)
292
+ },
293
+
294
+ isValidated() {
295
+ return this.everyComponent(it => it.isValidated)
296
+ },
297
+
298
+ hasErrors() {
299
+ return this.someComponent(it => it.hasErrors)
300
+ },
301
+
302
+ hasData() {
303
+ return !!this.data
304
+ },
305
+
306
+ hasLabel() {
307
+ return !!this.label || this.collapsible
308
+ },
309
+
310
+ hasTabs() {
311
+ return !!this.tabs
312
+ },
313
+
314
+ isTopLevelSchema() {
315
+ return !this.isNested && !this.inlined
316
+ },
317
+
318
+ hasTopLevelTabs() {
319
+ return this.hasTabs && this.isTopLevelSchema
320
+ },
321
+
322
+ hasMainPane() {
323
+ const { components } = this.schema
324
+ return !!components && Object.keys(components).length > 0
325
+ },
326
+
327
+ opened: getStoreAccessor('opened', {
328
+ default() {
329
+ return !this.collapsed
330
+ }
331
+ }),
332
+
333
+ components() {
334
+ return Object.values(this.componentsRegistry)
335
+ },
336
+
337
+ panes() {
338
+ return Object.values(this.panesRegistry)
339
+ },
340
+
341
+ panels() {
342
+ return Object.values(this.panelsRegistry)
343
+ },
344
+
345
+ componentsByDataPath() {
346
+ return this._listEntriesByDataPath(this.componentsRegistry)
347
+ },
348
+
349
+ panesByDataPath() {
350
+ return this._listEntriesByDataPath(this.panesRegistry)
351
+ },
352
+
353
+ panelsByDataPath() {
354
+ return this._listEntriesByDataPath(this.panelsRegistry)
355
+ },
356
+
357
+ wide: getSchemaAccessor('wide', {
358
+ type: Boolean,
359
+ default: false
360
+ })
361
+ },
362
+
363
+ watch: {
364
+ schema: {
365
+ immediate: true,
366
+ handler(schema) {
367
+ // For forms with type depending on loaded data, we need to wait for the
368
+ // actual schema to become ready before setting up schema related things
369
+ if (!isEmptySchema(schema)) {
370
+ this.setupSchema()
371
+ }
372
+ }
373
+ },
374
+
375
+ routeTab: {
376
+ immediate: true,
377
+ // https://github.com/vuejs/vue-router/issues/3393#issuecomment-1158470149
378
+ flush: 'post',
379
+ handler(routeTab) {
380
+ // Remember the current path to know if tab changes should still be
381
+ // handled, but remove the trailing `/create` or `/:id` from it so that
382
+ // tabs informs that stay open after creation still work.
383
+ if (this.hasTopLevelTabs) {
384
+ this.selectedTab = routeTab
385
+ }
386
+ }
387
+ },
388
+
389
+ selectedTab(newTab, oldTab) {
390
+ if (this.scrollable) {
391
+ const { content } = this.$refs
392
+ this.scrollPositions[oldTab] = content.scrollTop
393
+ this.$nextTick(() => {
394
+ content.scrollTop = this.scrollPositions[newTab] ?? 0
395
+ })
396
+ }
397
+ if (this.hasTopLevelTabs) {
398
+ const tab = this.shouldRenderSchema(this.tabs[newTab])
399
+ ? newTab
400
+ : this.defaultTab
401
+ this.$router.replace({
402
+ query: this.$route.query,
403
+ hash: tab ? `#${tab}` : null
404
+ })
405
+ }
406
+ if (this.hasErrors) {
407
+ this.repositionErrors()
408
+ }
409
+ }
410
+ },
411
+
412
+ created() {
413
+ this._register(true)
414
+ if (this.scrollable && this.wide) {
415
+ this.appState.pageClass = 'dito-page--wide'
416
+ }
417
+ },
418
+
419
+ mounted() {
420
+ this.selectedTab = this.routeTab || this.defaultTab
421
+ },
422
+
423
+ unmounted() {
424
+ this.emitEvent('destroy')
425
+ this._register(false)
426
+ if (this.scrollable && this.wide) {
427
+ this.appState.pageClass = null
428
+ }
429
+ },
430
+
431
+ methods: {
432
+ setupSchema() {
433
+ this.setupSchemaFields()
434
+ // Delegate change events through to parent schema:
435
+ this.delegate('change', this.parentSchemaComponent)
436
+ this.emitEvent('initialize') // Not `'create'`, since that's for data.
437
+ },
438
+
439
+ getComponentsByDataPath(dataPath) {
440
+ return this._getEntriesByDataPath(this.componentsByDataPath, dataPath)
441
+ },
442
+
443
+ getComponentByDataPath(dataPath) {
444
+ return this.getComponentsByDataPath(dataPath)[0] || null
445
+ },
446
+
447
+ getComponentsByName(dataPath) {
448
+ return this._getEntriesByName(this.componentsByDataPath, dataPath)
449
+ },
450
+
451
+ getComponentByName(name) {
452
+ return this.getComponentsByName(name)[0] || null
453
+ },
454
+
455
+ getComponents(dataPathOrName) {
456
+ return this._getEntries(this.componentsByDataPath, dataPathOrName)
457
+ },
458
+
459
+ getComponent(dataPathOrName) {
460
+ return this.getComponents(dataPathOrName)[0] || null
461
+ },
462
+
463
+ getPanelsByDataPath(dataPath) {
464
+ return this._getEntriesByDataPath(this.panelsByDataPath, dataPath)
465
+ },
466
+
467
+ getPanelByDataPath(dataPath) {
468
+ return this.getPanelsByDataPath(dataPath)[0] || null
469
+ },
470
+
471
+ getPanels(dataPathOrName) {
472
+ return this._getEntries(this.panelsByDataPath, dataPathOrName)
473
+ },
474
+
475
+ getPanel(dataPathOrName) {
476
+ return this.getPanels(dataPathOrName)[0] || null
477
+ },
478
+
479
+ someComponent(callback) {
480
+ return this.isPopulated && this.components.some(callback)
481
+ },
482
+
483
+ everyComponent(callback) {
484
+ return this.isPopulated && this.components.every(callback)
485
+ },
486
+
487
+ onOpen(open) {
488
+ this.emitEvent('open', { context: { open } })
489
+ // Prevent closing the schema with invalid data, since the in-component
490
+ // validation will not be executed once it's closed.
491
+
492
+ // TODO: Move validation out of components, to schema, just like
493
+ // processing, and use `showValidationErrors()` for the resulting errors,
494
+ // then remove this requirement, since we can validate closed forms and
495
+ // schemas then.
496
+ if (!this.opened || open || this.validateAll()) {
497
+ this.opened = open
498
+ }
499
+ },
500
+
501
+ onChange() {
502
+ this.emitEvent('change')
503
+ },
504
+
505
+ resetValidation() {
506
+ for (const component of this.components) {
507
+ component.resetValidation()
508
+ }
509
+ },
510
+
511
+ clearErrors() {
512
+ for (const component of this.components) {
513
+ component.clearErrors()
514
+ }
515
+ },
516
+
517
+ repositionErrors() {
518
+ // Fire a fake scroll event to force the repositioning of error tooltips,
519
+ // as otherwise they sometimes don't show up in the right place initially
520
+ // when changing tabs.
521
+ const scrollContainer = this.$refs.content.closest('.dito-scroll')
522
+ const dispatch = () => scrollContainer.dispatchEvent(new Event('scroll'))
523
+ dispatch()
524
+ // This is required to handle `&--label-vertical` based layout changes.
525
+ setTimeout(dispatch, 0)
526
+ },
527
+
528
+ focus() {
529
+ this.opened = true
530
+ return this.parentSchemaComponent?.focus()
531
+ },
532
+
533
+ validateAll(match, notify = true) {
534
+ const { componentsByDataPath } = this
535
+ let dataPaths
536
+ if (match) {
537
+ const check = isFunction(match)
538
+ ? match
539
+ : isRegExp(match)
540
+ ? field => match.test(field)
541
+ : null
542
+ dataPaths = check
543
+ ? Object.keys(componentsByDataPath).filter(check)
544
+ : isArray(match)
545
+ ? match
546
+ : [match]
547
+ }
548
+ if (notify) {
549
+ this.clearErrors()
550
+ }
551
+ let isValid = true
552
+ let first = true
553
+ dataPaths ||= Object.keys(componentsByDataPath)
554
+ for (const dataPath of dataPaths) {
555
+ const components = this.getComponentsByDataPath(dataPath)
556
+ for (const component of components) {
557
+ if (!component.validate(notify)) {
558
+ // Focus first error field
559
+ if (notify && first) {
560
+ component.scrollIntoView()
561
+ }
562
+ first = false
563
+ isValid = false
564
+ }
565
+ }
566
+ }
567
+ if (notify && !isValid) {
568
+ this.notifyErrors()
569
+ }
570
+ return isValid
571
+ },
572
+
573
+ verifyAll(match) {
574
+ return this.validateAll(match, false)
575
+ },
576
+
577
+ async showValidationErrors(errors, focus, first = true) {
578
+ this.clearErrors()
579
+ const unmatched = []
580
+ const wasFirst = first
581
+ for (const [dataPath, errs] of Object.entries(errors)) {
582
+ // If the schema is a data-root, prefix its own dataPath to all errors,
583
+ // since the data that it sends and validates will be unprefixed.
584
+ const fullDataPath = this.hasOwnData
585
+ ? appendDataPath(this.dataPath, dataPath)
586
+ : dataPath
587
+ // console.log(this, this.dataPath, this.hasOwnData, fullDataPath)
588
+ // Convert from JavaScript property access notation, to our own form
589
+ // of relative JSON pointers as data-paths:
590
+ const dataPathParts = parseDataPath(fullDataPath)
591
+ let found = false
592
+ const components = this.getComponentsByDataPath(dataPathParts)
593
+ for (const component of components) {
594
+ if (component.showValidationErrors(errs, first && focus)) {
595
+ found = true
596
+ first = false
597
+ break
598
+ }
599
+ }
600
+ if (!found) {
601
+ // Couldn't find a component in an active form for the given dataPath.
602
+ // See if we can find a component serving a part of the dataPath,
603
+ // and take it from there:
604
+ const property = dataPathParts.pop()
605
+ while (dataPathParts.length > 0) {
606
+ const components = this.getComponentsByDataPath(dataPathParts)
607
+ for (const component of components) {
608
+ const navigated = await component.navigateToComponent?.(
609
+ fullDataPath,
610
+ subComponents => {
611
+ let found = false
612
+ for (const component of subComponents) {
613
+ const matched = Object.fromEntries(
614
+ Object.entries(errors).filter(
615
+ ([dataPath]) =>
616
+ normalizeDataPath(dataPath).startsWith(
617
+ component.dataPath
618
+ )
619
+ )
620
+ )
621
+ if (
622
+ Object.keys(matched).length > 0 &&
623
+ component.showValidationErrors(matched, first && focus)
624
+ ) {
625
+ found = true
626
+ first = false
627
+ break
628
+ }
629
+ }
630
+ return found
631
+ }
632
+ )
633
+ if (navigated) {
634
+ // Found a nested form to display at least parts fo the errors.
635
+ // We can't show all errors at once, so we're done. Don't call
636
+ // `notifyErrors()` yet, as we can only display it once
637
+ // `showValidationErrors()` was called from `DitoForm.mounted()`
638
+ return
639
+ }
640
+ }
641
+ // Still here, so keep removing the last part until we find a match.
642
+ dataPathParts.pop()
643
+ }
644
+ // When the error can't be matched, add it to a list of unmatched
645
+ // errors with decent message, to report at the end.
646
+ const field = labelize(property)
647
+ for (const err of errs) {
648
+ const prefix = field
649
+ ? `The field ${field}`
650
+ : `The ${this.formLabel}`
651
+ unmatched.push(`${prefix} ${err.message}`)
652
+ }
653
+ }
654
+ first = false
655
+ }
656
+ if (wasFirst && !first) {
657
+ this.notifyErrors(unmatched.join('\n'))
658
+ }
659
+ return !first
660
+ },
661
+
662
+ notifyErrors(message) {
663
+ this.notify({
664
+ type: 'error',
665
+ title: 'Validation Errors',
666
+ text: message || 'Please correct the highlighted errors.'
667
+ })
668
+ },
669
+
670
+ resetData() {
671
+ // We can't set `this.data = ...` because it's a property, but we can set
672
+ // all known properties on it to the values returned by
673
+ // `setDefaultValues()`, as they are all reactive already from the starts:
674
+ // eslint-disable-next-line vue/no-mutating-props
675
+ Object.assign(this.data, setDefaultValues(this.dataSchema, {}, this))
676
+ this.clearErrors()
677
+ },
678
+
679
+ setData(data) {
680
+ for (const name in data) {
681
+ if (name in this.data) {
682
+ if (!equals(this.data[name], data[name])) {
683
+ // eslint-disable-next-line vue/no-mutating-props
684
+ this.data[name] = data[name]
685
+ for (const component of this.getComponentsByName(name)) {
686
+ component.markDirty()
687
+ }
688
+ }
689
+ }
690
+ }
691
+ },
692
+
693
+ filterData(data) {
694
+ // Filters out arrays and objects that are backed by data resources
695
+ // themselves, as those are already taken care of through their own API
696
+ // resource end-points and shouldn't be set.
697
+ const localData = {}
698
+ const foreignData = {}
699
+ for (const [name, value] of Object.entries(data)) {
700
+ if (isArray(value) || isObject(value)) {
701
+ const components = this.getComponentsByName(name)
702
+ if (components.some(component => component.providesData)) {
703
+ foreignData[name] = value
704
+ continue
705
+ }
706
+ }
707
+ localData[name] = value
708
+ }
709
+ return { localData, foreignData }
710
+ },
711
+
712
+ processData({ target = 'clipboard', schemaOnly = true } = {}) {
713
+ return processData(
714
+ this.dataSchema,
715
+ this.sourceSchema,
716
+ this.data,
717
+ this.dataPath,
718
+ {
719
+ // Needed for DitoContext handling inside `processData` and
720
+ // `processSchemaData()`:
721
+ rootData: this.rootData,
722
+ component: this,
723
+ schemaOnly,
724
+ target
725
+ }
726
+ )
727
+ },
728
+
729
+ _register(add) {
730
+ // `$schemaParentComponent()` is only set if one of the ancestors uses
731
+ // the `SchemaParentMixin`:
732
+ this.$schemaParentComponent()?._registerSchemaComponent(this, add)
733
+ },
734
+
735
+ _registerComponent(component, add) {
736
+ this._registerEntry(this.componentsRegistry, component, add)
737
+ // Only register with the parent if schema shares data with it.
738
+ this.parentSchemaComponent?._registerComponent(component, add)
739
+ },
740
+
741
+ _registerPane(pane, add) {
742
+ this._registerEntry(this.panesRegistry, pane, add)
743
+ },
744
+
745
+ _registerPanel(panel, add) {
746
+ this._registerEntry(this.panelsRegistry, panel, add)
747
+ },
748
+
749
+ _registerEntry(registry, entry, add) {
750
+ const uid = entry.$uid
751
+ if (add) {
752
+ registry[uid] = entry
753
+ } else {
754
+ delete registry[uid]
755
+ }
756
+ },
757
+
758
+ _listEntriesByDataPath(registry) {
759
+ return Object.values(registry).reduce((entriesByDataPath, entry) => {
760
+ // Multiple entries can be linked to the same data-path, e.g. when
761
+ // there are tabs. Link each data-path to an array of entries.
762
+ const { dataPath } = entry
763
+ const entries = (entriesByDataPath[dataPath] ||= [])
764
+ entries.push(entry)
765
+ return entriesByDataPath
766
+ }, {})
767
+ },
768
+
769
+ _getEntries(entriesByDataPath, dataPath) {
770
+ return normalizeDataPath(dataPath).startsWith(this.dataPath)
771
+ ? this._getEntriesByDataPath(entriesByDataPath, dataPath)
772
+ : this._getEntriesByName(entriesByDataPath, dataPath)
773
+ },
774
+
775
+ _getEntriesByDataPath(entriesByDataPath, dataPath) {
776
+ return entriesByDataPath[normalizeDataPath(dataPath)] || []
777
+ },
778
+
779
+ _getEntriesByName(entriesByDataPath, name) {
780
+ return entriesByDataPath[appendDataPath(this.dataPath, name)] || []
781
+ }
782
+ }
783
+ })
784
+ </script>
785
+
786
+ <style lang="scss">
787
+ @import '../styles/_imports';
788
+
789
+ .dito-schema {
790
+ box-sizing: border-box;
791
+
792
+ > .dito-schema-header + .dito-schema-content > .dito-pane {
793
+ margin-top: $form-spacing-half;
794
+ }
795
+
796
+ &:has(> .dito-schema-content + .dito-edit-buttons) {
797
+ // Display the inlined edit buttons to the right of the schema:
798
+ display: flex;
799
+ flex-direction: row;
800
+ align-items: stretch;
801
+
802
+ > .dito-edit-buttons {
803
+ flex: 1 0 0%;
804
+ margin-left: $form-spacing;
805
+ }
806
+ }
807
+
808
+ > .dito-schema-content {
809
+ flex: 0 1 100%;
810
+ max-width: 100%;
811
+ // So that schema buttons can be sticky to the bottom.
812
+ // NOTE: We also need grid for `TransitionHeight` to work well. Switching
813
+ // to flex box here causes jumpy collapsing transitions.
814
+ display: grid;
815
+ grid-template-rows: min-content;
816
+ grid-template-columns: 100%;
817
+
818
+ > :only-child {
819
+ grid-row-end: none;
820
+ }
821
+ }
822
+ }
823
+
824
+ .dito-schema-header {
825
+ display: flex;
826
+ justify-content: space-between;
827
+
828
+ .dito-header & {
829
+ // When teleported into main header.
830
+ align-items: flex-end;
831
+ }
832
+
833
+ .dito-label & {
834
+ // When teleported into container label.
835
+ flex: 1;
836
+ }
837
+
838
+ > .dito-label {
839
+ margin-bottom: 0;
840
+ }
841
+
842
+ > .dito-buttons {
843
+ margin-left: var(--button-margin, 0);
844
+ }
845
+ }
846
+ </style>