@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,199 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.dito-upload-file
|
|
3
|
+
.dito-thumbnail(
|
|
4
|
+
v-if="thumbnail"
|
|
5
|
+
:class="`dito-thumbnail--${thumbnail}`"
|
|
6
|
+
)
|
|
7
|
+
.dito-thumbnail__inner
|
|
8
|
+
img(
|
|
9
|
+
v-if="source"
|
|
10
|
+
:src="source"
|
|
11
|
+
crossorigin="anonymous"
|
|
12
|
+
)
|
|
13
|
+
.dito-thumbnail__type(
|
|
14
|
+
v-else
|
|
15
|
+
)
|
|
16
|
+
span {{ type }}
|
|
17
|
+
span {{ file.name }}
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script>
|
|
21
|
+
import DitoComponent from '../DitoComponent.js'
|
|
22
|
+
|
|
23
|
+
// @vue/component
|
|
24
|
+
export default DitoComponent.component('DitoUploadFile', {
|
|
25
|
+
props: {
|
|
26
|
+
file: { type: Object, required: true },
|
|
27
|
+
thumbnail: { type: String, default: null },
|
|
28
|
+
thumbnailUrl: { type: String, default: null }
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
data() {
|
|
32
|
+
return {
|
|
33
|
+
uploadUrl: null
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
computed: {
|
|
38
|
+
type() {
|
|
39
|
+
return (
|
|
40
|
+
TYPES[this.file.type] ||
|
|
41
|
+
this.file.name.split('.').pop().toUpperCase()
|
|
42
|
+
)
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
source() {
|
|
46
|
+
return this.uploadUrl || this.thumbnailUrl
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
watch: {
|
|
51
|
+
'file.upload.file': {
|
|
52
|
+
immediate: true,
|
|
53
|
+
handler(file) {
|
|
54
|
+
if (this.thumbnail && file?.type.startsWith('image/')) {
|
|
55
|
+
const reader = new FileReader()
|
|
56
|
+
reader.onload = () => {
|
|
57
|
+
this.uploadUrl = reader.result
|
|
58
|
+
}
|
|
59
|
+
reader.readAsDataURL(file)
|
|
60
|
+
} else {
|
|
61
|
+
this.uploadUrl = null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const TYPES = {
|
|
69
|
+
'text/plain': 'TXT',
|
|
70
|
+
'text/html': 'HTML',
|
|
71
|
+
'text/css': 'CSS',
|
|
72
|
+
'text/javascript': 'JS',
|
|
73
|
+
'image/jpeg': 'JPG',
|
|
74
|
+
'image/png': 'PNG',
|
|
75
|
+
'image/gif': 'GIF',
|
|
76
|
+
'image/svg+xml': 'SVG',
|
|
77
|
+
'movie/mp4': 'MP4',
|
|
78
|
+
'audio/mpeg': 'MP3',
|
|
79
|
+
'application/json': 'JSON',
|
|
80
|
+
'application/xml': 'XML',
|
|
81
|
+
'application/pdf': 'PDF',
|
|
82
|
+
'application/zip': 'ZIP'
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<style lang="scss">
|
|
87
|
+
@use 'sass:math';
|
|
88
|
+
@import '../styles/_imports';
|
|
89
|
+
|
|
90
|
+
.dito-upload-file {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: flex-start;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.dito-thumbnail {
|
|
97
|
+
$self: &;
|
|
98
|
+
|
|
99
|
+
// Small size by default
|
|
100
|
+
--max-size: #{1 * $input-height};
|
|
101
|
+
--corner-size: calc(var(--max-size) / 5);
|
|
102
|
+
--shadow-size: 1px;
|
|
103
|
+
--min-size: calc(2 * var(--corner-size));
|
|
104
|
+
--margin: 0em;
|
|
105
|
+
--drop-shadow: drop-shadow(
|
|
106
|
+
0 calc(var(--shadow-size) * 0.75) var(--shadow-size)
|
|
107
|
+
#{rgba($color-black, 0.4)}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
position: relative;
|
|
111
|
+
margin: var(--margin);
|
|
112
|
+
margin-right: 0.5em;
|
|
113
|
+
filter: var(--drop-shadow);
|
|
114
|
+
|
|
115
|
+
&--small {
|
|
116
|
+
--max-size: #{1 * $input-height};
|
|
117
|
+
--margin: 0em;
|
|
118
|
+
--shadow-size: 1px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&--medium {
|
|
122
|
+
--max-size: #{2 * $input-height};
|
|
123
|
+
--margin: 0.25em;
|
|
124
|
+
--shadow-size: 1.5px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
&--large {
|
|
128
|
+
--max-size: #{4 * $input-height};
|
|
129
|
+
--margin: 0.5em;
|
|
130
|
+
--shadow-size: 2.5px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
&__inner {
|
|
134
|
+
background: #ffffff;
|
|
135
|
+
clip-path: polygon(
|
|
136
|
+
0 0,
|
|
137
|
+
calc(100% - var(--corner-size)) 0,
|
|
138
|
+
100% var(--corner-size),
|
|
139
|
+
100% 100%,
|
|
140
|
+
0 100%
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
&::after {
|
|
144
|
+
content: '';
|
|
145
|
+
position: absolute;
|
|
146
|
+
top: 0;
|
|
147
|
+
right: 0;
|
|
148
|
+
width: var(--corner-size);
|
|
149
|
+
height: var(--corner-size);
|
|
150
|
+
background: linear-gradient(45deg, #ffffff, #eeeeee 40%, #dddddd 50%);
|
|
151
|
+
filter: var(--drop-shadow);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
&__type {
|
|
156
|
+
--font-size: var(--corner-size);
|
|
157
|
+
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
min-width: var(--min-size);
|
|
162
|
+
min-height: var(--max-size);
|
|
163
|
+
aspect-ratio: 3 / 4;
|
|
164
|
+
|
|
165
|
+
span {
|
|
166
|
+
--color: #{$color-grey};
|
|
167
|
+
|
|
168
|
+
font-size: min(var(--font-size), #{1.25 * $font-size});
|
|
169
|
+
color: var(--color);
|
|
170
|
+
|
|
171
|
+
#{$self}:not(#{$self}--small) & {
|
|
172
|
+
padding: 0 calc(var(--font-size) / 4);
|
|
173
|
+
border-radius: calc(var(--font-size) / 4);
|
|
174
|
+
background: var(--color);
|
|
175
|
+
color: #ffffff;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#{$self}--medium & {
|
|
179
|
+
--color: #{$color-light};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#{$self}--large & {
|
|
183
|
+
--color: #{$color-lighter};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
img {
|
|
189
|
+
display: block;
|
|
190
|
+
// SVG images need 100% settings to scale into the container.
|
|
191
|
+
width: 100%;
|
|
192
|
+
height: 100%;
|
|
193
|
+
min-width: var(--min-size);
|
|
194
|
+
min-height: var(--min-size);
|
|
195
|
+
max-width: var(--max-size);
|
|
196
|
+
max-height: var(--max-size);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import DitoComponent from '../DitoComponent.js'
|
|
3
|
+
|
|
4
|
+
// @vue/component
|
|
5
|
+
function DitoVNode({ vnode }) {
|
|
6
|
+
return vnode
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
DitoVNode.props = {
|
|
10
|
+
vnode: { type: Object, required: true }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default DitoComponent.component('DitoVnode', DitoVNode)
|
|
14
|
+
</script>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
template(
|
|
3
|
+
v-if="user && shouldRenderSchema(viewSchema)"
|
|
4
|
+
)
|
|
5
|
+
//- Only render DitoView when it is active, otherwise a normal router-view
|
|
6
|
+
//- instead, to nest further route components.
|
|
7
|
+
//- NOTE: This is different from the handling in DitoForm, where `v-show` is
|
|
8
|
+
//- used to always render forms even when other nested forms are present.
|
|
9
|
+
RouterView(
|
|
10
|
+
v-if="!isLastRoute"
|
|
11
|
+
:key="name"
|
|
12
|
+
)
|
|
13
|
+
.dito-view.dito-scroll-parent(
|
|
14
|
+
v-else
|
|
15
|
+
:data-resource="sourceSchema.path"
|
|
16
|
+
)
|
|
17
|
+
DitoSchema(
|
|
18
|
+
:key="name"
|
|
19
|
+
:schema="viewSchema"
|
|
20
|
+
:data="data"
|
|
21
|
+
:meta="meta"
|
|
22
|
+
:store="getChildStore(name)"
|
|
23
|
+
padding="root"
|
|
24
|
+
:disabled="isLoading"
|
|
25
|
+
scrollable
|
|
26
|
+
single
|
|
27
|
+
)
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script>
|
|
31
|
+
import DitoComponent from '../DitoComponent.js'
|
|
32
|
+
import RouteMixin from '../mixins/RouteMixin.js'
|
|
33
|
+
import {
|
|
34
|
+
isSingleComponentView,
|
|
35
|
+
someNestedSchemaComponent
|
|
36
|
+
} from '../utils/schema.js'
|
|
37
|
+
import { hasResource } from '../utils/resource.js'
|
|
38
|
+
|
|
39
|
+
// @vue/component
|
|
40
|
+
export default DitoComponent.component('DitoView', {
|
|
41
|
+
mixins: [RouteMixin],
|
|
42
|
+
|
|
43
|
+
provide() {
|
|
44
|
+
// Redirect $sourceComponent and $resourceComponent to the main component:
|
|
45
|
+
return {
|
|
46
|
+
$sourceComponent: () => this.mainComponent?.sourceComponent || null,
|
|
47
|
+
$resourceComponent: () => this.mainComponent?.resourceComponent || null
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
data() {
|
|
52
|
+
return {
|
|
53
|
+
// Updated from LoadingMixin through `setLoading(isLoading)`:
|
|
54
|
+
isLoading: false,
|
|
55
|
+
// NOTE: Data is shared across all views because the router recycles the
|
|
56
|
+
// DitoView component.
|
|
57
|
+
data: {}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
computed: {
|
|
62
|
+
schema() {
|
|
63
|
+
return this.meta.schema ?? {}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
name() {
|
|
67
|
+
return this.schema.name
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
isView() {
|
|
71
|
+
return true
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
isSingleComponentView() {
|
|
75
|
+
return isSingleComponentView(this.schema)
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
mainComponent() {
|
|
79
|
+
return this.mainSchemaComponent?.getComponentByDataPath(this.name)
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
viewSchema() {
|
|
83
|
+
const { component, ...schema } = this.schema
|
|
84
|
+
// Translate single-component schemas into multi-component schemas,
|
|
85
|
+
// so they can be rendered directly through DitoSchema also:
|
|
86
|
+
return this.isSingleComponentView
|
|
87
|
+
? {
|
|
88
|
+
...schema,
|
|
89
|
+
components: {
|
|
90
|
+
[schema.name]: {
|
|
91
|
+
name: schema.name,
|
|
92
|
+
label: false,
|
|
93
|
+
...component
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
: schema
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
providesData() {
|
|
101
|
+
return someNestedSchemaComponent(this.viewSchema, hasResource)
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
watch: {
|
|
106
|
+
$route: {
|
|
107
|
+
// https://github.com/vuejs/vue-router/issues/3393#issuecomment-1158470149
|
|
108
|
+
flush: 'post',
|
|
109
|
+
handler(to, from) {
|
|
110
|
+
// See if the route changes completely, and clear the data if it does.
|
|
111
|
+
if (this.isFullRouteChange(to, from)) {
|
|
112
|
+
this.isLoading = false
|
|
113
|
+
this.data = {}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
mounted() {
|
|
120
|
+
// Prevent bypassing of if-condition by direct URL access.
|
|
121
|
+
if (!this.shouldRenderSchema(this.viewSchema)) {
|
|
122
|
+
this.$router.replace({ path: '/' })
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
methods: {
|
|
127
|
+
setData(data) {
|
|
128
|
+
this.data = data
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
getChildPath(path) {
|
|
132
|
+
// Lists inside single-component views use the view's path for sub-paths:
|
|
133
|
+
return this.isSingleComponentView
|
|
134
|
+
? this.path
|
|
135
|
+
: `${this.path}/${path}`
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
setLoading(isLoading) {
|
|
139
|
+
this.isLoading = !!isLoading
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
</script>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// NOTE: index.js exports nothing, but Dito.js components will be registered in
|
|
2
|
+
// DitoComponent and can be rendered through their tag-names.
|
|
3
|
+
// NOTE: Sequence is meaningful for reasons of CSS declaration sequence. The
|
|
4
|
+
// convention is in order of encountered hierarchy in the DOM.
|
|
5
|
+
|
|
6
|
+
export { default as DitoRoot } from './DitoRoot.vue'
|
|
7
|
+
export { default as DitoMenu } from './DitoMenu.vue'
|
|
8
|
+
export { default as DitoTrail } from './DitoTrail.vue'
|
|
9
|
+
export { default as DitoHeader } from './DitoHeader.vue'
|
|
10
|
+
export { default as DitoNavigation } from './DitoNavigation.vue'
|
|
11
|
+
export { default as DitoNotifications } from './DitoNotifications.vue'
|
|
12
|
+
export { default as DitoSidebar } from './DitoSidebar.vue'
|
|
13
|
+
export { default as DitoAccount } from './DitoAccount.vue'
|
|
14
|
+
export { default as DitoDialog } from './DitoDialog.vue'
|
|
15
|
+
export { default as DitoLabel } from './DitoLabel.vue'
|
|
16
|
+
export { default as DitoSchema } from './DitoSchema.vue'
|
|
17
|
+
export { default as DitoSchemaInlined } from './DitoSchemaInlined.vue'
|
|
18
|
+
export { default as DitoPane } from './DitoPane.vue'
|
|
19
|
+
export { default as DitoContainer } from './DitoContainer.vue'
|
|
20
|
+
export { default as DitoTabs } from './DitoTabs.vue'
|
|
21
|
+
export { default as DitoPanel } from './DitoPanel.vue'
|
|
22
|
+
export { default as DitoPanels } from './DitoPanels.vue'
|
|
23
|
+
export { default as DitoButtons } from './DitoButtons.vue'
|
|
24
|
+
export { default as DitoEditButtons } from './DitoEditButtons.vue'
|
|
25
|
+
export { default as DitoCreateButton } from './DitoCreateButton.vue'
|
|
26
|
+
export { default as DitoClipboard } from './DitoClipboard.vue'
|
|
27
|
+
export { default as DitoView } from './DitoView.vue'
|
|
28
|
+
export { default as DitoForm } from './DitoForm.vue'
|
|
29
|
+
export { default as DitoFormInner } from './DitoFormInner.vue'
|
|
30
|
+
export { default as DitoFormNested } from './DitoFormNested.vue'
|
|
31
|
+
export { default as DitoErrors } from './DitoErrors.vue'
|
|
32
|
+
export { default as DitoScopes } from './DitoScopes.vue'
|
|
33
|
+
export { default as DitoPagination } from './DitoPagination.vue'
|
|
34
|
+
export { default as DitoTreeItem } from './DitoTreeItem.vue'
|
|
35
|
+
export { default as DitoTableHead } from './DitoTableHead.vue'
|
|
36
|
+
export { default as DitoTableCell } from './DitoTableCell.vue'
|
|
37
|
+
export { default as DitoUploadFile } from './DitoUploadFile.vue'
|
|
38
|
+
export { default as DitoDraggable } from './DitoDraggable.vue'
|
|
39
|
+
export { default as DitoSpinner } from './DitoSpinner.vue'
|
|
40
|
+
export { default as DitoAffix } from './DitoAffix.vue'
|
|
41
|
+
export { default as DitoAffixes } from './DitoAffixes.vue'
|
|
42
|
+
export { default as DitoVNode } from './DitoVNode.vue'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { asArray } from '@ditojs/utils'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
mounted(node, binding) {
|
|
5
|
+
observeResize(node, binding.value, binding.arg)
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
unmounted(node, binding) {
|
|
9
|
+
unobserveResize(node, binding.value, binding.arg)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function observeResize(node, handler, options) {
|
|
14
|
+
Observer.getObserver(options).observe(node, handler)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function unobserveResize(node, handler, options) {
|
|
18
|
+
Observer.getObserver(options).unobserve(node, handler)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const isResizeSupported = typeof ResizeObserver !== 'undefined'
|
|
22
|
+
|
|
23
|
+
const observers = {}
|
|
24
|
+
|
|
25
|
+
class Observer {
|
|
26
|
+
constructor(key, options) {
|
|
27
|
+
this.key = key
|
|
28
|
+
this.options = options
|
|
29
|
+
this.observer = isResizeSupported
|
|
30
|
+
? new ResizeObserver(entries => this.handle(entries))
|
|
31
|
+
: null
|
|
32
|
+
this.handlersByNode = new WeakMap()
|
|
33
|
+
this.nodeCount = 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
observe(node, handler) {
|
|
37
|
+
let handlers = this.handlersByNode.get(node)
|
|
38
|
+
if (!handlers) {
|
|
39
|
+
handlers = new Set()
|
|
40
|
+
this.handlersByNode.set(node, handlers)
|
|
41
|
+
this.observer?.observe(node, this.options)
|
|
42
|
+
this.nodeCount++
|
|
43
|
+
}
|
|
44
|
+
handlers.add(handler)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
unobserve(node, handler) {
|
|
48
|
+
const handlers = this.handlersByNode.get(node)
|
|
49
|
+
if (handlers?.delete(handler) && handlers.size === 0) {
|
|
50
|
+
this.handlersByNode.delete(node)
|
|
51
|
+
this.observer?.unobserve(node)
|
|
52
|
+
if (--this.nodeCount === 0) {
|
|
53
|
+
delete observers[this.key]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
handle(entries) {
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const handlers = this.handlersByNode.get(entry.target)
|
|
61
|
+
if (handlers) {
|
|
62
|
+
const event = {
|
|
63
|
+
target: entry.target,
|
|
64
|
+
contentRect: entry.contentRect,
|
|
65
|
+
// Use `asArray` since Firefox before v92 returns these as objects:
|
|
66
|
+
borderBoxSize: asArray(entry.borderBoxSize),
|
|
67
|
+
contentBoxSize: asArray(entry.contentBoxSize),
|
|
68
|
+
devicePixelContentBoxSize: asArray(entry.devicePixelContentBoxSize)
|
|
69
|
+
}
|
|
70
|
+
for (const handler of handlers) {
|
|
71
|
+
handler(event)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static getObserver({ box = 'content-box' } = {}) {
|
|
78
|
+
const options = { box }
|
|
79
|
+
const key = JSON.stringify(options)
|
|
80
|
+
observers[key] ||= new Observer(key, options)
|
|
81
|
+
return observers[key]
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './DitoAdmin.js'
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import DitoContext from '../DitoContext.js'
|
|
2
|
+
import { getItem, getParentItem } from '../utils/data.js'
|
|
3
|
+
|
|
4
|
+
// @vue/component
|
|
5
|
+
export default {
|
|
6
|
+
computed: {
|
|
7
|
+
context() {
|
|
8
|
+
return new DitoContext(this, { nested: this.nested })
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
// The following computed properties are similar to `DitoContext`
|
|
12
|
+
// properties, so that we can access these on `this` as well:
|
|
13
|
+
// NOTE: While internally, we speak of `data`, in the API surface the
|
|
14
|
+
// term `item` is used for the data that relates to editing objects.
|
|
15
|
+
// NOTE: This should always return the same as:
|
|
16
|
+
// return getItem(this.rootData, this.dataPath, false)
|
|
17
|
+
parentData() {
|
|
18
|
+
const data = getParentItem(this.rootData, this.dataPath, this.nested)
|
|
19
|
+
return data !== this.data ? data : null
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// @overridable
|
|
23
|
+
processedData() {
|
|
24
|
+
return getProcessedParentData(this, this.dataPath, this.nested)
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
processedRootData() {
|
|
28
|
+
return getProcessedParentData(this, '', false)
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
item() {
|
|
32
|
+
return this.data
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
parentItem() {
|
|
36
|
+
return this.parentData
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
rootItem() {
|
|
40
|
+
return this.rootData
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
processedItem() {
|
|
44
|
+
return this.processedData
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
processedRootItem() {
|
|
48
|
+
return this.processedRootData
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getProcessedParentData(component, dataPath, nested = false) {
|
|
54
|
+
// We can only get the processed data through the schemaComponent, but
|
|
55
|
+
// that's not necessarily the item represented by this component.
|
|
56
|
+
// Solution: Find the relative path and the processed sub-item from there:
|
|
57
|
+
let { schemaComponent } = component
|
|
58
|
+
// Find the schema component that contains the desired data-path:
|
|
59
|
+
while (schemaComponent.dataPath.length > dataPath.length) {
|
|
60
|
+
schemaComponent = schemaComponent.parentSchemaComponent
|
|
61
|
+
}
|
|
62
|
+
return getItem(
|
|
63
|
+
schemaComponent.processedData,
|
|
64
|
+
// Get the dataPath relative to the schemaComponent's data:
|
|
65
|
+
dataPath.slice(schemaComponent.dataPath.length),
|
|
66
|
+
nested
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isObject,
|
|
3
|
+
isFunction,
|
|
4
|
+
isPromise,
|
|
5
|
+
normalizeDataPath,
|
|
6
|
+
getValueAtDataPath
|
|
7
|
+
} from '@ditojs/utils'
|
|
8
|
+
import { markRaw, ref } from 'vue'
|
|
9
|
+
import LoadingMixin from './LoadingMixin.js'
|
|
10
|
+
|
|
11
|
+
// @vue/component
|
|
12
|
+
export default {
|
|
13
|
+
mixins: [LoadingMixin],
|
|
14
|
+
|
|
15
|
+
data() {
|
|
16
|
+
return {
|
|
17
|
+
isLoading: false,
|
|
18
|
+
asyncDataEntries: markRaw({})
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
methods: {
|
|
23
|
+
handleDataSchema(schema, name, loadingOptions) {
|
|
24
|
+
if (!isObject(schema)) {
|
|
25
|
+
schema = { data: schema }
|
|
26
|
+
}
|
|
27
|
+
let { data = undefined, dataPath = null } = schema
|
|
28
|
+
// Create a reactive entry for the async data, if it doesn't exist yet.
|
|
29
|
+
// NOTE: `markRaw()` is used to avoid reactivity on `asyncDataEntries`
|
|
30
|
+
// itself, as reactivity is only desired on `reactiveVersion`, which is
|
|
31
|
+
// used to trigger controlled reevaluation of the computed getter.
|
|
32
|
+
const asyncEntry = (this.asyncDataEntries[name] ??= {
|
|
33
|
+
reactiveVersion: ref(1),
|
|
34
|
+
dependencyFunction: null,
|
|
35
|
+
resolvedData: undefined,
|
|
36
|
+
resolving: false,
|
|
37
|
+
resolved: false
|
|
38
|
+
})
|
|
39
|
+
// If the data callback provided a dependency function when it was called,
|
|
40
|
+
// cal it in every call of `handleDataSchema()` to force Vue to keep track
|
|
41
|
+
// of the async dependencies. Also access `reactiveVersion.value` right
|
|
42
|
+
// away, to ensure that the reactive property is tracked as a dependency:
|
|
43
|
+
asyncEntry.reactiveVersion.value &&
|
|
44
|
+
asyncEntry.dependencyFunction?.(this.context)
|
|
45
|
+
|
|
46
|
+
if (asyncEntry.resolved) {
|
|
47
|
+
// If the data was resolved already, return it and clear the resolved
|
|
48
|
+
// value. This works because Vue caches the result of computed getters
|
|
49
|
+
// and only reevaluates if one of the dependencies changed. This is to
|
|
50
|
+
// ensure that a cached value here doesn't block / override
|
|
51
|
+
// reevaluation when a dependency changes:
|
|
52
|
+
const { resolvedData } = asyncEntry
|
|
53
|
+
asyncEntry.resolvedData = undefined
|
|
54
|
+
asyncEntry.resolved = false
|
|
55
|
+
return resolvedData
|
|
56
|
+
}
|
|
57
|
+
// Avoid calling the data function twice:
|
|
58
|
+
if (asyncEntry.resolving) {
|
|
59
|
+
data = null
|
|
60
|
+
} else if (data) {
|
|
61
|
+
if (isFunction(data)) {
|
|
62
|
+
const result = data(this.context)
|
|
63
|
+
// If the result of the data function is another function, then the
|
|
64
|
+
// first data function is there to track dependencies and the real
|
|
65
|
+
// data loading happens in the function that it returned. Keep track
|
|
66
|
+
// it in `dependencyFunction` so it can be called on each call of
|
|
67
|
+
// `handleDataSchema()` to keep the dependencies intact, and call
|
|
68
|
+
// the function that it returned once to get the actual data:
|
|
69
|
+
if (isFunction(result)) {
|
|
70
|
+
asyncEntry.dependencyFunction = data
|
|
71
|
+
data = result(this.context)
|
|
72
|
+
} else {
|
|
73
|
+
data = result
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// NOTE: If the data is not a promise, it is resolved already.
|
|
77
|
+
if (isPromise(data)) {
|
|
78
|
+
// If the data is asynchronous, it can't be returned straight away.
|
|
79
|
+
// But we can cheat using computed properties and `resolvedData`,
|
|
80
|
+
// which is going to receive the loaded data asynchronously,
|
|
81
|
+
// triggering a recompute of the computed property that calls
|
|
82
|
+
// `handleDataSchema()`.
|
|
83
|
+
asyncEntry.resolving = true
|
|
84
|
+
this.resolveData(data, loadingOptions)
|
|
85
|
+
.then(data => {
|
|
86
|
+
asyncEntry.resolvedData = data
|
|
87
|
+
asyncEntry.resolving = false
|
|
88
|
+
asyncEntry.resolved = true
|
|
89
|
+
// Trigger reevaluation of the computed getter by increasing the
|
|
90
|
+
// `reactiveVersion` value.
|
|
91
|
+
asyncEntry.reactiveVersion.value++
|
|
92
|
+
})
|
|
93
|
+
.catch(error => {
|
|
94
|
+
console.error(error)
|
|
95
|
+
asyncEntry.resolving = false
|
|
96
|
+
})
|
|
97
|
+
// Clear data until promise is resolved and `resolvedData` is set
|
|
98
|
+
data = null
|
|
99
|
+
}
|
|
100
|
+
} else if (dataPath) {
|
|
101
|
+
data = getValueAtDataPath(
|
|
102
|
+
this.rootData,
|
|
103
|
+
normalizeDataPath(`${this.dataPath}/${dataPath}`)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
return data
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async resolveData(load, loadingOptions = {}) {
|
|
110
|
+
// Use a timeout to allow already resolved promises to return data without
|
|
111
|
+
// showing a loading indicator.
|
|
112
|
+
let clearLoading = false
|
|
113
|
+
const timer = setTimeout(() => {
|
|
114
|
+
this.setLoading(true, loadingOptions)
|
|
115
|
+
clearLoading = true
|
|
116
|
+
}, 0)
|
|
117
|
+
let data = null
|
|
118
|
+
try {
|
|
119
|
+
data = await (isFunction(load) ? load() : load)
|
|
120
|
+
} catch (error) {
|
|
121
|
+
this.addError(error.message || error)
|
|
122
|
+
}
|
|
123
|
+
if (clearLoading) {
|
|
124
|
+
this.setLoading(false, loadingOptions)
|
|
125
|
+
} else {
|
|
126
|
+
clearTimeout(timer)
|
|
127
|
+
}
|
|
128
|
+
return data
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|