@ditojs/admin 2.4.0 → 2.4.1

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.
@@ -6,5 +6,6 @@
6
6
  @import '_scroll';
7
7
  @import '_sortable';
8
8
  @import '_table';
9
- @import '_notifications';
10
9
  @import '_info';
10
+ @import '_tippy';
11
+ @import '_notifications';
@@ -1,6 +1,7 @@
1
1
  <template lang="pug">
2
2
  button.dito-button(
3
3
  :id="dataPath"
4
+ ref="element"
4
5
  :type="type"
5
6
  :title="title"
6
7
  :class="buttonClass"
@@ -1,6 +1,7 @@
1
1
  <template lang="pug">
2
2
  input.dito-checkbox(
3
3
  :id="dataPath"
4
+ ref="element"
4
5
  v-model="value"
5
6
  type="checkbox"
6
7
  v-bind="attributes"
@@ -3,6 +3,7 @@
3
3
  //- involve actually rendering it when the component is not visible.
4
4
  input.dito-text.dito-input(
5
5
  :id="dataPath"
6
+ ref="element"
6
7
  :name="name"
7
8
  type="text"
8
9
  :value="value"
@@ -80,7 +80,7 @@
80
80
  :dataPath="getDataPath(index)"
81
81
  :data="item"
82
82
  :meta="nestedMeta"
83
- :store="getChildStore(index)"
83
+ :store="getChildStore(getItemUid(schema, item), index)"
84
84
  :disabled="disabled || isLoading"
85
85
  :collapsed="collapsed"
86
86
  :collapsible="collapsible"
@@ -117,7 +117,7 @@
117
117
  :dataPath="getDataPath(index)"
118
118
  :data="item"
119
119
  :meta="nestedMeta"
120
- :store="getChildStore(index)"
120
+ :store="getChildStore(getItemUid(schema, item), index)"
121
121
  @delete="deleteItem(item, index)"
122
122
  )
123
123
  //- Render create buttons inside table when not in a single component view:
@@ -379,6 +379,7 @@ export default DitoTypeComponent.register('markup', {
379
379
 
380
380
  getButtons(settingsName, descriptions) {
381
381
  const list = []
382
+ const { commands } = this.editor
382
383
 
383
384
  const addButton = ({ name, icon, attributes, ignoreActive, onClick }) => {
384
385
  list.push({
@@ -389,8 +390,11 @@ export default DitoTypeComponent.register('markup', {
389
390
  (ignoreActive == null || !ignoreActive())
390
391
  ),
391
392
  onClick: () => {
392
- const key = `toggle${camelize(name, true)}`
393
- if (this.editor.commands[key]) {
393
+ const key =
394
+ name in commands
395
+ ? name
396
+ : `toggle${camelize(name, true)}`
397
+ if (key in commands) {
394
398
  const command = attributes =>
395
399
  this.editor.chain()[key](attributes).focus().run()
396
400
  onClick
@@ -439,7 +443,6 @@ export default DitoTypeComponent.register('markup', {
439
443
  },
440
444
 
441
445
  focusElement() {
442
- this.$el.scrollIntoView?.()
443
446
  this.editor.focus()
444
447
  }
445
448
  }
@@ -1,6 +1,7 @@
1
1
  <template lang="pug">
2
2
  InputField.dito-number(
3
3
  :id="dataPath"
4
+ ref="element"
4
5
  v-model="inputValue"
5
6
  type="number"
6
7
  v-bind="attributes"
@@ -1,6 +1,7 @@
1
1
  <template lang="pug">
2
2
  progress.dito-progress(
3
3
  :id="dataPath"
4
+ ref="element"
4
5
  :value="progressValue"
5
6
  :max="progressMax"
6
7
  v-bind="attributes"
@@ -1,6 +1,7 @@
1
1
  <template lang="pug">
2
2
  SwitchButton.dito-switch(
3
3
  :id="dataPath"
4
+ ref="element"
4
5
  v-model="value"
5
6
  :labels="labels"
6
7
  v-bind="attributes"
@@ -1,6 +1,7 @@
1
1
  <template lang="pug">
2
2
  InputField.dito-text(
3
3
  :id="dataPath"
4
+ ref="element"
4
5
  v-model="inputValue"
5
6
  :type="inputType"
6
7
  v-bind="attributes"
@@ -1,6 +1,7 @@
1
1
  <template lang="pug">
2
2
  textarea.dito-textarea.dito-input(
3
3
  :id="dataPath"
4
+ ref="element"
4
5
  v-model="value"
5
6
  v-bind="attributes"
6
7
  :rows="lines"
@@ -1,11 +1,31 @@
1
1
  <template lang="pug">
2
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
+ )
3
23
  table.dito-table.dito-table-separators.dito-table-background
4
- //- Styling comes from DitoTableHead
24
+ //- Styling comes from `DitoTableHead`
5
25
  thead.dito-table-head
6
26
  tr
7
27
  th
8
- span Name
28
+ span File
9
29
  th
10
30
  span Size
11
31
  th
@@ -18,48 +38,81 @@
18
38
  :options="getSortableOptions(false)"
19
39
  :draggable="draggable"
20
40
  )
21
- tr(
22
- v-for="(file, index) in files"
23
- :key="file.key"
41
+ template(
42
+ v-if="multiple || !isUploadActive"
24
43
  )
25
- td(
26
- v-html="renderFile(file, index)"
44
+ tr(
45
+ v-for="(file, index) in files"
46
+ :key="file.key"
27
47
  )
28
- td {{ formatFileSize(file.size) }}
29
- td
30
- template(
31
- v-if="file.upload"
48
+ td(
49
+ v-if="render"
50
+ v-html="renderFile(file, index)"
32
51
  )
33
- template(
34
- v-if="file.upload.error"
35
- )
36
- | Error: {{ file.upload.error }}
37
- template(
38
- v-else-if="file.upload.active"
39
- )
40
- | Uploading...
41
- template(
42
- v-else-if="file.upload.success"
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)"
43
60
  )
44
- | Uploaded
45
- template(
61
+ DitoUploadFile(
62
+ :file="file"
63
+ :thumbnail="thumbnails"
64
+ :thumbnailUrl="thumbnailUrls[index]"
65
+ )
66
+ td(
46
67
  v-else
47
68
  )
48
- | Stored
49
- td.dito-cell-edit-buttons
50
- .dito-buttons.dito-buttons-round
51
- //- Firefox doesn't like <button> here, so use <a> instead:
52
- a.dito-button(
53
- v-if="draggable"
54
- v-bind="getButtonAttributes(verbs.drag)"
69
+ DitoUploadFile(
70
+ :file="file"
71
+ :thumbnail="thumbnails"
72
+ :thumbnailUrl="thumbnailUrls[index]"
55
73
  )
56
- button.dito-button(
57
- v-if="deletable"
58
- type="button"
59
- v-bind="getButtonAttributes(verbs.delete)"
60
- @click="deleteFile(file, index)"
74
+ td {{ formatFileSize(file.size) }}
75
+ td
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
61
93
  )
62
- tfoot
94
+ | Stored
95
+ td.dito-cell-edit-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
+ )
63
116
  tr
64
117
  td(:colspan="4")
65
118
  .dito-upload-footer
@@ -74,24 +127,11 @@
74
127
  type="button"
75
128
  @click.prevent="upload.active = false"
76
129
  ) Cancel
77
- VueUpload.dito-button(
78
- ref="upload"
79
- v-model="uploads"
80
- :inputId="dataPath"
81
- :name="dataPath"
82
- :disabled="disabled"
83
- :postAction="uploadPath"
84
- :extensions="extensions"
85
- :accept="accept"
86
- :multiple="multiple"
87
- :size="maxSize"
88
- :title="multiple ? 'Upload Files' : 'Upload File'"
89
- :drop="$el?.closest('.dito-container')"
90
- :dropDirectory="true"
91
- @input-filter="inputFilter"
92
- @input-file="inputFile"
130
+ button.dito-button.dito-button-upload(
131
+ v-if="multiple || !hasFiles"
132
+ :title="uploadTitle"
133
+ @click="onClickUpload"
93
134
  )
94
- .dito-button-upload
95
135
  </template>
96
136
 
97
137
  <script>
@@ -102,7 +142,7 @@ import parseFileSize from 'filesize-parser'
102
142
  import { getSchemaAccessor } from '../utils/accessor.js'
103
143
  import { formatFileSize } from '../utils/units.js'
104
144
  import { appendDataPath } from '../utils/data.js'
105
- import { isArray, asArray, escapeHtml } from '@ditojs/utils'
145
+ import { isArray, asArray } from '@ditojs/utils'
106
146
  import VueUpload from 'vue-upload-component'
107
147
 
108
148
  // @vue/component
@@ -123,10 +163,22 @@ export default DitoTypeComponent.register('upload', {
123
163
  return this.$refs.upload
124
164
  },
125
165
 
166
+ uploadTitle() {
167
+ return this.multiple ? 'Upload Files' : 'Upload File'
168
+ },
169
+
126
170
  files() {
127
171
  return asFiles(this.value)
128
172
  },
129
173
 
174
+ downloadUrls() {
175
+ return this.files.map((file, index) => this.getDownloadUrl(file, index))
176
+ },
177
+
178
+ thumbnailUrls() {
179
+ return this.files.map((file, index) => this.getThumbnailUrl(file, index))
180
+ },
181
+
130
182
  multiple: getSchemaAccessor('multiple', {
131
183
  type: Boolean,
132
184
  default: false,
@@ -165,20 +217,43 @@ export default DitoTypeComponent.register('upload', {
165
217
  default: false
166
218
  }),
167
219
 
220
+ render: getSchemaAccessor('render', {
221
+ type: Function,
222
+ default: null
223
+ }),
224
+
225
+ thumbnails: getSchemaAccessor('thumbnails', {
226
+ type: [Boolean, String],
227
+ default(thumbnails) {
228
+ return thumbnails ?? !!this.schema.thumbnailUrl
229
+ },
230
+ get(thumbnails) {
231
+ return thumbnails === true ? 'medium' : thumbnails || null
232
+ }
233
+ }),
234
+
235
+ hasFiles() {
236
+ return this.files.length > 0
237
+ },
238
+
239
+ hasUploads() {
240
+ return this.uploads.length > 0
241
+ },
242
+
168
243
  isUploadReady() {
169
244
  return (
170
- this.uploads.length > 0 &&
245
+ this.hasUploads &&
171
246
  !(this.upload.active || this.upload.uploaded)
172
247
  )
173
248
  },
174
249
 
175
250
  isUploadActive() {
176
- return this.uploads.length && this.upload.active
251
+ return this.hasUploads && this.upload.active
177
252
  },
178
253
 
179
254
  uploadProgress() {
180
255
  return (
181
- this.uploads.reduce((total, file) => +file.progress + total, 0) /
256
+ this.uploads.reduce((total, file) => total + +file.progress, 0) /
182
257
  this.uploads.length
183
258
  )
184
259
  },
@@ -205,19 +280,45 @@ export default DitoTypeComponent.register('upload', {
205
280
  methods: {
206
281
  formatFileSize,
207
282
 
283
+ getFileContext(file, index) {
284
+ return this.multiple
285
+ ? new DitoContext(this, {
286
+ value: file,
287
+ data: this.files,
288
+ index,
289
+ dataPath: appendDataPath(this.dataPath, index)
290
+ })
291
+ : this.context
292
+ },
293
+
208
294
  renderFile(file, index) {
209
- const { render } = this.schema
210
- return render
211
- ? render.call(
212
- this,
213
- new DitoContext(this, {
214
- value: file,
215
- data: this.files,
216
- index,
217
- dataPath: appendDataPath(this.dataPath, index)
295
+ return this.render(this.getFileContext(file, index))
296
+ },
297
+
298
+ getDownloadUrl(file, index) {
299
+ return file.url
300
+ ? file.url
301
+ : !file.upload || file.upload.success
302
+ ? this.getSchemaValue('downloadUrl', {
303
+ type: 'String',
304
+ default: null,
305
+ context: this.getFileContext(file, index)
218
306
  })
307
+ : null
308
+ },
309
+
310
+ getThumbnailUrl(file, index) {
311
+ return !file.upload || file.upload.success
312
+ ? this.getSchemaValue('thumbnailUrl', {
313
+ type: 'String',
314
+ default: null,
315
+ context: this.getFileContext(file, index)
316
+ }) || (
317
+ file.type.startsWith('image/')
318
+ ? file.url
319
+ : null
219
320
  )
220
- : escapeHtml(file.name)
321
+ : null
221
322
  },
222
323
 
223
324
  deleteFile(file, index) {
@@ -283,7 +384,7 @@ export default DitoTypeComponent.register('upload', {
283
384
  this.replaceFile(file, null)
284
385
  },
285
386
 
286
- inputFile(newFile, oldFile) {
387
+ onInputFile(newFile, oldFile) {
287
388
  if (newFile && !oldFile) {
288
389
  const { id, name, size } = newFile
289
390
  this.addFile({ id, name, size, upload: newFile })
@@ -324,11 +425,31 @@ export default DitoTypeComponent.register('upload', {
324
425
  }
325
426
  },
326
427
 
327
- inputFilter(newFile /*, oldFile, prevent */) {
428
+ onInputFilter(newFile /*, oldFile, prevent */) {
328
429
  const xhr = newFile?.xhr
329
430
  if (this.api.cors?.credentials && xhr && !xhr.withCredentials) {
330
431
  xhr.withCredentials = true
331
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
+ )
332
453
  }
333
454
  },
334
455
 
@@ -351,18 +472,16 @@ function asFiles(value) {
351
472
 
352
473
  .dito-upload {
353
474
  .dito-table {
354
- tr {
475
+ tr,
476
+ .dito-cell-edit-buttons {
355
477
  vertical-align: middle;
356
478
  }
357
479
  }
358
480
 
359
- .dito-button-upload {
360
- padding: 0;
361
-
362
- > * {
363
- position: absolute;
364
- cursor: pointer;
365
- }
481
+ .dito-upload-input {
482
+ // See `onClickUpload()` method for details.
483
+ position: absolute;
484
+ pointer-events: none;
366
485
  }
367
486
 
368
487
  .dito-upload-footer {
@@ -1042,5 +1042,5 @@ export function getItemUid(sourceSchema, item) {
1042
1042
  // either way, pass through `getUid()` so that the ids are associated with the
1043
1043
  // item through a weak map, as the ids can be filtered out in `processData()`
1044
1044
  // while the components that use the uids as key are still visible.
1045
- return getUid(item, getItemId(sourceSchema, item))
1045
+ return getUid(item, item => getItemId(sourceSchema, item))
1046
1046
  }
package/src/utils/uid.js CHANGED
@@ -1,12 +1,15 @@
1
+ import { toRaw } from 'vue'
2
+
1
3
  const uidMap = new WeakMap()
2
4
 
3
5
  // Generated and remembers unique ids per passed object using a weak map.
4
6
  let uid = 0
5
- export function getUid(object, itemId) {
6
- let id = uidMap.get(object)
7
+ export function getUid(item, getItemId = null) {
8
+ const raw = toRaw(item)
9
+ let id = uidMap.get(raw)
7
10
  if (!id) {
8
- id = itemId || `@${++uid}`
9
- uidMap.set(object, id)
11
+ id = getItemId?.(item) || `@${++uid}`
12
+ uidMap.set(raw, id)
10
13
  }
11
14
  return id
12
15
  }