@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,146 @@
1
+ <template lang="pug">
2
+ .dito-create-button
3
+ button.dito-button(
4
+ v-if="creatableForm"
5
+ :type="isInlined ? 'button' : 'submit'"
6
+ :disabled="disabled"
7
+ v-bind="getButtonAttributes(verb)"
8
+ @click="createItem(creatableForm)"
9
+ ) {{ text }}
10
+ template(
11
+ v-else-if="creatableForms"
12
+ )
13
+ button.dito-button(
14
+ type="button"
15
+ :disabled="disabled"
16
+ v-bind="getButtonAttributes(verb)"
17
+ @mousedown.stop="onPulldownMouseDown()"
18
+ ) {{ text }}
19
+ ul.dito-pulldown(:class="{ 'dito-pulldown--open': pulldown.open }")
20
+ li(
21
+ v-for="(form, type) in creatableForms"
22
+ v-show="shouldShowSchema(form)"
23
+ )
24
+ a.dito-pulldown__item(
25
+ :class=`{
26
+ 'dito-pulldown__item--disabled': shouldDisableSchema(form)
27
+ }`
28
+ @mousedown.stop="onPulldownMouseDown(type)"
29
+ @mouseup="onPulldownMouseUp(type)"
30
+ ) {{ getLabel(form) }}
31
+ </template>
32
+
33
+ <script>
34
+ import DitoComponent from '../DitoComponent.js'
35
+ import ContextMixin from '../mixins/ContextMixin.js'
36
+ import PulldownMixin from '../mixins/PulldownMixin.js'
37
+ import { getFormSchemas, isInlined } from '../utils/schema.js'
38
+
39
+ // @vue/component
40
+ export default DitoComponent.component('DitoCreateButton', {
41
+ mixins: [ContextMixin, PulldownMixin],
42
+
43
+ props: {
44
+ schema: { type: Object, required: true },
45
+ // The next four props are there for `DitoContext` and the `context()`
46
+ // getter in `DitoMixin`.
47
+ // TODO: Should they be moved to shared mixin that defines them as required
48
+ // and also provides the `context()` getter, perhaps `ContextMixin`?
49
+ // `schema` could be included as well, and `ContextMixin` could be used in
50
+ // `DitoForm`, `DitoView`, `DitoPanel`, `DitoSchema`, `DitoEditButtons`,
51
+ // etc? But the problem with the root components is that they don't have
52
+ // these props. We could add a `contextAttributes()` getter for easy passing
53
+ // on as `v-bind="contextAttributes"`.
54
+ dataPath: { type: String, required: true },
55
+ data: { type: [Object, Array], default: null },
56
+ meta: { type: Object, required: true },
57
+ store: { type: Object, required: true },
58
+ nested: { type: Boolean, default: false },
59
+ path: { type: String, required: true },
60
+ verb: { type: String, required: true },
61
+ text: { type: String, default: null },
62
+ disabled: { type: Boolean, required: true }
63
+ },
64
+
65
+ computed: {
66
+ forms() {
67
+ return getFormSchemas(this.schema, this.context)
68
+ },
69
+
70
+ creatableForms() {
71
+ const entries = Object.entries(this.forms).filter(
72
+ ([, form]) => this.isFormCreatable(form)
73
+ )
74
+ return entries.length > 0
75
+ ? Object.fromEntries(entries)
76
+ : null
77
+ },
78
+
79
+ creatableForm() {
80
+ const forms = this.creatableForms
81
+ return forms && Object.keys(forms).length === 1 && forms.default || null
82
+ },
83
+
84
+ isInlined() {
85
+ return isInlined(this.schema)
86
+ }
87
+ },
88
+
89
+ methods: {
90
+ isFormCreatable(form) {
91
+ // Forms can be excluded from the list by providing `if: false` or
92
+ // `creatable: false`.
93
+ return (
94
+ this.shouldRenderSchema(form) &&
95
+ this.getSchemaValue('creatable', {
96
+ type: Boolean,
97
+ default: true,
98
+ schema: form
99
+ })
100
+ )
101
+ },
102
+
103
+ createItem(form, type = null) {
104
+ if (!this.shouldDisableSchema(form)) {
105
+ if (this.isInlined) {
106
+ this.sourceComponent.createItem(form, type)
107
+ } else {
108
+ const { creatable } = this.schema
109
+ const query = {
110
+ ...(type && { type }),
111
+ ...creatable?.query?.(this.context)
112
+ }
113
+ this.$router.push({
114
+ path: `${this.path}/create`,
115
+ query
116
+ })
117
+ }
118
+ } else {
119
+ throw new Error('Not allowed to create item for given form')
120
+ }
121
+ },
122
+
123
+ onPulldownSelect(type) {
124
+ this.createItem(this.forms[type], type)
125
+ this.setPulldownOpen(false)
126
+ }
127
+ }
128
+ })
129
+ </script>
130
+
131
+ <style lang="scss">
132
+ .dito-create-button {
133
+ position: relative;
134
+
135
+ .dito-pulldown {
136
+ right: 0;
137
+
138
+ .dito-buttons--sticky & {
139
+ top: unset;
140
+ right: unset;
141
+ bottom: 0;
142
+ left: 0;
143
+ }
144
+ }
145
+ }
146
+ </style>
@@ -0,0 +1,242 @@
1
+ <template lang="pug">
2
+ .dito-dialog(
3
+ ref="dialog"
4
+ role="dialog"
5
+ aria-expanded="true"
6
+ aria-modal="true"
7
+ :style="{ '--width': settings.width ? `${settings.width}px` : null }"
8
+ @mouseup="onMouseUp"
9
+ )
10
+ UseFocusTrap.dito-dialog__focus-trap(:options="focusTrapOptions")
11
+ form.dito-scroll-parent(
12
+ @submit.prevent="submit"
13
+ @keydown.enter="onEnter"
14
+ )
15
+ // Add an invisible button that prevents the clearable buttons from being
16
+ // pressed when the user presses the Enter key:
17
+ button(
18
+ v-show="false"
19
+ )
20
+ DitoSchema(
21
+ :schema="schema"
22
+ :data="dialogData"
23
+ padding="root"
24
+ scrollable
25
+ generateLabels
26
+ )
27
+ template(#buttons)
28
+ DitoButtons.dito-buttons--large(
29
+ :buttons="buttonSchemas"
30
+ :data="dialogData"
31
+ )
32
+ </template>
33
+
34
+ <script>
35
+ import { clone } from '@ditojs/utils'
36
+ import DitoComponent from '../DitoComponent.js'
37
+ import DomMixin from '../mixins/DomMixin.js'
38
+ import { getButtonSchemas } from '../utils/schema.js'
39
+ import { UseFocusTrap } from '@vueuse/integrations/useFocusTrap/component'
40
+
41
+ // @vue/component
42
+ export default DitoComponent.component('DitoDialog', {
43
+ mixins: [DomMixin],
44
+ components: { UseFocusTrap },
45
+ emits: ['remove'],
46
+
47
+ provide() {
48
+ return {
49
+ $dialogComponent: () => this
50
+ }
51
+ },
52
+
53
+ props: {
54
+ components: { type: Object, required: true },
55
+ buttons: { type: Object, required: true },
56
+ promise: { type: Object, required: true },
57
+ data: { type: Object, default: () => ({}) },
58
+ settings: {
59
+ type: Object,
60
+ default: () => ({
61
+ width: 480,
62
+ clickToClose: false
63
+ })
64
+ }
65
+ },
66
+
67
+ data() {
68
+ // Make sure dialog data contains all the expected keys
69
+ const dialogData = clone(this.data)
70
+ for (const key in this.components) {
71
+ if (!(key in dialogData)) {
72
+ dialogData[key] = null
73
+ }
74
+ }
75
+ return {
76
+ dialogData
77
+ }
78
+ },
79
+
80
+ computed: {
81
+ dialogComponent() {
82
+ return this
83
+ },
84
+
85
+ schema() {
86
+ return {
87
+ type: 'dialog',
88
+ components: this.components
89
+ }
90
+ },
91
+
92
+ buttonSchemas() {
93
+ return Object.fromEntries(
94
+ Object.entries(getButtonSchemas(this.buttons)).map(
95
+ // Process the button schemas to add default click events
96
+ // for both 'submit' and 'cancel' buttons:
97
+ ([key, schema]) => {
98
+ if (key === 'cancel' && !schema.events) {
99
+ schema = {
100
+ ...schema,
101
+ events: {
102
+ click: () => this.cancel()
103
+ }
104
+ }
105
+ }
106
+ return [key, schema]
107
+ }
108
+ )
109
+ )
110
+ },
111
+
112
+ focusTrapOptions() {
113
+ return {
114
+ immediate: true,
115
+ fallbackFocus: () => this.$refs.dialog,
116
+ onDeactivate: this.cancel
117
+ }
118
+ },
119
+
120
+ hasButtons() {
121
+ return Object.keys(this.buttonSchemas).length > 0
122
+ },
123
+
124
+ hasCancel() {
125
+ return !!this.buttonSchemas.cancel
126
+ }
127
+ },
128
+
129
+ mounted() {
130
+ this.domOn(window, {
131
+ keyup: event => {
132
+ if ((this.hasCancel || !this.hasButtons) && event.keyCode === 27) {
133
+ this.cancel()
134
+ }
135
+ }
136
+ })
137
+ },
138
+
139
+ methods: {
140
+ remove() {
141
+ this.$emit('remove')
142
+ },
143
+
144
+ resolve(value) {
145
+ this.promise.resolve(value)
146
+ this.remove()
147
+ },
148
+
149
+ reject(value) {
150
+ this.promise.reject(value)
151
+ this.remove()
152
+ },
153
+
154
+ onEnter(event) {
155
+ // Handle Enter key on input fields to submit the form. This also handles
156
+ // password managers (e.g. 1Password) that simulate an untrusted Enter
157
+ // keypress to submit, which browsers ignore.
158
+ if (event.target instanceof HTMLInputElement) {
159
+ this.submit()
160
+ }
161
+ },
162
+
163
+ submit() {
164
+ this.resolve(this.dialogData)
165
+ },
166
+
167
+ cancel() {
168
+ // When cancelling, resolve as `undefined` so we can have dialogs
169
+ // returning null as a defined value as well.
170
+ this.resolve(undefined)
171
+ },
172
+
173
+ close() {
174
+ this.cancel()
175
+ },
176
+
177
+ onMouseUp() {
178
+ if (this.settings.clickToClose) {
179
+ this.close()
180
+ }
181
+ }
182
+ }
183
+ })
184
+ </script>
185
+
186
+ <style lang="scss">
187
+ @import '../styles/_imports';
188
+
189
+ .dito-dialog {
190
+ position: fixed;
191
+ display: flex;
192
+ inset: 0;
193
+ z-index: $z-index-dialog;
194
+ padding: $content-padding;
195
+ align-items: center;
196
+ justify-content: center;
197
+ // Prevent scrolling of the page behind the dialog:
198
+ overflow: hidden;
199
+ background: rgb(0, 0, 0, 0.2);
200
+
201
+ &__focus-trap {
202
+ display: flex;
203
+ max-height: 100%;
204
+ }
205
+
206
+ // Place cancel button visually first even though submit comes first in DOM,
207
+ // so that password managers (e.g. 1Password) find the submit button first.
208
+ .dito-container:has(.dito-button--cancel) {
209
+ order: -1;
210
+ }
211
+
212
+ // TODO: `&__inner`
213
+ form {
214
+ position: relative;
215
+ display: flex;
216
+ box-sizing: border-box;
217
+ background: white;
218
+ border-radius: $border-radius;
219
+ max-width: var(--width, 480px);
220
+ max-height: 100%;
221
+ box-shadow: 0 20px 60px -2px rgb(27, 33, 58, 0.4);
222
+ }
223
+ }
224
+
225
+ .dito-dialog-enter-active,
226
+ .dito-dialog-leave-active {
227
+ transition: opacity 0.15s;
228
+
229
+ form {
230
+ transition: transform 0.25s;
231
+ }
232
+ }
233
+
234
+ .dito-dialog-enter-from,
235
+ .dito-dialog-leave-to {
236
+ opacity: 0;
237
+
238
+ form {
239
+ transform: translateY(-20px);
240
+ }
241
+ }
242
+ </style>
@@ -0,0 +1,117 @@
1
+ <template lang="pug">
2
+ UseSortable.dito-draggable(
3
+ v-if="draggable"
4
+ :class="{ 'dito-draggable--dragging': isDragging }"
5
+ :as="as"
6
+ :modelValue="modelValue"
7
+ :options="{ ...options, onStart, onEnd }"
8
+ @update:modelValue="$emit('update:modelValue', $event)"
9
+ )
10
+ slot
11
+ component(
12
+ v-else
13
+ :is="as"
14
+ )
15
+ slot
16
+ </template>
17
+
18
+ <script>
19
+ import DitoComponent from '../DitoComponent'
20
+ import DomMixin from '../mixins/DomMixin.js'
21
+ import { UseSortable } from '@vueuse/integrations/useSortable/component'
22
+
23
+ // @vue/component
24
+ export default DitoComponent.component('DitoDraggable', {
25
+ mixins: [DomMixin],
26
+ components: { UseSortable },
27
+ emits: ['update:modelValue'],
28
+
29
+ props: {
30
+ modelValue: {
31
+ type: Array,
32
+ required: true
33
+ },
34
+ as: {
35
+ type: String,
36
+ default: 'div'
37
+ },
38
+ options: {
39
+ type: Object,
40
+ required: true
41
+ },
42
+ draggable: {
43
+ type: Boolean,
44
+ default: true
45
+ }
46
+ },
47
+
48
+ data() {
49
+ return {
50
+ mouseEvents: null,
51
+ isDragging: false
52
+ }
53
+ },
54
+
55
+ methods: {
56
+ onStart(event) {
57
+ this.options.onStart?.(event)
58
+ this.isDragging = true
59
+ this.mouseEvents?.remove()
60
+ },
61
+
62
+ onEnd(event) {
63
+ this.options.onEnd?.(event)
64
+ // Keep `isDragging` true until the next mouse interaction so that
65
+ // confused hover states are cleared before removing the hover catcher.
66
+ this.mouseEvents = this.domOn(document, {
67
+ mousedown: this.onMouse,
68
+ mousemove: this.onMouse,
69
+ mouseleave: this.onMouse
70
+ })
71
+ },
72
+
73
+ onMouse() {
74
+ this.isDragging = false
75
+ this.mouseEvents?.remove()
76
+ this.mouseEvents = null
77
+ }
78
+ }
79
+ })
80
+ </script>
81
+
82
+ <style lang="scss">
83
+ @import '../styles/_imports';
84
+
85
+ .dito-draggable {
86
+ // Overlay a hover catcher while we're dragging to prevent hover states from
87
+ // getting stuck / confused. Safari struggles with this, so disable it there.
88
+ @include browser-query(('chrome', 'firefox')) {
89
+ &:has(&__chosen),
90
+ &--dragging {
91
+ > * {
92
+ position: relative;
93
+
94
+ > :first-child::after {
95
+ content: '';
96
+ position: absolute;
97
+ inset: 0;
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ &__fallback {
104
+ filter: drop-shadow(0 2px 4px $color-shadow);
105
+
106
+ // Nested <td> need to also switch to `display: flex` style during dragging.
107
+ &,
108
+ td {
109
+ display: flex;
110
+
111
+ > * {
112
+ flex: 1;
113
+ }
114
+ }
115
+ }
116
+ }
117
+ </style>
@@ -0,0 +1,135 @@
1
+ <template lang="pug">
2
+ //- Set `@click.stop` to prevent click events from bubbling to dito-label.
3
+ DitoButtons.dito-edit-buttons.dito-buttons--round(
4
+ :buttons="buttons"
5
+ :dataPath="dataPath"
6
+ :data="data"
7
+ :meta="meta"
8
+ :store="store"
9
+ :nested="nested"
10
+ @click.stop
11
+ )
12
+ //- Firefox doesn't like <button> here, so use <a> instead:
13
+ a.dito-button(
14
+ v-if="draggable"
15
+ :class="{ 'dito-button--disabled': isDraggableDisabled }"
16
+ v-bind="getButtonAttributes(verbs.drag)"
17
+ )
18
+ RouterLink.dito-button(
19
+ v-if="editable"
20
+ :class="{ 'dito-button--disabled': isEditableDisabled }"
21
+ :to="editPath ? { path: editPath } : {}"
22
+ v-bind="getButtonAttributes(verbs.edit)"
23
+ )
24
+ DitoCreateButton(
25
+ v-if="creatable"
26
+ :schema="schema"
27
+ :dataPath="dataPath"
28
+ :data="data"
29
+ :meta="meta"
30
+ :store="store"
31
+ :nested="nested"
32
+ :path="createPath"
33
+ :verb="verbs.create"
34
+ :text="createButtonText"
35
+ :disabled="isCreatableDisabled"
36
+ )
37
+ button.dito-button(
38
+ v-if="deletable"
39
+ type="button"
40
+ :disabled="isDeletableDisabled"
41
+ v-bind="getButtonAttributes(verbs.delete)"
42
+ @click="$emit('delete')"
43
+ )
44
+ </template>
45
+
46
+ <script>
47
+ import DitoComponent from '../DitoComponent.js'
48
+ import ContextMixin from '../mixins/ContextMixin.js'
49
+ import { capitalize } from '@ditojs/utils'
50
+
51
+ // @vue/component
52
+ export default DitoComponent.component('DitoEditButtons', {
53
+ mixins: [ContextMixin],
54
+ emits: ['delete'],
55
+
56
+ props: {
57
+ buttons: { type: Object, default: null },
58
+ schema: { type: Object, required: true },
59
+ dataPath: { type: String, required: true },
60
+ data: { type: [Object, Array], default: null },
61
+ meta: { type: Object, required: true },
62
+ store: { type: Object, required: true },
63
+ nested: { type: Boolean, default: false },
64
+ disabled: { type: Boolean, required: true },
65
+ draggable: { type: Boolean, default: false },
66
+ editable: { type: Boolean, default: false },
67
+ creatable: { type: Boolean, default: false },
68
+ deletable: { type: Boolean, default: false },
69
+ editPath: { type: String, default: null },
70
+ createPath: { type: String, default: null }
71
+ },
72
+
73
+ computed: {
74
+ formLabel() {
75
+ return this.getLabel(this.schema.form)
76
+ },
77
+
78
+ isDraggableDisabled() {
79
+ return this.disabled || !this.hasSchemaOption('draggable')
80
+ },
81
+
82
+ isDeletableDisabled() {
83
+ return this.disabled || !this.hasSchemaOption('deletable')
84
+ },
85
+
86
+ isEditableDisabled() {
87
+ return (
88
+ this.disabled ||
89
+ !this.editPath ||
90
+ !this.hasSchemaOption('editable')
91
+ )
92
+ },
93
+
94
+ isCreatableDisabled() {
95
+ return (
96
+ this.disabled ||
97
+ !this.createPath ||
98
+ !this.hasSchemaOption('creatable')
99
+ )
100
+ },
101
+
102
+ createButtonText() {
103
+ return (
104
+ // Allow schema to override create button through creatable object:
105
+ this.schema.creatable?.label || (
106
+ // Auto-generate create button labels from from labels for list
107
+ // sources with only one form:
108
+ this.formLabel &&
109
+ `${capitalize(this.verbs.create)} ${this.formLabel}`
110
+ ) ||
111
+ null
112
+ )
113
+ }
114
+ },
115
+
116
+ methods: {
117
+ hasSchemaOption(name) {
118
+ // All options can be disabled on a per-form basis by setting
119
+ // `schema[name]` to `false` or a callback returning `false`.
120
+ return this.getSchemaValue(name, {
121
+ type: Boolean,
122
+ default: true
123
+ })
124
+ }
125
+ }
126
+ })
127
+ </script>
128
+
129
+ <style lang="scss">
130
+ .dito-edit-buttons {
131
+ // Override cursor from collapsible dito-label:
132
+ cursor: default;
133
+ flex: none;
134
+ }
135
+ </style>
@@ -0,0 +1,83 @@
1
+ <template lang="pug">
2
+ .dito-errors(
3
+ v-if="errors"
4
+ )
5
+ ul
6
+ li(
7
+ v-for="error of errors"
8
+ ) {{ error }}
9
+ </template>
10
+
11
+ <script>
12
+ import tippy from 'tippy.js'
13
+ import DitoComponent from '../DitoComponent.js'
14
+ import { markRaw } from 'vue'
15
+
16
+ // @vue/component
17
+ export default DitoComponent.component('DitoErrors', {
18
+ props: {
19
+ errors: { type: Array, default: null }
20
+ },
21
+
22
+ data() {
23
+ return {
24
+ tip: null
25
+ }
26
+ },
27
+
28
+ watch: {
29
+ errors() {
30
+ this.$nextTick(this.updateErrors)
31
+ }
32
+ },
33
+
34
+ unmounted() {
35
+ this.tip?.destroy()
36
+ },
37
+
38
+ methods: {
39
+ updateErrors() {
40
+ let { tip } = this
41
+ tip?.hide()
42
+ if (this.errors) {
43
+ tip = this.tip ??= markRaw(tippy(this.$el.closest('.dito-container')))
44
+ tip.setProps({
45
+ content: this.errors.join('\n'),
46
+ theme: 'error',
47
+ trigger: 'manual',
48
+ appendTo: 'parent',
49
+ placement: 'bottom-start',
50
+ animation: 'shift-away-subtle',
51
+ popperOptions: {
52
+ modifiers: [
53
+ {
54
+ name: 'flip',
55
+ enabled: false
56
+ }
57
+ ]
58
+ },
59
+ interactive: true,
60
+ hideOnClick: false,
61
+ offset: [3, 3], // 1/2 form-spacing
62
+ zIndex: 1
63
+ })
64
+ tip.popper.addEventListener('mousedown', () => tip.hide())
65
+ tip.show()
66
+ }
67
+ }
68
+ }
69
+ })
70
+ </script>
71
+
72
+ <style lang="scss">
73
+ @import '../styles/_imports';
74
+
75
+ .dito-errors {
76
+ position: absolute;
77
+ visibility: hidden;
78
+
79
+ ul {
80
+ color: $color-error;
81
+ }
82
+ }
83
+ </style>