@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.
- package/README.md +180 -0
- package/dist/dito-admin.css +1 -0
- package/dist/dito-admin.es.js +12106 -0
- package/dist/dito-admin.umd.js +7 -0
- package/package.json +96 -0
- package/src/DitoAdmin.js +293 -0
- package/src/DitoComponent.js +34 -0
- package/src/DitoContext.js +318 -0
- package/src/DitoTypeComponent.js +42 -0
- package/src/DitoUser.js +12 -0
- package/src/appState.js +12 -0
- package/src/components/DitoAccount.vue +60 -0
- package/src/components/DitoAffix.vue +68 -0
- package/src/components/DitoAffixes.vue +200 -0
- package/src/components/DitoButtons.vue +80 -0
- package/src/components/DitoClipboard.vue +186 -0
- package/src/components/DitoContainer.vue +374 -0
- package/src/components/DitoCreateButton.vue +146 -0
- package/src/components/DitoDialog.vue +242 -0
- package/src/components/DitoDraggable.vue +117 -0
- package/src/components/DitoEditButtons.vue +135 -0
- package/src/components/DitoErrors.vue +83 -0
- package/src/components/DitoForm.vue +521 -0
- package/src/components/DitoFormInner.vue +26 -0
- package/src/components/DitoFormNested.vue +17 -0
- package/src/components/DitoHeader.vue +84 -0
- package/src/components/DitoLabel.vue +200 -0
- package/src/components/DitoMenu.vue +186 -0
- package/src/components/DitoNavigation.vue +40 -0
- package/src/components/DitoNotifications.vue +170 -0
- package/src/components/DitoPagination.vue +42 -0
- package/src/components/DitoPane.vue +334 -0
- package/src/components/DitoPanel.vue +256 -0
- package/src/components/DitoPanels.vue +61 -0
- package/src/components/DitoRoot.vue +524 -0
- package/src/components/DitoSchema.vue +846 -0
- package/src/components/DitoSchemaInlined.vue +97 -0
- package/src/components/DitoScopes.vue +76 -0
- package/src/components/DitoSidebar.vue +50 -0
- package/src/components/DitoSpinner.vue +95 -0
- package/src/components/DitoTableCell.vue +64 -0
- package/src/components/DitoTableHead.vue +121 -0
- package/src/components/DitoTabs.vue +103 -0
- package/src/components/DitoTrail.vue +124 -0
- package/src/components/DitoTreeItem.vue +420 -0
- package/src/components/DitoUploadFile.vue +199 -0
- package/src/components/DitoVNode.vue +14 -0
- package/src/components/DitoView.vue +143 -0
- package/src/components/index.js +42 -0
- package/src/directives/resize.js +83 -0
- package/src/index.js +1 -0
- package/src/mixins/ContextMixin.js +68 -0
- package/src/mixins/DataMixin.js +131 -0
- package/src/mixins/DitoMixin.js +591 -0
- package/src/mixins/DomMixin.js +29 -0
- package/src/mixins/EmitterMixin.js +158 -0
- package/src/mixins/ItemMixin.js +144 -0
- package/src/mixins/LoadingMixin.js +23 -0
- package/src/mixins/NumberMixin.js +118 -0
- package/src/mixins/OptionsMixin.js +304 -0
- package/src/mixins/PulldownMixin.js +63 -0
- package/src/mixins/ResourceMixin.js +398 -0
- package/src/mixins/RouteMixin.js +190 -0
- package/src/mixins/SchemaParentMixin.js +33 -0
- package/src/mixins/SortableMixin.js +49 -0
- package/src/mixins/SourceMixin.js +734 -0
- package/src/mixins/TextMixin.js +26 -0
- package/src/mixins/TypeMixin.js +280 -0
- package/src/mixins/ValidationMixin.js +119 -0
- package/src/mixins/ValidatorMixin.js +57 -0
- package/src/mixins/ValueMixin.js +31 -0
- package/src/styles/_base.scss +17 -0
- package/src/styles/_button.scss +191 -0
- package/src/styles/_imports.scss +3 -0
- package/src/styles/_info.scss +19 -0
- package/src/styles/_layout.scss +19 -0
- package/src/styles/_pulldown.scss +38 -0
- package/src/styles/_scroll.scss +13 -0
- package/src/styles/_settings.scss +88 -0
- package/src/styles/_table.scss +223 -0
- package/src/styles/_tippy.scss +45 -0
- package/src/styles/style.scss +9 -0
- package/src/types/DitoTypeButton.vue +143 -0
- package/src/types/DitoTypeCheckbox.vue +27 -0
- package/src/types/DitoTypeCheckboxes.vue +65 -0
- package/src/types/DitoTypeCode.vue +199 -0
- package/src/types/DitoTypeColor.vue +272 -0
- package/src/types/DitoTypeComponent.vue +31 -0
- package/src/types/DitoTypeComputed.vue +50 -0
- package/src/types/DitoTypeDate.vue +99 -0
- package/src/types/DitoTypeLabel.vue +23 -0
- package/src/types/DitoTypeList.vue +364 -0
- package/src/types/DitoTypeMarkup.vue +700 -0
- package/src/types/DitoTypeMultiselect.vue +522 -0
- package/src/types/DitoTypeNumber.vue +66 -0
- package/src/types/DitoTypeObject.vue +136 -0
- package/src/types/DitoTypePanel.vue +18 -0
- package/src/types/DitoTypeProgress.vue +40 -0
- package/src/types/DitoTypeRadio.vue +45 -0
- package/src/types/DitoTypeSection.vue +80 -0
- package/src/types/DitoTypeSelect.vue +133 -0
- package/src/types/DitoTypeSlider.vue +66 -0
- package/src/types/DitoTypeSpacer.vue +11 -0
- package/src/types/DitoTypeSwitch.vue +40 -0
- package/src/types/DitoTypeText.vue +101 -0
- package/src/types/DitoTypeTextarea.vue +48 -0
- package/src/types/DitoTypeTreeList.vue +193 -0
- package/src/types/DitoTypeUpload.vue +503 -0
- package/src/types/index.js +30 -0
- package/src/utils/SchemaGraph.js +147 -0
- package/src/utils/accessor.js +75 -0
- package/src/utils/agent.js +47 -0
- package/src/utils/data.js +92 -0
- package/src/utils/filter.js +266 -0
- package/src/utils/math.js +14 -0
- package/src/utils/options.js +48 -0
- package/src/utils/path.js +5 -0
- package/src/utils/resource.js +44 -0
- package/src/utils/route.js +53 -0
- package/src/utils/schema.js +1121 -0
- package/src/utils/type.js +81 -0
- package/src/utils/uid.js +15 -0
- package/src/utils/units.js +5 -0
- package/src/validators/_creditcard.js +6 -0
- package/src/validators/_decimals.js +11 -0
- package/src/validators/_domain.js +6 -0
- package/src/validators/_email.js +6 -0
- package/src/validators/_hostname.js +6 -0
- package/src/validators/_integer.js +6 -0
- package/src/validators/_max.js +6 -0
- package/src/validators/_min.js +6 -0
- package/src/validators/_password.js +5 -0
- package/src/validators/_range.js +6 -0
- package/src/validators/_required.js +9 -0
- package/src/validators/_url.js +6 -0
- package/src/validators/index.js +12 -0
- package/src/verbs.js +17 -0
- package/types/index.d.ts +3298 -0
- package/types/tests/admin.test-d.ts +27 -0
- package/types/tests/component-buttons.test-d.ts +44 -0
- package/types/tests/component-list.test-d.ts +159 -0
- package/types/tests/component-misc.test-d.ts +137 -0
- package/types/tests/component-object.test-d.ts +69 -0
- package/types/tests/component-section.test-d.ts +174 -0
- package/types/tests/component-select.test-d.ts +107 -0
- package/types/tests/components.test-d.ts +81 -0
- package/types/tests/context.test-d.ts +31 -0
- package/types/tests/fixtures.ts +24 -0
- package/types/tests/form.test-d.ts +109 -0
- package/types/tests/instance.test-d.ts +20 -0
- package/types/tests/schema-features.test-d.ts +402 -0
- package/types/tests/variance.test-d.ts +125 -0
- 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>
|