@ditojs/admin 3.2.5 → 3.2.7
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/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +218 -154
- package/dist/dito-admin.umd.js +5 -5
- package/package.json +5 -5
- package/src/DitoAdmin.js +4 -3
- package/src/components/DitoButtons.vue +4 -5
- package/src/components/DitoCreateButton.vue +1 -0
- package/src/components/DitoDialog.vue +16 -15
- package/src/components/DitoForm.vue +0 -2
- package/src/components/DitoLabel.vue +1 -0
- package/src/components/DitoPane.vue +1 -0
- package/src/components/DitoPanel.vue +2 -0
- package/src/components/DitoSchema.vue +1 -0
- package/src/components/DitoTreeItem.vue +52 -2
- package/src/mixins/OptionsMixin.js +15 -1
- package/src/mixins/ResourceMixin.js +22 -3
- package/src/styles/_button.scss +45 -46
- package/src/types/DitoTypeCode.vue +6 -3
- package/src/types/DitoTypeMarkup.vue +1 -0
- package/src/types/DitoTypeMultiselect.vue +61 -16
- package/src/types/DitoTypeTreeList.vue +2 -0
- package/src/utils/DataSchemaResolver.js +1 -1
- package/src/utils/options.js +2 -2
- package/src/utils/schema/data.js +5 -0
- package/types/index.d.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/admin",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Dito.js Admin is a schema based admin interface for Dito.js Server, featuring auto-generated views and forms and built with Vue.js",
|
|
6
6
|
"repository": "https://github.com/ditojs/dito/tree/main/packages/admin",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"not ie_mob > 0"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@ditojs/ui": "^3.2.
|
|
45
|
-
"@ditojs/utils": "^3.2.
|
|
44
|
+
"@ditojs/ui": "^3.2.6",
|
|
45
|
+
"@ditojs/utils": "^3.2.6"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@kyvg/vue3-notification": "^3.4.2",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"vue-upload-component": "^3.2.2"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
|
-
"@ditojs/build": "^3.2.
|
|
90
|
+
"@ditojs/build": "^3.2.6",
|
|
91
91
|
"@kyvg/vue3-notification": "^3.4.2",
|
|
92
92
|
"@tiptap/core": "^3.31.4",
|
|
93
93
|
"@tiptap/extension-blockquote": "^3.31.4",
|
|
@@ -131,5 +131,5 @@
|
|
|
131
131
|
"vue-router": "^5.3.1",
|
|
132
132
|
"vue-upload-component": "^3.2.2"
|
|
133
133
|
},
|
|
134
|
-
"gitHead": "
|
|
134
|
+
"gitHead": "88a3df9bbd036594e886ea6169cef3dfd4b10fe7"
|
|
135
135
|
}
|
package/src/DitoAdmin.js
CHANGED
|
@@ -150,9 +150,10 @@ export default class DitoAdmin {
|
|
|
150
150
|
components: {
|
|
151
151
|
DitoRoot,
|
|
152
152
|
VueNotifications,
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
153
|
+
// Registered globally too, so that the custom components of apps can
|
|
154
|
+
// use them in their templates: Unlike the admin's own components, they
|
|
155
|
+
// don't extend `DitoComponent`, which shares its registry. This also
|
|
156
|
+
// keeps the modules from being tree-shaken.
|
|
156
157
|
...components,
|
|
157
158
|
...types
|
|
158
159
|
},
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
template(
|
|
6
6
|
v-for="(buttonSchema, buttonDataPath) in buttonSchemas"
|
|
7
7
|
)
|
|
8
|
-
DitoContainer(
|
|
8
|
+
DitoContainer.dito-buttons__container(
|
|
9
9
|
v-if="shouldRenderSchema(buttonSchema)"
|
|
10
10
|
:key="buttonDataPath"
|
|
11
11
|
:schema="buttonSchema"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
)
|
|
22
22
|
//- Render each node in the default slot through `dito-vnode`,
|
|
23
23
|
//- so it can be wrapped in a `.dito-container` class.
|
|
24
|
-
.dito-container(
|
|
24
|
+
.dito-container.dito-buttons__container(
|
|
25
25
|
v-if="hasVNodeContent(vnode)"
|
|
26
26
|
)
|
|
27
27
|
DitoVnode(:vnode="vnode")
|
|
@@ -86,8 +86,7 @@ export default DitoComponent.component('DitoButtons', {
|
|
|
86
86
|
|
|
87
87
|
<style lang="scss">
|
|
88
88
|
.dito-buttons {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
89
|
+
// The containers of the buttons don't add padding around them.
|
|
90
|
+
--container-padding: 0px;
|
|
92
91
|
}
|
|
93
92
|
</style>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
@mouseup="onMouseUp"
|
|
9
9
|
)
|
|
10
10
|
UseFocusTrap.dito-dialog__focus-trap(:options="focusTrapOptions")
|
|
11
|
-
form.dito-scroll-parent(
|
|
11
|
+
form.dito-dialog__form.dito-scroll-parent(
|
|
12
12
|
@submit.prevent="submit"
|
|
13
13
|
@keydown.enter="onEnter"
|
|
14
14
|
)
|
|
@@ -209,6 +209,8 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
209
209
|
@import '../styles/_imports';
|
|
210
210
|
|
|
211
211
|
.dito-dialog {
|
|
212
|
+
$self: &;
|
|
213
|
+
|
|
212
214
|
position: fixed;
|
|
213
215
|
display: flex;
|
|
214
216
|
inset: 0;
|
|
@@ -231,8 +233,7 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
231
233
|
order: -1;
|
|
232
234
|
}
|
|
233
235
|
|
|
234
|
-
|
|
235
|
-
form {
|
|
236
|
+
&__form {
|
|
236
237
|
position: relative;
|
|
237
238
|
display: flex;
|
|
238
239
|
box-sizing: border-box;
|
|
@@ -242,23 +243,23 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
242
243
|
max-height: 100%;
|
|
243
244
|
box-shadow: 0 20px 60px -2px rgb(27, 33, 58, 0.4);
|
|
244
245
|
}
|
|
245
|
-
}
|
|
246
246
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
247
|
+
&-enter-active,
|
|
248
|
+
&-leave-active {
|
|
249
|
+
transition: opacity 0.15s;
|
|
250
250
|
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
#{$self}__form {
|
|
252
|
+
transition: transform 0.25s;
|
|
253
|
+
}
|
|
253
254
|
}
|
|
254
|
-
}
|
|
255
255
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
256
|
+
&-enter-from,
|
|
257
|
+
&-leave-to {
|
|
258
|
+
opacity: 0;
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
#{$self}__form {
|
|
261
|
+
transform: translateY(-20px);
|
|
262
|
+
}
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
265
|
</style>
|
|
@@ -311,8 +311,6 @@ export default DitoComponent.component('DitoForm', {
|
|
|
311
311
|
if (
|
|
312
312
|
param &&
|
|
313
313
|
this.providesData &&
|
|
314
|
-
// TODO: See if we can remove this due to `flush: 'post'`.
|
|
315
|
-
from.matched[0].path === to.matched[0].path && // Staying on same form
|
|
316
314
|
from.params[param] !== 'create' && // But haven't been creating
|
|
317
315
|
to.params[param] !== from.params[param] // Going to a different entity
|
|
318
316
|
) {
|
|
@@ -152,6 +152,7 @@ export default DitoComponent.component('DitoLabel', {
|
|
|
152
152
|
transform: translateY(-4px);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
|
|
155
156
|
.dito-buttons {
|
|
156
157
|
// Move the label padding inside .dito-buttons, so that it captures all
|
|
157
158
|
// near mouse events:
|
|
@@ -349,6 +349,7 @@ export default DitoComponent.component('DitoPane', {
|
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
// TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
|
|
352
353
|
&__main + .dito-buttons--main {
|
|
353
354
|
// Needed forms with sticky main buttons.
|
|
354
355
|
margin: $content-padding;
|
|
@@ -244,6 +244,7 @@ export default DitoComponent.component('DitoPanel', {
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
|
|
247
248
|
.dito-buttons {
|
|
248
249
|
position: absolute;
|
|
249
250
|
right: $input-padding-ver;
|
|
@@ -266,6 +267,7 @@ export default DitoComponent.component('DitoPanel', {
|
|
|
266
267
|
padding: 0;
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
// TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
|
|
269
271
|
> .dito-buttons {
|
|
270
272
|
--button-margin: #{$form-spacing};
|
|
271
273
|
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
:draggable="childrenDraggable"
|
|
98
98
|
:label="getItemLabel(childrenSchema, item.data, { index })"
|
|
99
99
|
:level="level + 1"
|
|
100
|
+
@delete="deleteChild(index)"
|
|
100
101
|
)
|
|
101
102
|
//- TODO: Convert dito-tree-item to use dito-label internally, and then
|
|
102
103
|
//- pass `asObject: true` in the `getItemLabel()` call above.
|
|
@@ -115,7 +116,7 @@ import { stripHtml } from '@ditojs/utils'
|
|
|
115
116
|
// @vue/component
|
|
116
117
|
export default DitoComponent.component('DitoTreeItem', {
|
|
117
118
|
mixins: [ItemMixin, SortableMixin],
|
|
118
|
-
emits: ['update:data'],
|
|
119
|
+
emits: ['update:data', 'delete'],
|
|
119
120
|
inject: ['container'],
|
|
120
121
|
|
|
121
122
|
props: {
|
|
@@ -302,8 +303,56 @@ export default DitoComponent.component('DitoTreeItem', {
|
|
|
302
303
|
this.editPath(this.path)
|
|
303
304
|
},
|
|
304
305
|
|
|
306
|
+
// Items are removed from the children of their parent, see
|
|
307
|
+
// `deleteChild()`.
|
|
305
308
|
onDelete() {
|
|
306
|
-
|
|
309
|
+
this.$emit('delete')
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
// Removes the child after confirming it, like inlined lists do. Open forms
|
|
313
|
+
// of the child or of its later siblings are closed, as the removal shifts
|
|
314
|
+
// the indices in their paths. The removal is transient, as the tree is
|
|
315
|
+
// part of its form's data.
|
|
316
|
+
deleteChild(index) {
|
|
317
|
+
const { childrenSchema, container } = this
|
|
318
|
+
const item = this.childrenList[index]
|
|
319
|
+
const label = this.getItemLabel(childrenSchema, item, {
|
|
320
|
+
index,
|
|
321
|
+
extended: true
|
|
322
|
+
})
|
|
323
|
+
if (
|
|
324
|
+
window.confirm(`Do you really want to ${this.verbs.delete} ${label}?`)
|
|
325
|
+
) {
|
|
326
|
+
if (this.isEditingChildFrom(index)) {
|
|
327
|
+
this.$router.push({
|
|
328
|
+
path: container.path,
|
|
329
|
+
query: this.$route.query
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
this.childrenList = this.childrenList.toSpliced(index, 1)
|
|
333
|
+
this.onChange()
|
|
334
|
+
this.notify({
|
|
335
|
+
type: 'info',
|
|
336
|
+
title: 'Successfully Removed',
|
|
337
|
+
text: [`${label} was ${this.verbs.deleted}.`, container.transientNote]
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
// Returns whether the form of a child at the index or after it is open,
|
|
343
|
+
// or the form of one of their own children.
|
|
344
|
+
isEditingChildFrom(index) {
|
|
345
|
+
const childrenPath = this.childrenSchema.path
|
|
346
|
+
if (!childrenPath) {
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
const prefix = `${this.path}/${childrenPath}/`
|
|
350
|
+
const { editPath } = this.container
|
|
351
|
+
if (!editPath.startsWith(prefix)) {
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
const [childIndex] = editPath.slice(prefix.length).split('/')
|
|
355
|
+
return Number(childIndex) >= index
|
|
307
356
|
},
|
|
308
357
|
|
|
309
358
|
onChange() {
|
|
@@ -391,6 +440,7 @@ export default DitoComponent.component('DitoTreeItem', {
|
|
|
391
440
|
color: rgba($color-black, 0.2);
|
|
392
441
|
}
|
|
393
442
|
|
|
443
|
+
// TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
|
|
394
444
|
.dito-buttons {
|
|
395
445
|
flex: auto;
|
|
396
446
|
display: flex;
|
|
@@ -5,6 +5,7 @@ import { getMultipleValue } from '../utils/schema/data.js'
|
|
|
5
5
|
import { getSchemaAccessor } from '../utils/accessor.js'
|
|
6
6
|
import { setTemporaryId, isReference } from '../utils/data.js'
|
|
7
7
|
import {
|
|
8
|
+
asArray,
|
|
8
9
|
isObject,
|
|
9
10
|
isArray,
|
|
10
11
|
isString,
|
|
@@ -46,7 +47,12 @@ export default {
|
|
|
46
47
|
if (!this.relate) {
|
|
47
48
|
return value
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
// Keep related objects without options, see
|
|
51
|
+
// `DitoTypeMultiselect.getFallbackOption()`.
|
|
52
|
+
const option = (
|
|
53
|
+
this.getOptionForValue(value) ??
|
|
54
|
+
this.getRelatedObject(value)
|
|
55
|
+
)
|
|
50
56
|
if (this.shouldSetTemporaryId(option)) {
|
|
51
57
|
// Options without ids, e.g. new items of the edited data, get
|
|
52
58
|
// temporary ids when selected, so that the relation can reference
|
|
@@ -325,6 +331,14 @@ export default {
|
|
|
325
331
|
: value
|
|
326
332
|
},
|
|
327
333
|
|
|
334
|
+
// Returns the related object in the value with the given value of its
|
|
335
|
+
// option, e.g. its id.
|
|
336
|
+
getRelatedObject(value) {
|
|
337
|
+
return asArray(this.value).find(
|
|
338
|
+
object => this.getValueForOption(object) === value
|
|
339
|
+
)
|
|
340
|
+
},
|
|
341
|
+
|
|
328
342
|
getValueForOption(option) {
|
|
329
343
|
if (this.shouldSetTemporaryId(option)) {
|
|
330
344
|
// Until they're selected and get temporary ids, options without ids
|
|
@@ -26,6 +26,12 @@ export default {
|
|
|
26
26
|
},
|
|
27
27
|
|
|
28
28
|
computed: {
|
|
29
|
+
// Whether the component is part of a form, e.g. a list that loads its own
|
|
30
|
+
// data, unlike forms and the lists of views.
|
|
31
|
+
isInsideForm() {
|
|
32
|
+
return !this.isForm && !!this.formComponent
|
|
33
|
+
},
|
|
34
|
+
|
|
29
35
|
resourceComponent() {
|
|
30
36
|
return this
|
|
31
37
|
},
|
|
@@ -215,9 +221,15 @@ export default {
|
|
|
215
221
|
this.onFilterErrors?.(data.errors)
|
|
216
222
|
) {
|
|
217
223
|
return true
|
|
218
|
-
} else if (
|
|
219
|
-
|
|
220
|
-
|
|
224
|
+
} else if (
|
|
225
|
+
this.isUnauthorizedError(response) &&
|
|
226
|
+
this.isInsideForm
|
|
227
|
+
) {
|
|
228
|
+
// The user is logged in, see `sendRequest()`, but may not access
|
|
229
|
+
// the resource of a component in a form, e.g. a list that loads
|
|
230
|
+
// its own data. Show it on the component, as several components
|
|
231
|
+
// of the form may be affected. Others notify like for errors.
|
|
232
|
+
this.addUnauthorizedError()
|
|
221
233
|
return true
|
|
222
234
|
}
|
|
223
235
|
}
|
|
@@ -242,6 +254,13 @@ export default {
|
|
|
242
254
|
return response?.status === 401
|
|
243
255
|
},
|
|
244
256
|
|
|
257
|
+
addUnauthorizedError() {
|
|
258
|
+
const error = `You don't have permission to load this data.`
|
|
259
|
+
if (!this.errors?.includes(error)) {
|
|
260
|
+
this.addError(error)
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
|
|
245
264
|
async handleRequest(
|
|
246
265
|
{
|
|
247
266
|
method,
|
package/src/styles/_button.scss
CHANGED
|
@@ -109,10 +109,53 @@
|
|
|
109
109
|
cursor: grabbing;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
.dito-buttons--large & {
|
|
114
|
+
border-radius: 2em;
|
|
115
|
+
padding: $button-round-padding;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Buttons in round and small button groups show icons instead of text.
|
|
119
|
+
// For now, nothing for create: &--create:empty::before
|
|
120
|
+
.dito-buttons--round &,
|
|
121
|
+
.dito-buttons--small & {
|
|
122
|
+
&--add:empty::before {
|
|
123
|
+
@extend %icon-add;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
&--upload:empty::before {
|
|
127
|
+
@extend %icon-upload;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
&--delete:empty::before,
|
|
131
|
+
&--remove:empty::before {
|
|
132
|
+
@extend %icon-remove;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&--edit:empty::before {
|
|
136
|
+
@extend %icon-edit;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
&--clear:empty::before {
|
|
140
|
+
@extend %icon-clear;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
&--drag:empty::before {
|
|
144
|
+
@extend %icon-drag;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
&--copy:empty::before {
|
|
148
|
+
@extend %icon-copy;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
&--paste:empty::before {
|
|
152
|
+
@extend %icon-paste;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
112
155
|
}
|
|
113
156
|
|
|
114
157
|
.dito-buttons {
|
|
115
|
-
|
|
158
|
+
$self: &;
|
|
116
159
|
|
|
117
160
|
&--large {
|
|
118
161
|
--button-margin: 3px;
|
|
@@ -122,15 +165,10 @@
|
|
|
122
165
|
justify-content: center;
|
|
123
166
|
padding-bottom: $content-padding;
|
|
124
167
|
|
|
125
|
-
|
|
168
|
+
#{$self}__container {
|
|
126
169
|
// Do not specify this on .dito-buttons directly as it would break borders
|
|
127
170
|
margin-top: $content-padding;
|
|
128
171
|
}
|
|
129
|
-
|
|
130
|
-
.dito-button {
|
|
131
|
-
border-radius: 2em;
|
|
132
|
-
padding: $button-round-padding;
|
|
133
|
-
}
|
|
134
172
|
}
|
|
135
173
|
|
|
136
174
|
&--main {
|
|
@@ -152,43 +190,4 @@
|
|
|
152
190
|
display: none;
|
|
153
191
|
}
|
|
154
192
|
}
|
|
155
|
-
|
|
156
|
-
&--round,
|
|
157
|
-
&--small {
|
|
158
|
-
// For now, nothing for create:
|
|
159
|
-
// .dito-button--create:empty::before
|
|
160
|
-
|
|
161
|
-
.dito-button--add:empty::before {
|
|
162
|
-
@extend %icon-add;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
.dito-button--upload:empty::before {
|
|
166
|
-
@extend %icon-upload;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.dito-button--delete:empty::before,
|
|
170
|
-
.dito-button--remove:empty::before {
|
|
171
|
-
@extend %icon-remove;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
.dito-button--edit:empty::before {
|
|
175
|
-
@extend %icon-edit;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
.dito-button--clear:empty::before {
|
|
179
|
-
@extend %icon-clear;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
.dito-button--drag:empty::before {
|
|
183
|
-
@extend %icon-drag;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
.dito-button--copy:empty::before {
|
|
187
|
-
@extend %icon-copy;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.dito-button--paste:empty::before {
|
|
191
|
-
@extend %icon-paste;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
193
|
}
|
|
@@ -47,10 +47,10 @@ export default DitoTypeComponent.register('code', {
|
|
|
47
47
|
default: false
|
|
48
48
|
}),
|
|
49
49
|
|
|
50
|
+
// The height of the lines is set through `--lines` and the styles, which
|
|
51
|
+
// add the padding, unless the editor was resized to a height of its own.
|
|
50
52
|
style() {
|
|
51
|
-
return {
|
|
52
|
-
height: this.height || `calc(${this.lines}em * var(--line-height))`
|
|
53
|
-
}
|
|
53
|
+
return this.height ? { height: this.height } : { '--lines': this.lines }
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
|
|
@@ -175,6 +175,9 @@ export default DitoTypeComponent.register('code', {
|
|
|
175
175
|
// For proper sizing of content along with :style="style" setting above,
|
|
176
176
|
// for proper line-height calculation.
|
|
177
177
|
padding: $input-padding;
|
|
178
|
+
height: calc(
|
|
179
|
+
var(--lines) * 1em * var(--line-height) + 2 * #{$input-padding-ver}
|
|
180
|
+
);
|
|
178
181
|
min-height: calc(1em * var(--line-height) + 2 * $input-padding-ver);
|
|
179
182
|
|
|
180
183
|
.codeflask {
|
|
@@ -37,6 +37,21 @@
|
|
|
37
37
|
@tag="onAddTag"
|
|
38
38
|
@search-change="onSearchChange"
|
|
39
39
|
)
|
|
40
|
+
//- The default tag, which marks values without options.
|
|
41
|
+
template(#tag="{ option, remove }")
|
|
42
|
+
span.multiselect__tag.dito-multiselect__tag(
|
|
43
|
+
:class=`{
|
|
44
|
+
'dito-multiselect__tag--unavailable': isUnavailableOption(option)
|
|
45
|
+
}`
|
|
46
|
+
:title="isUnavailableOption(option) ? 'Not among the options' : null"
|
|
47
|
+
@mousedown.prevent
|
|
48
|
+
)
|
|
49
|
+
span {{ getLabelForOption(option) }}
|
|
50
|
+
i.multiselect__tag-icon(
|
|
51
|
+
tabindex="1"
|
|
52
|
+
@keydown.enter.prevent="remove(option)"
|
|
53
|
+
@mousedown.prevent="remove(option)"
|
|
54
|
+
)
|
|
40
55
|
DitoAffixes(
|
|
41
56
|
:items="schema.suffix"
|
|
42
57
|
position="suffix"
|
|
@@ -70,7 +85,7 @@ import OptionsMixin from '../mixins/OptionsMixin.js'
|
|
|
70
85
|
import DitoAffixes from '../components/DitoAffixes.vue'
|
|
71
86
|
import VueMultiselect from 'vue-multiselect'
|
|
72
87
|
import { getSchemaAccessor } from '../utils/accessor.js'
|
|
73
|
-
import { isBoolean } from '@ditojs/utils'
|
|
88
|
+
import { isArray, isBoolean, isObject, isString } from '@ditojs/utils'
|
|
74
89
|
|
|
75
90
|
// @vue/component
|
|
76
91
|
export default DitoTypeComponent.register('multiselect', {
|
|
@@ -90,18 +105,22 @@ export default DitoTypeComponent.register('multiselect', {
|
|
|
90
105
|
computed: {
|
|
91
106
|
selectedOptions: {
|
|
92
107
|
get() {
|
|
108
|
+
// Values without options stay visible once the options are loaded,
|
|
109
|
+
// and are kept when the selection changes, see `getFallbackOption()`,
|
|
110
|
+
// unless the multiselect is taggable and can add them as options.
|
|
93
111
|
return this.multiple
|
|
94
|
-
?
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
? this.hasOptions && isArray(this.value)
|
|
113
|
+
? this.value.map(value => {
|
|
114
|
+
const selectedValue = this.relate
|
|
115
|
+
? this.getValueForOption(value)
|
|
116
|
+
: value
|
|
117
|
+
return (
|
|
118
|
+
this.getOptionForValue(selectedValue) ||
|
|
119
|
+
this.addTagOption(selectedValue) ||
|
|
120
|
+
this.getFallbackOption(value)
|
|
100
121
|
)
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
// TODO: Should we display an error instead?
|
|
104
|
-
.filter(Boolean)
|
|
122
|
+
})
|
|
123
|
+
: []
|
|
105
124
|
: this.selectedOption
|
|
106
125
|
},
|
|
107
126
|
|
|
@@ -196,6 +215,27 @@ export default DitoTypeComponent.register('multiselect', {
|
|
|
196
215
|
}
|
|
197
216
|
},
|
|
198
217
|
|
|
218
|
+
// Returns whether the option is a fallback option for a value without
|
|
219
|
+
// one, see `getFallbackOption()`. Plain options are their own values.
|
|
220
|
+
isUnavailableOption(option) {
|
|
221
|
+
return this.optionValue
|
|
222
|
+
? !this.getOptionForValue(this.getValueForOption(option))
|
|
223
|
+
: !this.options.includes(option)
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// Returns an option for a value without one, e.g. of options that only
|
|
227
|
+
// hold the results of searches: Related objects are their own options,
|
|
228
|
+
// other values get an option that holds them as value and label.
|
|
229
|
+
getFallbackOption(value) {
|
|
230
|
+
const { optionValue, optionLabel } = this
|
|
231
|
+
return isObject(value) || !isString(optionValue)
|
|
232
|
+
? value
|
|
233
|
+
: {
|
|
234
|
+
[optionValue]: value,
|
|
235
|
+
...(isString(optionLabel) && { [optionLabel]: `${value}` })
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
|
|
199
239
|
focusElement() {
|
|
200
240
|
this.$refs.element.activate()
|
|
201
241
|
},
|
|
@@ -289,6 +329,16 @@ $tag-line-height: 1em;
|
|
|
289
329
|
display: inline-flex;
|
|
290
330
|
position: relative;
|
|
291
331
|
|
|
332
|
+
&__tag {
|
|
333
|
+
color: $color-text-inverted;
|
|
334
|
+
background: $color-active;
|
|
335
|
+
|
|
336
|
+
// Values without options, see `getFallbackOption()`.
|
|
337
|
+
&--unavailable {
|
|
338
|
+
background: $color-grey;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
292
342
|
&__inner {
|
|
293
343
|
flex: 1;
|
|
294
344
|
position: relative;
|
|
@@ -463,11 +513,6 @@ $tag-line-height: 1em;
|
|
|
463
513
|
}
|
|
464
514
|
}
|
|
465
515
|
|
|
466
|
-
&__tag {
|
|
467
|
-
color: $color-text-inverted;
|
|
468
|
-
background: $color-active;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
516
|
&__tag-icon {
|
|
472
517
|
background: none;
|
|
473
518
|
border-radius: 1em;
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
// e.g. `data: ({ item: { id } }) => async () => load(id)`, so that loading
|
|
18
18
|
// only starts again when these dependencies change.
|
|
19
19
|
// - `dataPath` is resolved relative to the data path of the context, in its
|
|
20
|
-
// root data, e.g. `'../
|
|
20
|
+
// root data, e.g. `'../author/books'`.
|
|
21
21
|
// - Data schemas that aren't objects are values, e.g. `options: ['A', 'B']`.
|
|
22
22
|
//
|
|
23
23
|
// While a value is loading, `value` is `undefined` and `isLoading` is `true`.
|
package/src/utils/options.js
CHANGED
|
@@ -44,8 +44,8 @@ const ditoOptionKeys = [
|
|
|
44
44
|
'processSchema',
|
|
45
45
|
'getPanelSchema',
|
|
46
46
|
'getFormSchemasForProcessing',
|
|
47
|
-
//
|
|
48
|
-
//
|
|
47
|
+
// vue-router reads route guards from the raw component options, without
|
|
48
|
+
// their mixins, see `extractComponentsGuards()` in its `navigationGuards.ts`.
|
|
49
49
|
'beforeRouteUpdate',
|
|
50
50
|
'beforeRouteLeave'
|
|
51
51
|
]
|