@ditojs/admin 3.2.6 → 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.
Files changed (112) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +6012 -5827
  3. package/dist/dito-admin.umd.js +3 -7
  4. package/package.json +18 -17
  5. package/src/DitoAdmin.js +44 -24
  6. package/src/DitoContext.js +28 -2
  7. package/src/DitoTypeComponent.js +1 -0
  8. package/src/appState.js +6 -3
  9. package/src/components/DitoAccount.vue +35 -27
  10. package/src/components/DitoAffixes.vue +9 -2
  11. package/src/components/DitoClipboard.vue +59 -93
  12. package/src/components/DitoConfirmMessage.vue +18 -0
  13. package/src/components/DitoContainer.vue +56 -18
  14. package/src/components/DitoCreateButton.vue +36 -84
  15. package/src/components/DitoDialog.vue +20 -15
  16. package/src/components/DitoDragHandle.vue +44 -0
  17. package/src/components/DitoDraggable.vue +43 -16
  18. package/src/components/DitoEditButtons.vue +48 -24
  19. package/src/components/DitoErrors.vue +9 -56
  20. package/src/components/DitoForm.vue +82 -42
  21. package/src/components/DitoInputAffixes.vue +50 -0
  22. package/src/components/DitoLabel.vue +1 -15
  23. package/src/components/DitoMenu.vue +45 -52
  24. package/src/components/DitoNotifications.vue +38 -14
  25. package/src/components/DitoOptionList.vue +81 -0
  26. package/src/components/DitoOptionsEditButtons.vue +57 -0
  27. package/src/components/DitoPagination.vue +1 -8
  28. package/src/components/DitoPane.vue +51 -39
  29. package/src/components/DitoPanel.vue +12 -26
  30. package/src/components/DitoPanels.vue +11 -5
  31. package/src/components/DitoResizeHandle.vue +47 -0
  32. package/src/components/DitoRoot.vue +113 -320
  33. package/src/components/DitoSchema.vue +72 -63
  34. package/src/components/DitoSchemaInlined.vue +8 -9
  35. package/src/components/DitoScopes.vue +14 -18
  36. package/src/components/DitoSidebar.vue +4 -29
  37. package/src/components/DitoSourceItem.vue +114 -0
  38. package/src/components/DitoTableCell.vue +4 -0
  39. package/src/components/DitoTableHead.vue +15 -17
  40. package/src/components/DitoTabs.vue +25 -0
  41. package/src/components/DitoTrail.vue +25 -25
  42. package/src/components/DitoTreeItem.vue +102 -141
  43. package/src/components/DitoUploadFile.vue +5 -2
  44. package/src/components/DitoView.vue +33 -24
  45. package/src/index.js +4 -1
  46. package/src/mixins/DitoMixin.js +77 -110
  47. package/src/mixins/DomMixin.js +12 -14
  48. package/src/mixins/EmitterMixin.js +3 -2
  49. package/src/mixins/ItemMixin.js +38 -94
  50. package/src/mixins/OptionsMixin.js +38 -25
  51. package/src/mixins/ResourceMixin.js +182 -111
  52. package/src/mixins/RouteMixin.js +61 -44
  53. package/src/mixins/SchemaParentMixin.js +4 -1
  54. package/src/mixins/SourceMixin.js +118 -100
  55. package/src/mixins/TypeMixin.js +42 -40
  56. package/src/mixins/ValidationMixin.js +16 -4
  57. package/src/mixins/ValidatorMixin.js +1 -1
  58. package/src/styles/_settings.scss +4 -0
  59. package/src/styles/_tippy.scss +0 -11
  60. package/src/styles/style.scss +0 -1
  61. package/src/types/DitoTypeButton.vue +11 -14
  62. package/src/types/DitoTypeCheckboxes.vue +35 -17
  63. package/src/types/DitoTypeCode.vue +19 -41
  64. package/src/types/DitoTypeColor.vue +67 -19
  65. package/src/types/DitoTypeDate.vue +11 -14
  66. package/src/types/DitoTypeList.vue +50 -62
  67. package/src/types/DitoTypeMarkup.vue +41 -54
  68. package/src/types/DitoTypeMultiselect.vue +82 -52
  69. package/src/types/DitoTypeNumber.vue +12 -15
  70. package/src/types/DitoTypeObject.vue +4 -38
  71. package/src/types/DitoTypeRadio.vue +19 -6
  72. package/src/types/DitoTypeSection.vue +23 -9
  73. package/src/types/DitoTypeSelect.vue +39 -23
  74. package/src/types/DitoTypeSlider.vue +9 -1
  75. package/src/types/DitoTypeSwitch.vue +1 -1
  76. package/src/types/DitoTypeText.vue +11 -14
  77. package/src/types/DitoTypeTreeList.vue +46 -8
  78. package/src/types/DitoTypeUpload.vue +190 -67
  79. package/src/utils/DataModel.js +368 -146
  80. package/src/utils/FileDropTargets.js +97 -0
  81. package/src/utils/ItemStores.js +31 -0
  82. package/src/utils/ListQuery.js +115 -0
  83. package/src/utils/LoadingTracker.js +76 -0
  84. package/src/utils/SchemaGraph.js +3 -2
  85. package/src/utils/Session.js +151 -0
  86. package/src/utils/ViewRegistry.js +85 -0
  87. package/src/utils/accessor.js +12 -4
  88. package/src/utils/affixes.js +33 -0
  89. package/src/utils/changeOnceEdited.js +33 -0
  90. package/src/utils/clipboard.js +55 -0
  91. package/src/utils/data.js +10 -5
  92. package/src/utils/dialogs.js +59 -0
  93. package/src/utils/dragResize.js +61 -0
  94. package/src/utils/filter.js +36 -2
  95. package/src/utils/html.js +9 -0
  96. package/src/utils/list.js +10 -0
  97. package/src/utils/options.js +4 -0
  98. package/src/utils/registration.js +18 -0
  99. package/src/utils/request.js +108 -13
  100. package/src/utils/route.js +6 -17
  101. package/src/utils/schema/data.js +29 -2
  102. package/src/utils/schema/events.js +19 -0
  103. package/src/utils/schema/item.js +114 -0
  104. package/src/utils/schema/lookup.js +39 -14
  105. package/src/utils/schema/setup.js +5 -5
  106. package/src/utils/schema/validation.js +16 -2
  107. package/src/utils/uid.js +105 -8
  108. package/src/validators/_decimals.js +16 -5
  109. package/types/index.d.ts +168 -41
  110. package/src/mixins/LoadingMixin.js +0 -23
  111. package/src/mixins/PulldownMixin.js +0 -72
  112. package/src/styles/_pulldown.scss +0 -38
@@ -16,7 +16,7 @@
16
16
  :disabled="disabled"
17
17
  )
18
18
  DitoLabel(
19
- v-if="hasLabel"
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
- :accumulatedBasis="combinedBasis"
39
+ :errorsElementId="errorsElementId"
40
40
  @errors="onErrors"
41
41
  @update:component="value => (component = value)"
42
42
  )
43
- DitoErrors(:errors="errors")
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 { accumulatedBasis, flexBasis } = this
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--width-80 & {
318
- flex-grow: 1;
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--width-60 & {
324
- flex-basis: calc(2 * var(--basis));
325
- // DEBUG: background: orange;
364
+ .dito-page--wide & {
365
+ @include narrow-page-layout($content-width-wide);
326
366
  }
327
367
 
328
- .dito-sidebar--width-99 & {
368
+ @container dito-sidebar (width < #{$sidebar-max-width}) {
329
369
  flex-grow: 1;
330
- // DEBUG: background: yellow;
331
370
  }
332
371
 
333
- .dito-sidebar--width-60 & {
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
- button.dito-button(
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
- button.dito-button(
14
- type="button"
15
- :disabled="disabled"
16
- v-bind=`{
17
- ...pulldownTriggerAttributes,
18
- ...getButtonAttributes(verb, null, text)
19
- }`
20
- @mousedown.stop="onPulldownMouseDown()"
21
- ) {{ text }}
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 { getFormSchemas } from '../utils/schema/lookup.js'
28
+ import { getCreatableForms } from '../utils/schema/data.js'
46
29
 
47
30
  // @vue/component
48
31
  export default DitoComponent.component('DitoCreateButton', {
49
- mixins: [ContextMixin, PulldownMixin],
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
- forms() {
76
- // When inserting per-item, `this.schema` is the item's form, not the
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
- creatableForms() {
87
- const entries = Object.entries(this.forms).filter(
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 forms && Object.keys(forms).length === 1 && forms.default || null
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
- onPulldownSelect(type) {
148
- this.createItem(this.forms[type], type)
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="schema.label || 'Dialog'"
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
- onDeactivate: this.cancel
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="{ ...options, onStart, onEnd }"
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.mouseEvents?.remove()
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.mouseEvents = this.domOn(document, {
67
- mousedown: this.onMouse,
68
- mousemove: this.onMouse,
69
- mouseleave: this.onMouse
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
- onMouse() {
96
+ onMouseAfterDragEnd() {
74
97
  this.isDragging = false
75
- this.mouseEvents?.remove()
76
- this.mouseEvents = null
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.dito-buttons--round(
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
- //- Firefox doesn't like <button> here, so use <a> instead:
13
- a.dito-button(
14
+ DitoDragHandle(
14
15
  v-if="draggable"
15
- :class="{ 'dito-button--disabled': isDraggableDisabled }"
16
- v-bind="getButtonAttributes(verbs.drag)"
16
+ :disabled="isDraggableDisabled"
17
+ @move="delta => $emit('move', delta)"
17
18
  )
18
- RouterLink.dito-button(
19
+ DitoButton(
19
20
  v-if="editable"
20
- :class="{ 'dito-button--disabled': isEditableDisabled }"
21
- :to="editPath ? { path: editPath } : {}"
22
- v-bind="getButtonAttributes(verbs.edit)"
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
- button.dito-button(
40
+ DitoButton(
39
41
  v-if="deletable"
40
- type="button"
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
- emits: ['delete'],
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.hasSchemaOption('draggable')
101
+ return this.disabled || !this.hasFormOption('draggable')
82
102
  },
83
103
 
84
104
  isDeletableDisabled() {
85
- return this.disabled || !this.hasSchemaOption('deletable')
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.hasSchemaOption('editable')
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.hasSchemaOption('creatable')
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
- hasSchemaOption(name) {
139
+ hasFormOption(name) {
120
140
  // All options can be disabled on a per-form basis by setting
121
- // `schema[name]` to `false` or a callback returning `false`.
122
- return this.getSchemaValue(name, {
123
- type: Boolean,
124
- default: true
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
  })