@ditojs/admin 3.2.7 → 3.3.0
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 +6011 -5826
- package/dist/dito-admin.umd.js +3 -7
- package/package.json +18 -17
- package/src/DitoAdmin.js +44 -24
- package/src/DitoContext.js +28 -2
- package/src/DitoTypeComponent.js +1 -0
- package/src/appState.js +6 -3
- package/src/components/DitoAccount.vue +35 -27
- package/src/components/DitoAffixes.vue +9 -2
- package/src/components/DitoClipboard.vue +59 -93
- package/src/components/DitoConfirmMessage.vue +18 -0
- package/src/components/DitoContainer.vue +56 -18
- package/src/components/DitoCreateButton.vue +36 -84
- package/src/components/DitoDialog.vue +20 -15
- package/src/components/DitoDragHandle.vue +44 -0
- package/src/components/DitoDraggable.vue +43 -16
- package/src/components/DitoEditButtons.vue +48 -24
- package/src/components/DitoErrors.vue +9 -56
- package/src/components/DitoForm.vue +82 -42
- package/src/components/DitoInputAffixes.vue +50 -0
- package/src/components/DitoLabel.vue +1 -15
- package/src/components/DitoMenu.vue +45 -52
- package/src/components/DitoNotifications.vue +38 -14
- package/src/components/DitoOptionList.vue +81 -0
- package/src/components/DitoOptionsEditButtons.vue +57 -0
- package/src/components/DitoPagination.vue +1 -8
- package/src/components/DitoPane.vue +51 -39
- package/src/components/DitoPanel.vue +12 -26
- package/src/components/DitoPanels.vue +11 -5
- package/src/components/DitoResizeHandle.vue +47 -0
- package/src/components/DitoRoot.vue +113 -320
- package/src/components/DitoSchema.vue +72 -63
- package/src/components/DitoSchemaInlined.vue +8 -9
- package/src/components/DitoScopes.vue +14 -18
- package/src/components/DitoSidebar.vue +4 -29
- package/src/components/DitoSourceItem.vue +114 -0
- package/src/components/DitoTableCell.vue +4 -0
- package/src/components/DitoTableHead.vue +15 -17
- package/src/components/DitoTabs.vue +25 -0
- package/src/components/DitoTrail.vue +25 -25
- package/src/components/DitoTreeItem.vue +102 -141
- package/src/components/DitoUploadFile.vue +5 -2
- package/src/components/DitoView.vue +33 -24
- package/src/index.js +4 -1
- package/src/mixins/DitoMixin.js +77 -110
- package/src/mixins/DomMixin.js +12 -14
- package/src/mixins/EmitterMixin.js +3 -2
- package/src/mixins/ItemMixin.js +38 -94
- package/src/mixins/OptionsMixin.js +38 -25
- package/src/mixins/ResourceMixin.js +182 -111
- package/src/mixins/RouteMixin.js +61 -44
- package/src/mixins/SchemaParentMixin.js +4 -1
- package/src/mixins/SourceMixin.js +118 -100
- package/src/mixins/TypeMixin.js +42 -40
- package/src/mixins/ValidationMixin.js +16 -4
- package/src/mixins/ValidatorMixin.js +1 -1
- package/src/styles/_settings.scss +4 -0
- package/src/styles/_tippy.scss +0 -11
- package/src/styles/style.scss +0 -1
- package/src/types/DitoTypeButton.vue +11 -14
- package/src/types/DitoTypeCheckboxes.vue +35 -17
- package/src/types/DitoTypeCode.vue +19 -41
- package/src/types/DitoTypeColor.vue +67 -19
- package/src/types/DitoTypeDate.vue +11 -14
- package/src/types/DitoTypeList.vue +50 -62
- package/src/types/DitoTypeMarkup.vue +41 -54
- package/src/types/DitoTypeMultiselect.vue +82 -52
- package/src/types/DitoTypeNumber.vue +12 -15
- package/src/types/DitoTypeObject.vue +4 -38
- package/src/types/DitoTypeRadio.vue +19 -6
- package/src/types/DitoTypeSection.vue +23 -9
- package/src/types/DitoTypeSelect.vue +39 -23
- package/src/types/DitoTypeSlider.vue +9 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +11 -14
- package/src/types/DitoTypeTreeList.vue +46 -8
- package/src/types/DitoTypeUpload.vue +190 -67
- package/src/utils/DataModel.js +368 -146
- package/src/utils/FileDropTargets.js +97 -0
- package/src/utils/ItemStores.js +31 -0
- package/src/utils/ListQuery.js +115 -0
- package/src/utils/LoadingTracker.js +76 -0
- package/src/utils/SchemaGraph.js +3 -2
- package/src/utils/Session.js +151 -0
- package/src/utils/ViewRegistry.js +85 -0
- package/src/utils/accessor.js +12 -4
- package/src/utils/affixes.js +33 -0
- package/src/utils/changeOnceEdited.js +33 -0
- package/src/utils/clipboard.js +55 -0
- package/src/utils/data.js +10 -5
- package/src/utils/dialogs.js +59 -0
- package/src/utils/dragResize.js +61 -0
- package/src/utils/filter.js +36 -2
- package/src/utils/html.js +9 -0
- package/src/utils/list.js +10 -0
- package/src/utils/options.js +4 -0
- package/src/utils/registration.js +18 -0
- package/src/utils/request.js +108 -13
- package/src/utils/route.js +6 -17
- package/src/utils/schema/data.js +24 -2
- package/src/utils/schema/events.js +19 -0
- package/src/utils/schema/item.js +114 -0
- package/src/utils/schema/lookup.js +39 -14
- package/src/utils/schema/setup.js +5 -5
- package/src/utils/schema/validation.js +16 -2
- package/src/utils/uid.js +105 -8
- package/src/validators/_decimals.js +16 -5
- package/types/index.d.ts +168 -41
- package/src/mixins/LoadingMixin.js +0 -23
- package/src/mixins/PulldownMixin.js +0 -72
- package/src/styles/_pulldown.scss +0 -38
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
:disabled="disabled"
|
|
17
17
|
)
|
|
18
18
|
DitoLabel(
|
|
19
|
-
v-if="
|
|
19
|
+
v-if="shouldRenderLabel"
|
|
20
20
|
:class="labelClasses"
|
|
21
21
|
:htmlFor="labelHtmlFor"
|
|
22
22
|
:label="label"
|
|
@@ -36,11 +36,14 @@
|
|
|
36
36
|
:label="label"
|
|
37
37
|
:single="single"
|
|
38
38
|
:nested="nested"
|
|
39
|
-
:
|
|
39
|
+
:errorsElementId="errorsElementId"
|
|
40
40
|
@errors="onErrors"
|
|
41
41
|
@update:component="value => (component = value)"
|
|
42
42
|
)
|
|
43
|
-
DitoErrors(
|
|
43
|
+
DitoErrors(
|
|
44
|
+
:id="errorsElementId"
|
|
45
|
+
:errors="errors"
|
|
46
|
+
)
|
|
44
47
|
</template>
|
|
45
48
|
|
|
46
49
|
<script>
|
|
@@ -59,6 +62,20 @@ import { appendDataPath } from '../utils/data.js'
|
|
|
59
62
|
// @vue/component
|
|
60
63
|
export default DitoComponent.component('DitoContainer', {
|
|
61
64
|
mixins: [ValueMixin, ContextMixin],
|
|
65
|
+
|
|
66
|
+
provide() {
|
|
67
|
+
return {
|
|
68
|
+
$accumulatedBasis: () => this.combinedBasis
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
inject: {
|
|
73
|
+
// The basis of the closest ancestor container as a fraction of the width
|
|
74
|
+
// of the closest non-inlined schema, which provides `1`, see `DitoSchema`.
|
|
75
|
+
// The default is a factory that returns the injected function.
|
|
76
|
+
$accumulatedBasis: { default: () => () => 1 }
|
|
77
|
+
},
|
|
78
|
+
|
|
62
79
|
props: {
|
|
63
80
|
schema: { type: Object, required: true },
|
|
64
81
|
dataPath: { type: String, default: '' },
|
|
@@ -70,8 +87,7 @@ export default DitoComponent.component('DitoContainer', {
|
|
|
70
87
|
disabled: { type: Boolean, required: true },
|
|
71
88
|
compact: { type: Boolean, default: false },
|
|
72
89
|
generateLabels: { type: Boolean, default: false },
|
|
73
|
-
verticalLabels: { type: Boolean, default: false }
|
|
74
|
-
accumulatedBasis: { type: Number, default: null }
|
|
90
|
+
verticalLabels: { type: Boolean, default: false }
|
|
75
91
|
},
|
|
76
92
|
|
|
77
93
|
data() {
|
|
@@ -129,6 +145,19 @@ export default DitoComponent.component('DitoContainer', {
|
|
|
129
145
|
return this.hasLabel ? this.getLabel(this.schema) : null
|
|
130
146
|
},
|
|
131
147
|
|
|
148
|
+
shouldRenderLabel() {
|
|
149
|
+
// Type components can display the label themselves, e.g. sections in the
|
|
150
|
+
// header of their schema. They still receive it through `label`.
|
|
151
|
+
return (
|
|
152
|
+
this.hasLabel &&
|
|
153
|
+
!this.typeComponent?.rendersOwnLabel?.(this.context)
|
|
154
|
+
)
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
errorsElementId() {
|
|
158
|
+
return `${this.componentPath}-errors`
|
|
159
|
+
},
|
|
160
|
+
|
|
132
161
|
labelHtmlFor() {
|
|
133
162
|
// Type components use their component path as DOM id. Unnested types
|
|
134
163
|
// don't display a value of their own to label:
|
|
@@ -205,7 +234,8 @@ export default DitoComponent.component('DitoContainer', {
|
|
|
205
234
|
},
|
|
206
235
|
|
|
207
236
|
combinedBasis() {
|
|
208
|
-
const
|
|
237
|
+
const accumulatedBasis = this.$accumulatedBasis()
|
|
238
|
+
const { flexBasis } = this
|
|
209
239
|
return isNumber(accumulatedBasis) && isNumber(flexBasis)
|
|
210
240
|
? accumulatedBasis * flexBasis
|
|
211
241
|
: null
|
|
@@ -262,7 +292,7 @@ export default DitoComponent.component('DitoContainer', {
|
|
|
262
292
|
this.nested ? this.componentPath : this.parentComponent.componentPath,
|
|
263
293
|
this.componentPath,
|
|
264
294
|
this.$refs.component,
|
|
265
|
-
this.tabComponent
|
|
295
|
+
this.tabComponent?.panelTab ?? null
|
|
266
296
|
)
|
|
267
297
|
}
|
|
268
298
|
},
|
|
@@ -286,6 +316,17 @@ export default DitoComponent.component('DitoContainer', {
|
|
|
286
316
|
<style lang="scss">
|
|
287
317
|
@import '../styles/_imports';
|
|
288
318
|
|
|
319
|
+
@mixin narrow-page-layout($content-width) {
|
|
320
|
+
@container dito-page (width <= #{0.8 * $content-width}) {
|
|
321
|
+
flex-grow: 1;
|
|
322
|
+
flex-basis: var(--basis-mobile, var(--basis));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
@container dito-page (width <= #{0.6 * $content-width}) {
|
|
326
|
+
flex-basis: calc(2 * var(--basis));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
289
330
|
.dito-container {
|
|
290
331
|
--grow: 0;
|
|
291
332
|
--shrink: 1;
|
|
@@ -313,26 +354,23 @@ export default DitoComponent.component('DitoContainer', {
|
|
|
313
354
|
padding: 0;
|
|
314
355
|
}
|
|
315
356
|
|
|
357
|
+
// Let the components grow and wrap in pages and sidebars that are narrower
|
|
358
|
+
// than the width that their schemas are laid out for:
|
|
316
359
|
.dito-pane > & {
|
|
317
|
-
.dito-page--
|
|
318
|
-
|
|
319
|
-
flex-basis: var(--basis-mobile, var(--basis));
|
|
320
|
-
// DEBUG: background: yellow;
|
|
360
|
+
.dito-page:not(.dito-page--wide) & {
|
|
361
|
+
@include narrow-page-layout($content-width);
|
|
321
362
|
}
|
|
322
363
|
|
|
323
|
-
.dito-page--
|
|
324
|
-
|
|
325
|
-
// DEBUG: background: orange;
|
|
364
|
+
.dito-page--wide & {
|
|
365
|
+
@include narrow-page-layout($content-width-wide);
|
|
326
366
|
}
|
|
327
367
|
|
|
328
|
-
|
|
368
|
+
@container dito-sidebar (width < #{$sidebar-max-width}) {
|
|
329
369
|
flex-grow: 1;
|
|
330
|
-
// DEBUG: background: yellow;
|
|
331
370
|
}
|
|
332
371
|
|
|
333
|
-
|
|
372
|
+
@container dito-sidebar (width <= #{0.6 * $sidebar-max-width}) {
|
|
334
373
|
flex-basis: calc(2 * var(--basis));
|
|
335
|
-
// DEBUG: background: orange;
|
|
336
374
|
}
|
|
337
375
|
}
|
|
338
376
|
|
|
@@ -1,54 +1,40 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
2
|
.dito-create-button
|
|
3
|
-
|
|
3
|
+
DitoButton(
|
|
4
4
|
v-if="creatableForm"
|
|
5
5
|
:type="isInlined ? 'button' : 'submit'"
|
|
6
|
+
:verb="verb"
|
|
7
|
+
:subject="formLabel"
|
|
8
|
+
:text="text"
|
|
6
9
|
:disabled="disabled"
|
|
7
|
-
v-bind="getButtonAttributes(verb, formLabel, text)"
|
|
8
10
|
@click="createItem(creatableForm)"
|
|
9
|
-
) {{ text }}
|
|
10
|
-
template(
|
|
11
|
-
v-else-if="creatableForms"
|
|
12
11
|
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ul.dito-pulldown(
|
|
23
|
-
role="menu"
|
|
24
|
-
:class="{ 'dito-pulldown--open': pulldown.open }"
|
|
25
|
-
)
|
|
26
|
-
li(
|
|
27
|
-
v-for="(form, type) in creatableForms"
|
|
28
|
-
v-show="shouldShowSchema(form)"
|
|
29
|
-
)
|
|
30
|
-
a.dito-pulldown__item(
|
|
31
|
-
role="menuitem"
|
|
32
|
-
:class=`{
|
|
33
|
-
'dito-pulldown__item--disabled': shouldDisableSchema(form)
|
|
34
|
-
}`
|
|
35
|
-
@mousedown.stop="onPulldownMouseDown(type)"
|
|
36
|
-
@mouseup="onPulldownMouseUp(type)"
|
|
37
|
-
) {{ getLabel(form) }}
|
|
12
|
+
DitoMenuButton(
|
|
13
|
+
v-else-if="hasCreatableForms"
|
|
14
|
+
:items="menuItems"
|
|
15
|
+
placement="bottom-right"
|
|
16
|
+
:disabled="disabled"
|
|
17
|
+
:verb="verb"
|
|
18
|
+
:text="text"
|
|
19
|
+
@select="onSelectMenuItem"
|
|
20
|
+
)
|
|
38
21
|
</template>
|
|
39
22
|
|
|
40
23
|
<script>
|
|
24
|
+
import { DitoButton, DitoMenuButton } from '@ditojs/ui/src'
|
|
41
25
|
import DitoComponent from '../DitoComponent.js'
|
|
42
26
|
import ContextMixin from '../mixins/ContextMixin.js'
|
|
43
|
-
import PulldownMixin from '../mixins/PulldownMixin.js'
|
|
44
27
|
import { isInlined } from '../utils/schema/structure.js'
|
|
45
|
-
import {
|
|
28
|
+
import { getCreatableForms } from '../utils/schema/data.js'
|
|
46
29
|
|
|
47
30
|
// @vue/component
|
|
48
31
|
export default DitoComponent.component('DitoCreateButton', {
|
|
49
|
-
mixins: [ContextMixin
|
|
32
|
+
mixins: [ContextMixin],
|
|
33
|
+
components: { DitoButton, DitoMenuButton },
|
|
50
34
|
|
|
51
35
|
props: {
|
|
36
|
+
// The schema of the list or object source that creates the item, also
|
|
37
|
+
// when inserting at `insertIndex` from the buttons of an item.
|
|
52
38
|
schema: { type: Object, required: true },
|
|
53
39
|
// The next four props are there for `DitoContext` and the `context()`
|
|
54
40
|
// getter in `DitoMixin`.
|
|
@@ -72,29 +58,27 @@ export default DitoComponent.component('DitoCreateButton', {
|
|
|
72
58
|
},
|
|
73
59
|
|
|
74
60
|
computed: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// list schema. Fall back to `sourceSchema` (the parent list's schema via
|
|
78
|
-
// `meta.schema`) so all creatable forms are discoverable in the pulldown.
|
|
79
|
-
const schema =
|
|
80
|
-
this.insertIndex != null
|
|
81
|
-
? this.sourceSchema || this.schema
|
|
82
|
-
: this.schema
|
|
83
|
-
return getFormSchemas(schema, this.context)
|
|
61
|
+
creatableForms() {
|
|
62
|
+
return getCreatableForms(this.schema, this.context)
|
|
84
63
|
},
|
|
85
64
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
([, form]) => this.isFormCreatable(form)
|
|
89
|
-
)
|
|
90
|
-
return entries.length > 0
|
|
91
|
-
? Object.fromEntries(entries)
|
|
92
|
-
: null
|
|
65
|
+
hasCreatableForms() {
|
|
66
|
+
return Object.keys(this.creatableForms).length > 0
|
|
93
67
|
},
|
|
94
68
|
|
|
95
69
|
creatableForm() {
|
|
96
70
|
const forms = this.creatableForms
|
|
97
|
-
return
|
|
71
|
+
return (Object.keys(forms).length === 1 && forms.default) || null
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
menuItems() {
|
|
75
|
+
return Object.entries(this.creatableForms)
|
|
76
|
+
.filter(([, form]) => this.shouldShowSchema(form))
|
|
77
|
+
.map(([type, form]) => ({
|
|
78
|
+
value: type,
|
|
79
|
+
label: this.getLabel(form),
|
|
80
|
+
disabled: this.shouldDisableSchema(form)
|
|
81
|
+
}))
|
|
98
82
|
},
|
|
99
83
|
|
|
100
84
|
formLabel() {
|
|
@@ -111,19 +95,6 @@ export default DitoComponent.component('DitoCreateButton', {
|
|
|
111
95
|
},
|
|
112
96
|
|
|
113
97
|
methods: {
|
|
114
|
-
isFormCreatable(form) {
|
|
115
|
-
// Forms can be excluded from the list by providing `if: false` or
|
|
116
|
-
// `creatable: false`.
|
|
117
|
-
return (
|
|
118
|
-
this.shouldRenderSchema(form) &&
|
|
119
|
-
this.getSchemaValue('creatable', {
|
|
120
|
-
type: Boolean,
|
|
121
|
-
default: true,
|
|
122
|
-
schema: form
|
|
123
|
-
})
|
|
124
|
-
)
|
|
125
|
-
},
|
|
126
|
-
|
|
127
98
|
createItem(form, type = null) {
|
|
128
99
|
if (!this.shouldDisableSchema(form)) {
|
|
129
100
|
if (this.isInlined) {
|
|
@@ -144,28 +115,9 @@ export default DitoComponent.component('DitoCreateButton', {
|
|
|
144
115
|
}
|
|
145
116
|
},
|
|
146
117
|
|
|
147
|
-
|
|
148
|
-
this.createItem(this.
|
|
149
|
-
this.setPulldownOpen(false)
|
|
118
|
+
onSelectMenuItem({ value: type }) {
|
|
119
|
+
this.createItem(this.creatableForms[type], type)
|
|
150
120
|
}
|
|
151
121
|
}
|
|
152
122
|
})
|
|
153
123
|
</script>
|
|
154
|
-
|
|
155
|
-
<style lang="scss">
|
|
156
|
-
.dito-create-button {
|
|
157
|
-
position: relative;
|
|
158
|
-
|
|
159
|
-
.dito-pulldown {
|
|
160
|
-
right: 0;
|
|
161
|
-
|
|
162
|
-
// TODO: BEM: Use a modifier of this block for sticky button groups.
|
|
163
|
-
.dito-buttons--sticky & {
|
|
164
|
-
top: unset;
|
|
165
|
-
right: unset;
|
|
166
|
-
bottom: 0;
|
|
167
|
-
left: 0;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
</style>
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
.dito-dialog(
|
|
3
3
|
ref="dialog"
|
|
4
4
|
role="dialog"
|
|
5
|
-
:aria-label="
|
|
5
|
+
:aria-label="settings.label || 'Dialog'"
|
|
6
6
|
aria-modal="true"
|
|
7
7
|
:style="{ '--width': settings.width ? `${settings.width}px` : null }"
|
|
8
|
-
@mouseup="onMouseUp"
|
|
8
|
+
@mouseup.self="onMouseUp"
|
|
9
|
+
@keydown="onKeyDown"
|
|
9
10
|
)
|
|
10
11
|
UseFocusTrap.dito-dialog__focus-trap(:options="focusTrapOptions")
|
|
11
12
|
form.dito-dialog__form.dito-scroll-parent(
|
|
@@ -35,14 +36,12 @@
|
|
|
35
36
|
import { markRaw } from 'vue'
|
|
36
37
|
import { clone } from '@ditojs/utils'
|
|
37
38
|
import DitoComponent from '../DitoComponent.js'
|
|
38
|
-
import DomMixin from '../mixins/DomMixin.js'
|
|
39
39
|
import { getButtonSchemas } from '../utils/schema/lookup.js'
|
|
40
40
|
import { DataModel } from '../utils/DataModel.js'
|
|
41
41
|
import { UseFocusTrap } from '@vueuse/integrations/useFocusTrap/component'
|
|
42
42
|
|
|
43
43
|
// @vue/component
|
|
44
44
|
export default DitoComponent.component('DitoDialog', {
|
|
45
|
-
mixins: [DomMixin],
|
|
46
45
|
components: { UseFocusTrap },
|
|
47
46
|
emits: ['remove'],
|
|
48
47
|
|
|
@@ -115,7 +114,10 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
115
114
|
return {
|
|
116
115
|
immediate: true,
|
|
117
116
|
fallbackFocus: () => this.$refs.dialog,
|
|
118
|
-
|
|
117
|
+
// Escape is handled in `onKeyDown()`, as only dialogs with a cancel
|
|
118
|
+
// button or without buttons can be cancelled. The trap is also
|
|
119
|
+
// deactivated on unmount, e.g. after submitting, so it can't cancel.
|
|
120
|
+
escapeDeactivates: false
|
|
119
121
|
}
|
|
120
122
|
},
|
|
121
123
|
|
|
@@ -140,16 +142,6 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
140
142
|
)
|
|
141
143
|
},
|
|
142
144
|
|
|
143
|
-
mounted() {
|
|
144
|
-
this.domOn(window, {
|
|
145
|
-
keyup: event => {
|
|
146
|
-
if ((this.hasCancel || !this.hasButtons) && event.keyCode === 27) {
|
|
147
|
-
this.cancel()
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
},
|
|
152
|
-
|
|
153
145
|
beforeUnmount() {
|
|
154
146
|
// Stop the model before the component's own watchers are stopped, as the
|
|
155
147
|
// model's watchers aren't part of the component, see `DataModel`.
|
|
@@ -200,6 +192,19 @@ export default DitoComponent.component('DitoDialog', {
|
|
|
200
192
|
if (this.settings.clickToClose) {
|
|
201
193
|
this.close()
|
|
202
194
|
}
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
onKeyDown(event) {
|
|
198
|
+
// Escape keys that the dialog's components handled, e.g. to close their
|
|
199
|
+
// menus, don't cancel the dialog.
|
|
200
|
+
if (
|
|
201
|
+
event.key === 'Escape' &&
|
|
202
|
+
!event.defaultPrevented &&
|
|
203
|
+
(this.hasCancel || !this.hasButtons)
|
|
204
|
+
) {
|
|
205
|
+
event.preventDefault()
|
|
206
|
+
this.cancel()
|
|
207
|
+
}
|
|
203
208
|
}
|
|
204
209
|
}
|
|
205
210
|
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
//- Firefox doesn't start dragging from a <button>, so the handle is an <a>,
|
|
3
|
+
//- focusable through `tabindex` for the moves by keyboard.
|
|
4
|
+
DitoButton(
|
|
5
|
+
as="a"
|
|
6
|
+
role="button"
|
|
7
|
+
:verb="verbs.drag"
|
|
8
|
+
:disabled="disabled"
|
|
9
|
+
:tabindex="disabled ? -1 : 0"
|
|
10
|
+
aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown"
|
|
11
|
+
@keydown.alt.up.prevent="move(-1)"
|
|
12
|
+
@keydown.alt.down.prevent="move(1)"
|
|
13
|
+
)
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script>
|
|
17
|
+
import { DitoButton } from '@ditojs/ui/src'
|
|
18
|
+
import DitoComponent from '../DitoComponent.js'
|
|
19
|
+
|
|
20
|
+
// The handle that items are dragged by, see `SortableMixin`. With the keyboard,
|
|
21
|
+
// Alt+ArrowUp and Alt+ArrowDown move the item by emitting `move` with the
|
|
22
|
+
// delta of its index, which the owner of the list applies.
|
|
23
|
+
// @vue/component
|
|
24
|
+
export default DitoComponent.component('DitoDragHandle', {
|
|
25
|
+
components: { DitoButton },
|
|
26
|
+
emits: ['move'],
|
|
27
|
+
|
|
28
|
+
props: {
|
|
29
|
+
disabled: { type: Boolean, default: false }
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
methods: {
|
|
33
|
+
async move(delta) {
|
|
34
|
+
if (!this.disabled) {
|
|
35
|
+
this.$emit('move', delta)
|
|
36
|
+
// Moving the item can move the handle's element in the DOM, which
|
|
37
|
+
// loses its focus.
|
|
38
|
+
await this.$nextTick()
|
|
39
|
+
this.$el.focus()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
</script>
|
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
|
+
//- The sortable is always rendered, also when not `draggable`, so that the
|
|
3
|
+
//- children aren't remounted when `draggable` changes, see `watch`.
|
|
2
4
|
UseSortable.dito-draggable(
|
|
3
|
-
v-if="draggable"
|
|
4
5
|
:class="{ 'dito-draggable--dragging': isDragging }"
|
|
5
6
|
:as="as"
|
|
6
7
|
:modelValue="modelValue"
|
|
7
|
-
:options="
|
|
8
|
+
:options="sortableOptions"
|
|
8
9
|
@update:modelValue="$emit('update:modelValue', $event)"
|
|
9
10
|
)
|
|
10
11
|
slot
|
|
11
|
-
component(
|
|
12
|
-
v-else
|
|
13
|
-
:is="as"
|
|
14
|
-
)
|
|
15
|
-
slot
|
|
16
12
|
</template>
|
|
17
13
|
|
|
18
14
|
<script>
|
|
19
15
|
import DitoComponent from '../DitoComponent'
|
|
20
16
|
import DomMixin from '../mixins/DomMixin.js'
|
|
21
17
|
import { UseSortable } from '@vueuse/integrations/useSortable/component'
|
|
18
|
+
import Sortable from 'sortablejs'
|
|
22
19
|
|
|
23
20
|
// @vue/component
|
|
24
21
|
export default DitoComponent.component('DitoDraggable', {
|
|
@@ -47,33 +44,63 @@ export default DitoComponent.component('DitoDraggable', {
|
|
|
47
44
|
|
|
48
45
|
data() {
|
|
49
46
|
return {
|
|
50
|
-
mouseEvents: null,
|
|
51
47
|
isDragging: false
|
|
52
48
|
}
|
|
53
49
|
},
|
|
54
50
|
|
|
51
|
+
watch: {
|
|
52
|
+
draggable(draggable) {
|
|
53
|
+
this.getSortable()?.option('disabled', !draggable)
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
created() {
|
|
58
|
+
// `UseSortable` reads its options only once when it's mounted, so they're
|
|
59
|
+
// built once here. The handlers call the handlers of the current
|
|
60
|
+
// `options`, and `draggable` toggles the `disabled` option, see `watch`.
|
|
61
|
+
this.sortableOptions = {
|
|
62
|
+
...this.options,
|
|
63
|
+
disabled: !this.draggable,
|
|
64
|
+
onStart: this.onStart,
|
|
65
|
+
onEnd: this.onEnd
|
|
66
|
+
}
|
|
67
|
+
// The document handlers that clear `isDragging`, see `onEnd()`:
|
|
68
|
+
this.dragEndHandlers = null
|
|
69
|
+
},
|
|
70
|
+
|
|
55
71
|
methods: {
|
|
72
|
+
// Returns the `Sortable` instance that `UseSortable` created on the
|
|
73
|
+
// element.
|
|
74
|
+
getSortable() {
|
|
75
|
+
return this.$el instanceof HTMLElement ? Sortable.get(this.$el) : null
|
|
76
|
+
},
|
|
77
|
+
|
|
56
78
|
onStart(event) {
|
|
57
79
|
this.options.onStart?.(event)
|
|
58
80
|
this.isDragging = true
|
|
59
|
-
this.
|
|
81
|
+
this.removeDragEndHandlers()
|
|
60
82
|
},
|
|
61
83
|
|
|
62
84
|
onEnd(event) {
|
|
63
85
|
this.options.onEnd?.(event)
|
|
64
86
|
// Keep `isDragging` true until the next mouse interaction so that
|
|
65
87
|
// confused hover states are cleared before removing the hover catcher.
|
|
66
|
-
this.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
this.removeDragEndHandlers()
|
|
89
|
+
this.dragEndHandlers = this.domOn(document, {
|
|
90
|
+
mousedown: this.onMouseAfterDragEnd,
|
|
91
|
+
mousemove: this.onMouseAfterDragEnd,
|
|
92
|
+
mouseleave: this.onMouseAfterDragEnd
|
|
70
93
|
})
|
|
71
94
|
},
|
|
72
95
|
|
|
73
|
-
|
|
96
|
+
onMouseAfterDragEnd() {
|
|
74
97
|
this.isDragging = false
|
|
75
|
-
this.
|
|
76
|
-
|
|
98
|
+
this.removeDragEndHandlers()
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
removeDragEndHandlers() {
|
|
102
|
+
this.dragEndHandlers?.remove()
|
|
103
|
+
this.dragEndHandlers = null
|
|
77
104
|
}
|
|
78
105
|
}
|
|
79
106
|
})
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
2
|
//- Set `@click.stop` to prevent click events from bubbling to dito-label.
|
|
3
|
-
DitoButtons.dito-edit-buttons
|
|
3
|
+
DitoButtons.dito-edit-buttons(
|
|
4
|
+
:class="{ 'dito-buttons--round': round }"
|
|
4
5
|
:buttons="buttons"
|
|
5
6
|
:dataPath="dataPath"
|
|
6
7
|
:data="data"
|
|
7
8
|
:meta="meta"
|
|
8
9
|
:store="store"
|
|
9
10
|
:nested="nested"
|
|
11
|
+
:disabled="disabled"
|
|
10
12
|
@click.stop
|
|
11
13
|
)
|
|
12
|
-
|
|
13
|
-
a.dito-button(
|
|
14
|
+
DitoDragHandle(
|
|
14
15
|
v-if="draggable"
|
|
15
|
-
:
|
|
16
|
-
|
|
16
|
+
:disabled="isDraggableDisabled"
|
|
17
|
+
@move="delta => $emit('move', delta)"
|
|
17
18
|
)
|
|
18
|
-
|
|
19
|
+
DitoButton(
|
|
19
20
|
v-if="editable"
|
|
20
|
-
|
|
21
|
-
:to="
|
|
22
|
-
|
|
21
|
+
as="RouterLink"
|
|
22
|
+
:to="editRoute"
|
|
23
|
+
:verb="verbs.edit"
|
|
24
|
+
:disabled="isEditableDisabled"
|
|
23
25
|
)
|
|
24
26
|
DitoCreateButton(
|
|
25
27
|
v-if="creatable"
|
|
@@ -35,28 +37,36 @@ DitoButtons.dito-edit-buttons.dito-buttons--round(
|
|
|
35
37
|
:disabled="isCreatableDisabled"
|
|
36
38
|
:insertIndex="insertIndex"
|
|
37
39
|
)
|
|
38
|
-
|
|
40
|
+
DitoButton(
|
|
39
41
|
v-if="deletable"
|
|
40
|
-
|
|
42
|
+
:verb="verbs.delete"
|
|
41
43
|
:disabled="isDeletableDisabled"
|
|
42
|
-
v-bind="getButtonAttributes(verbs.delete)"
|
|
43
44
|
@click="$emit('delete')"
|
|
44
45
|
)
|
|
45
46
|
</template>
|
|
46
47
|
|
|
47
48
|
<script>
|
|
48
49
|
import DitoComponent from '../DitoComponent.js'
|
|
50
|
+
import DitoDragHandle from './DitoDragHandle.vue'
|
|
49
51
|
import ContextMixin from '../mixins/ContextMixin.js'
|
|
52
|
+
import { DitoButton } from '@ditojs/ui/src'
|
|
50
53
|
import { capitalize } from '@ditojs/utils'
|
|
51
54
|
|
|
52
55
|
// @vue/component
|
|
53
56
|
export default DitoComponent.component('DitoEditButtons', {
|
|
54
57
|
mixins: [ContextMixin],
|
|
55
|
-
|
|
58
|
+
components: { DitoButton, DitoDragHandle },
|
|
59
|
+
emits: ['delete', 'move'],
|
|
56
60
|
|
|
57
61
|
props: {
|
|
58
62
|
buttons: { type: Object, default: null },
|
|
63
|
+
// The schema of the component that owns the buttons, e.g. the list or
|
|
64
|
+
// object source, or the select. The `draggable`, `editable`, `creatable`
|
|
65
|
+
// and `deletable` props are already resolved from it.
|
|
59
66
|
schema: { type: Object, required: true },
|
|
67
|
+
// The form schema of the item that per-item buttons act on, which can
|
|
68
|
+
// disable the buttons for its items, see `hasFormOption()`.
|
|
69
|
+
formSchema: { type: Object, default: null },
|
|
60
70
|
dataPath: { type: String, required: true },
|
|
61
71
|
data: { type: [Object, Array], default: null },
|
|
62
72
|
meta: { type: Object, required: true },
|
|
@@ -68,8 +78,13 @@ export default DitoComponent.component('DitoEditButtons', {
|
|
|
68
78
|
creatable: { type: Boolean, default: false },
|
|
69
79
|
deletable: { type: Boolean, default: false },
|
|
70
80
|
editPath: { type: String, default: null },
|
|
81
|
+
// The query of the edit route, e.g. to keep the current one in trees.
|
|
82
|
+
editQuery: { type: Object, default: null },
|
|
71
83
|
createPath: { type: String, default: null },
|
|
72
|
-
insertIndex: { type: Number, default: null }
|
|
84
|
+
insertIndex: { type: Number, default: null },
|
|
85
|
+
// Whether the buttons are round, as in lists and objects, or square and
|
|
86
|
+
// compact, as in the rows of trees.
|
|
87
|
+
round: { type: Boolean, default: true }
|
|
73
88
|
},
|
|
74
89
|
|
|
75
90
|
computed: {
|
|
@@ -77,19 +92,24 @@ export default DitoComponent.component('DitoEditButtons', {
|
|
|
77
92
|
return this.getLabel(this.schema.form)
|
|
78
93
|
},
|
|
79
94
|
|
|
95
|
+
editRoute() {
|
|
96
|
+
const { editPath, editQuery } = this
|
|
97
|
+
return editPath ? { path: editPath, query: editQuery ?? undefined } : {}
|
|
98
|
+
},
|
|
99
|
+
|
|
80
100
|
isDraggableDisabled() {
|
|
81
|
-
return this.disabled || !this.
|
|
101
|
+
return this.disabled || !this.hasFormOption('draggable')
|
|
82
102
|
},
|
|
83
103
|
|
|
84
104
|
isDeletableDisabled() {
|
|
85
|
-
return this.disabled || !this.
|
|
105
|
+
return this.disabled || !this.hasFormOption('deletable')
|
|
86
106
|
},
|
|
87
107
|
|
|
88
108
|
isEditableDisabled() {
|
|
89
109
|
return (
|
|
90
110
|
this.disabled ||
|
|
91
111
|
!this.editPath ||
|
|
92
|
-
!this.
|
|
112
|
+
!this.hasFormOption('editable')
|
|
93
113
|
)
|
|
94
114
|
},
|
|
95
115
|
|
|
@@ -97,7 +117,7 @@ export default DitoComponent.component('DitoEditButtons', {
|
|
|
97
117
|
return (
|
|
98
118
|
this.disabled ||
|
|
99
119
|
!this.createPath ||
|
|
100
|
-
!this.
|
|
120
|
+
!this.hasFormOption('creatable')
|
|
101
121
|
)
|
|
102
122
|
},
|
|
103
123
|
|
|
@@ -116,13 +136,17 @@ export default DitoComponent.component('DitoEditButtons', {
|
|
|
116
136
|
},
|
|
117
137
|
|
|
118
138
|
methods: {
|
|
119
|
-
|
|
139
|
+
hasFormOption(name) {
|
|
120
140
|
// All options can be disabled on a per-form basis by setting
|
|
121
|
-
// `
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
// `formSchema[name]` to `false` or a callback returning `false`.
|
|
142
|
+
return (
|
|
143
|
+
!this.formSchema ||
|
|
144
|
+
this.getSchemaValue(name, {
|
|
145
|
+
schema: this.formSchema,
|
|
146
|
+
type: Boolean,
|
|
147
|
+
default: true
|
|
148
|
+
})
|
|
149
|
+
)
|
|
126
150
|
}
|
|
127
151
|
}
|
|
128
152
|
})
|