@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,193 @@
1
+ <template lang="pug">
2
+ .dito-tree-list
3
+ DitoScopes(
4
+ v-if="scopes"
5
+ :query="query"
6
+ :scopes="scopes"
7
+ )
8
+ .dito-tree-panel
9
+ DitoTreeItem(
10
+ :schema="treeSchema"
11
+ :dataPath="treeDataPath"
12
+ :data="treeData"
13
+ :draggable="draggable"
14
+ :open="true"
15
+ @update:data="data => (value = data)"
16
+ )
17
+ .dito-tree-form-container(
18
+ v-if="editPath && hasEditableForms"
19
+ )
20
+ //- Include a router-view for the optional DitoFormInlined
21
+ RouterView
22
+ </template>
23
+
24
+ <script>
25
+ import DitoTypeComponent from '../DitoTypeComponent.js'
26
+ import SourceMixin from '../mixins/SourceMixin.js'
27
+ import {
28
+ hasFormSchema,
29
+ getFormSchemas,
30
+ resolveSchemaComponents
31
+ } from '../utils/schema.js'
32
+
33
+ export default DitoTypeComponent.register(
34
+ ['tree-list', 'tree-object'],
35
+ // @vue/component
36
+ {
37
+ mixins: [SourceMixin],
38
+
39
+ provide() {
40
+ return { container: this }
41
+ },
42
+
43
+ getSourceType(type) {
44
+ return type === 'tree-object' ? 'object' : 'list'
45
+ },
46
+
47
+ computed: {
48
+ path() {
49
+ // Accessed from DitoTreeItem through `container.path`:
50
+ return this.formComponent?.path
51
+ },
52
+
53
+ editPath() {
54
+ // Accessed from DitoTreeItem through `container.editPath`:
55
+ const path = this.$route.path.slice(this.path?.length)
56
+ return path.startsWith(`/${this.schema.path}`) ? path : ''
57
+ },
58
+
59
+ treeData() {
60
+ return this.isListSource
61
+ ? { [this.name]: this.value }
62
+ : this.value
63
+ },
64
+
65
+ treeDataPath() {
66
+ // Remove `name` from `dataPath`, as it is added
67
+ // to `treeData` and `treeSchema`
68
+ return this.isListSource
69
+ ? this.dataPath.slice(0, this.dataPath.length - this.name.length)
70
+ : this.dataPath
71
+ },
72
+
73
+ treeSchema() {
74
+ return this.isListSource
75
+ ? {
76
+ children: {
77
+ name: this.name,
78
+ ...this.schema
79
+ }
80
+ }
81
+ : this.schema
82
+ },
83
+
84
+ hasEditableForms() {
85
+ const hasEditableForms = schema => {
86
+ return (
87
+ hasFormSchema(schema) && (
88
+ this.getSchemaValue('editable', {
89
+ type: Boolean,
90
+ default: false,
91
+ schema
92
+ }) ||
93
+ schema.children &&
94
+ hasEditableForms(schema.children)
95
+ )
96
+ )
97
+ }
98
+ return hasEditableForms(this.schema)
99
+ }
100
+ },
101
+
102
+ async processSchema(
103
+ api,
104
+ schema,
105
+ name,
106
+ routes,
107
+ level,
108
+ nested = true,
109
+ flatten = false,
110
+ process = null
111
+ ) {
112
+ await Promise.all([
113
+ resolveSchemaComponents(schema.properties),
114
+ SourceMixin.processSchema(
115
+ api,
116
+ schema,
117
+ name,
118
+ routes,
119
+ level,
120
+ nested,
121
+ flatten,
122
+ // Pass process() to add more routes to childRoutes:
123
+ (childRoutes, level) => {
124
+ const { children } = schema
125
+ if (children) {
126
+ // Add `type` to the nested tree list.
127
+ children.type = 'tree-list'
128
+ // Recursively call `processSchema()` for the nested tree list:
129
+ return this.processSchema(
130
+ api,
131
+ children,
132
+ children.name,
133
+ childRoutes,
134
+ level,
135
+ nested,
136
+ true, // Pass `true` for `flatten` in tree lists.
137
+ process
138
+ )
139
+ }
140
+ }
141
+ )
142
+ ])
143
+ },
144
+
145
+ getFormSchemasForProcessing(schema, context) {
146
+ // Convert nested children schema to stand-alone schema component,
147
+ // present in each of the forms, as required by `processSchemaData()`
148
+ const { children } = schema
149
+ return getFormSchemas(
150
+ schema,
151
+ context,
152
+ children
153
+ ? form => ({
154
+ ...form,
155
+ components: {
156
+ ...form.components,
157
+ [children.name]: children
158
+ }
159
+ })
160
+ : null
161
+ )
162
+ }
163
+ }
164
+ )
165
+ </script>
166
+
167
+ <style lang="scss">
168
+ @import '../styles/_imports';
169
+
170
+ .dito-tree-list {
171
+ @extend %field;
172
+
173
+ .dito-tree-panel {
174
+ display: flex;
175
+ justify-content: space-between;
176
+
177
+ > .dito-tree-item {
178
+ flex: 1 1 25%;
179
+ }
180
+
181
+ > .dito-tree-form-container {
182
+ flex: 0 1 75%;
183
+ align-self: stretch;
184
+ background: $content-color-background;
185
+ border-left: $border-style;
186
+ border-top-right-radius: $border-radius - 1;
187
+ border-bottom-right-radius: $border-radius - 1;
188
+ margin: (-$input-padding-ver) (-$input-padding-hor);
189
+ margin-left: $input-padding-hor;
190
+ }
191
+ }
192
+ }
193
+ </style>
@@ -0,0 +1,503 @@
1
+ <template lang="pug">
2
+ .dito-upload
3
+ //- In order to handle upload buttons in multiple possible places, depending
4
+ //- on whether they handle single or multiple uploads, render the upload
5
+ //- component invisibly at the root, and delegate the click events to it from
6
+ //- the buttons rendered further below. Luckily this works surprisingly well.
7
+ VueUpload.dito-upload__input(
8
+ ref="upload"
9
+ v-model="uploads"
10
+ :inputId="dataPath"
11
+ :name="dataPath"
12
+ :disabled="disabled"
13
+ :postAction="uploadPath"
14
+ :extensions="extensions"
15
+ :accept="accept"
16
+ :multiple="multiple"
17
+ :size="maxSize"
18
+ :drop="$el?.closest('.dito-container')"
19
+ :dropDirectory="true"
20
+ @input-filter="onInputFilter"
21
+ @input-file="onInputFile"
22
+ )
23
+ table.dito-table.dito-table--separators.dito-table--background
24
+ //- Styling comes from `DitoTableHead`
25
+ thead.dito-table-head
26
+ tr
27
+ th
28
+ span File
29
+ th
30
+ span Size
31
+ th
32
+ span Status
33
+ th
34
+ span
35
+ DitoDraggable(
36
+ v-model="files"
37
+ as="tbody"
38
+ :options="getDraggableOptions()"
39
+ :draggable="draggable"
40
+ )
41
+ template(
42
+ v-if="multiple || !isUploadActive"
43
+ )
44
+ tr(
45
+ v-for="(file, index) in files"
46
+ :key="file.name"
47
+ )
48
+ td(
49
+ v-if="render"
50
+ v-html="renderFile(file, index)"
51
+ )
52
+ td(
53
+ v-else-if="downloadUrls[index]"
54
+ )
55
+ a(
56
+ :download="file.name"
57
+ :href="downloadUrls[index]"
58
+ target="_blank"
59
+ @click.prevent="onClickDownload(file, index)"
60
+ )
61
+ DitoUploadFile(
62
+ :file="file"
63
+ :thumbnail="thumbnails"
64
+ :thumbnailUrl="thumbnailUrls[index]"
65
+ )
66
+ td(
67
+ v-else
68
+ )
69
+ DitoUploadFile(
70
+ :file="file"
71
+ :thumbnail="thumbnails"
72
+ :thumbnailUrl="thumbnailUrls[index]"
73
+ )
74
+ td.dito-upload__size {{ formatFileSize(file.size) }}
75
+ td.dito-upload__status
76
+ template(
77
+ v-if="file.upload"
78
+ )
79
+ template(
80
+ v-if="file.upload.error"
81
+ )
82
+ | Error: {{ file.upload.error }}
83
+ template(
84
+ v-else-if="file.upload.active"
85
+ )
86
+ | Uploading...
87
+ template(
88
+ v-else-if="file.upload.success"
89
+ )
90
+ | Uploaded
91
+ template(
92
+ v-else
93
+ )
94
+ | Stored
95
+ td.dito-table__buttons
96
+ .dito-buttons.dito-buttons--round
97
+ button.dito-button.dito-button--upload(
98
+ v-if="!multiple"
99
+ :title="uploadTitle"
100
+ @click="onClickUpload"
101
+ )
102
+ //- Firefox doesn't like <button> here, so use <a> instead:
103
+ a.dito-button(
104
+ v-if="draggable"
105
+ v-bind="getButtonAttributes(verbs.drag)"
106
+ )
107
+ button.dito-button(
108
+ v-if="deletable"
109
+ type="button"
110
+ v-bind="getButtonAttributes(verbs.delete)"
111
+ @click="deleteFile(file, index)"
112
+ )
113
+ tfoot(
114
+ v-if="multiple || isUploadActive || !hasFiles"
115
+ )
116
+ tr
117
+ td(:colspan="4")
118
+ .dito-upload-footer
119
+ progress.dito-progress(
120
+ v-if="isUploadActive"
121
+ :value="uploadProgress"
122
+ max="100"
123
+ )
124
+ .dito-buttons.dito-buttons--round
125
+ button.dito-button(
126
+ v-if="isUploadActive"
127
+ type="button"
128
+ @click.prevent="upload.active = false"
129
+ ) Cancel
130
+ button.dito-button.dito-button--upload(
131
+ v-if="multiple || !hasFiles"
132
+ :title="uploadTitle"
133
+ @click="onClickUpload"
134
+ )
135
+ </template>
136
+
137
+ <script>
138
+ import DitoTypeComponent from '../DitoTypeComponent.js'
139
+ import DitoContext from '../DitoContext.js'
140
+ import SortableMixin from '../mixins/SortableMixin.js'
141
+ import parseFileSize from 'filesize-parser'
142
+ import { getSchemaAccessor } from '../utils/accessor.js'
143
+ import { formatFileSize } from '../utils/units.js'
144
+ import { appendDataPath } from '../utils/data.js'
145
+ import { isArray, asArray } from '@ditojs/utils'
146
+ import VueUpload from 'vue-upload-component'
147
+
148
+ // @vue/component
149
+ export default DitoTypeComponent.register('upload', {
150
+ mixins: [SortableMixin],
151
+ components: { VueUpload },
152
+
153
+ data() {
154
+ return {
155
+ uploads: []
156
+ }
157
+ },
158
+
159
+ computed: {
160
+ upload() {
161
+ return this.$refs.upload
162
+ },
163
+
164
+ uploadTitle() {
165
+ return this.multiple ? 'Upload Files' : 'Upload File'
166
+ },
167
+
168
+ files() {
169
+ return asFiles(this.value)
170
+ },
171
+
172
+ downloadUrls() {
173
+ return this.files.map((file, index) => this.getDownloadUrl(file, index))
174
+ },
175
+
176
+ thumbnailUrls() {
177
+ return this.files.map((file, index) => this.getThumbnailUrl(file, index))
178
+ },
179
+
180
+ multiple: getSchemaAccessor('multiple', {
181
+ type: Boolean,
182
+ default: false,
183
+ // No callback as it's used in `processValue()`
184
+ callback: false
185
+ }),
186
+
187
+ extensions: getSchemaAccessor('extensions', {
188
+ type: [Array, String, RegExp]
189
+ }),
190
+
191
+ accept: getSchemaAccessor('accept', {
192
+ type: Array,
193
+ get(accept) {
194
+ return isArray(accept) ? accept.join(',') : accept
195
+ }
196
+ }),
197
+
198
+ maxSize: getSchemaAccessor('maxSize', {
199
+ type: [String, Number],
200
+ get(maxSize) {
201
+ return maxSize ? parseFileSize(maxSize) : undefined
202
+ }
203
+ }),
204
+
205
+ draggable: getSchemaAccessor('draggable', {
206
+ type: Boolean,
207
+ default: false,
208
+ get(draggable) {
209
+ return draggable && this.files.length > 1
210
+ }
211
+ }),
212
+
213
+ deletable: getSchemaAccessor('deletable', {
214
+ type: Boolean,
215
+ default: false
216
+ }),
217
+
218
+ render: getSchemaAccessor('render', {
219
+ type: Function,
220
+ default: null
221
+ }),
222
+
223
+ thumbnails: getSchemaAccessor('thumbnails', {
224
+ type: [Boolean, String],
225
+ default(thumbnails) {
226
+ return thumbnails ?? !!this.schema.thumbnailUrl
227
+ },
228
+ get(thumbnails) {
229
+ return thumbnails === true ? 'medium' : thumbnails || null
230
+ }
231
+ }),
232
+
233
+ hasFiles() {
234
+ return this.files.length > 0
235
+ },
236
+
237
+ hasUploads() {
238
+ return this.uploads.length > 0
239
+ },
240
+
241
+ isUploadReady() {
242
+ return (
243
+ this.hasUploads &&
244
+ !(this.upload.active || this.upload.uploaded)
245
+ )
246
+ },
247
+
248
+ isUploadActive() {
249
+ return this.hasUploads && this.upload.active
250
+ },
251
+
252
+ uploadProgress() {
253
+ return (
254
+ this.uploads.reduce((total, file) => total + +file.progress, 0) /
255
+ this.uploads.length
256
+ )
257
+ },
258
+
259
+ uploadPath() {
260
+ return this.getResourceUrl({
261
+ type: 'upload',
262
+ method: 'post',
263
+ path: this.api.normalizePath(this.dataPath)
264
+ })
265
+ }
266
+ },
267
+
268
+ watch: {
269
+ isUploadReady(ready) {
270
+ if (ready) {
271
+ // Auto-upload.
272
+ this.$nextTick(() => {
273
+ this.upload.active = true
274
+ })
275
+ }
276
+ }
277
+ },
278
+
279
+ methods: {
280
+ formatFileSize,
281
+
282
+ getFileContext(file, index) {
283
+ return this.multiple
284
+ ? new DitoContext(this, {
285
+ value: file,
286
+ data: this.files,
287
+ index,
288
+ dataPath: appendDataPath(this.dataPath, index)
289
+ })
290
+ : this.context
291
+ },
292
+
293
+ renderFile(file, index) {
294
+ return this.render(this.getFileContext(file, index))
295
+ },
296
+
297
+ getDownloadUrl(file, index) {
298
+ return file.url
299
+ ? file.url
300
+ : !file.upload || file.upload.success
301
+ ? this.getSchemaValue('downloadUrl', {
302
+ type: 'String',
303
+ default: null,
304
+ context: this.getFileContext(file, index)
305
+ })
306
+ : null
307
+ },
308
+
309
+ getThumbnailUrl(file, index) {
310
+ return !file.upload || file.upload.success
311
+ ? this.getSchemaValue('thumbnailUrl', {
312
+ type: 'String',
313
+ default: null,
314
+ context: this.getFileContext(file, index)
315
+ }) || (
316
+ file.type.startsWith('image/')
317
+ ? file.url
318
+ : null
319
+ )
320
+ : null
321
+ },
322
+
323
+ deleteFile(file, index) {
324
+ const { name } = file
325
+
326
+ if (
327
+ file &&
328
+ window.confirm(
329
+ `Do you really want to ${this.verbs.remove} ${name}?`
330
+ )
331
+ ) {
332
+ if (this.multiple) {
333
+ this.value.splice(index, 1)
334
+ } else {
335
+ this.value = null
336
+ }
337
+ if (file.upload) {
338
+ this.upload.remove(file.upload)
339
+ }
340
+ this.onChange()
341
+ this.notify({
342
+ type: 'info',
343
+ title: 'Successfully Removed',
344
+ text: `${name} was ${this.verbs.deleted}.`
345
+ })
346
+ }
347
+ },
348
+
349
+ getFileIndex(file) {
350
+ return this.multiple && this.value
351
+ ? this.value.findIndex(it => it.id === file.id)
352
+ : -1
353
+ },
354
+
355
+ addFile(file) {
356
+ if (this.multiple) {
357
+ if (this.value) {
358
+ this.value.push(file)
359
+ } else {
360
+ this.value = [file]
361
+ }
362
+ } else {
363
+ this.value = file
364
+ }
365
+ },
366
+
367
+ replaceFile(file, newFile) {
368
+ if (this.multiple) {
369
+ const index = this.getFileIndex(file)
370
+ if (index >= 0) {
371
+ if (newFile) {
372
+ this.value[index] = newFile
373
+ } else {
374
+ this.value.splice(index, 1)
375
+ }
376
+ }
377
+ } else {
378
+ this.value = newFile
379
+ }
380
+ },
381
+
382
+ removeFile(file) {
383
+ this.replaceFile(file, null)
384
+ },
385
+
386
+ onInputFile(newFile, oldFile) {
387
+ if (newFile && !oldFile) {
388
+ const { id, name, size } = newFile
389
+ this.addFile({ id, name, size, upload: newFile })
390
+ }
391
+ if (newFile && oldFile) {
392
+ const { success, error } = newFile
393
+ if (success) {
394
+ this.onChange()
395
+ const file = newFile.response[0]
396
+ if (file) {
397
+ file.upload = newFile
398
+ // Replace the upload file object with the file object received
399
+ // from the upload response.
400
+ this.replaceFile(newFile, file)
401
+ } else {
402
+ this.removeFile(newFile)
403
+ }
404
+ } else if (error) {
405
+ this.removeFile(newFile)
406
+ const text = (
407
+ {
408
+ abort: 'Upload aborted',
409
+ denied: 'Upload denied',
410
+ extension: `Unsupported file-type: ${newFile.name}`,
411
+ network: 'Network error encountered during upload',
412
+ server: 'Server error occurred during upload',
413
+ size: `File is too large: ${formatFileSize(newFile.size)}`,
414
+ timeout: 'Timeout occurred during upload'
415
+ }[error] ||
416
+ `Unknown File Upload Error: '${error}'`
417
+ )
418
+ this.notify({
419
+ type: 'error',
420
+ error,
421
+ title: 'File Upload Error',
422
+ text
423
+ })
424
+ }
425
+ }
426
+ },
427
+
428
+ onInputFilter(newFile /*, oldFile, prevent */) {
429
+ const xhr = newFile?.xhr
430
+ if (this.api.cors?.credentials && xhr && !xhr.withCredentials) {
431
+ xhr.withCredentials = true
432
+ }
433
+ },
434
+
435
+ async onClickDownload(file, index) {
436
+ try {
437
+ const response = await fetch(this.downloadUrls[index])
438
+ const blob = await response.blob()
439
+ this.download({
440
+ filename: file.name,
441
+ url: URL.createObjectURL(blob)
442
+ })
443
+ } catch (error) {
444
+ console.error(error)
445
+ }
446
+ },
447
+
448
+ onClickUpload(event) {
449
+ // Delegate the click event to the hidden file input.
450
+ this.upload.$el.querySelector('input').dispatchEvent(
451
+ new event.constructor(event.type, event)
452
+ )
453
+ }
454
+ },
455
+
456
+ processValue({ schema, value }) {
457
+ // Filter out all newly added files that weren't actually uploaded.
458
+ const files = asFiles(value)
459
+ .map(({ upload, ...file }) => (!upload || upload.success ? file : null))
460
+ .filter(file => file)
461
+ return schema.multiple ? files : files[0] || null
462
+ }
463
+ })
464
+
465
+ function asFiles(value) {
466
+ return value ? asArray(value) : []
467
+ }
468
+ </script>
469
+
470
+ <style lang="scss">
471
+ @import '../styles/_imports';
472
+
473
+ .dito-upload {
474
+ .dito-table {
475
+ tr,
476
+ .dito-table__buttons {
477
+ vertical-align: middle;
478
+ }
479
+ }
480
+
481
+ &__size,
482
+ &__status {
483
+ white-space: nowrap;
484
+ }
485
+
486
+ & &__input {
487
+ // See `onClickUpload()` method for details.
488
+ display: block;
489
+ pointer-events: none;
490
+ }
491
+
492
+ &__footer {
493
+ display: flex;
494
+ justify-content: flex-end;
495
+ align-items: center;
496
+
497
+ .dito-progress {
498
+ flex: auto;
499
+ margin-right: $form-spacing;
500
+ }
501
+ }
502
+ }
503
+ </style>