@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.
Files changed (112) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +6011 -5826
  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 +24 -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/admin",
3
- "version": "3.2.7",
3
+ "version": "3.3.0",
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",
@@ -24,7 +24,9 @@
24
24
  "dist/",
25
25
  "types/",
26
26
  "!src/**/*.test.js",
27
- "!src/**/__snapshots__/"
27
+ "!src/**/__snapshots__/",
28
+ "!src/test/",
29
+ "!src/**/node_modules/"
28
30
  ],
29
31
  "scripts": {
30
32
  "build": "vite build",
@@ -41,10 +43,12 @@
41
43
  "not ie_mob > 0"
42
44
  ],
43
45
  "dependencies": {
44
- "@ditojs/ui": "^3.2.6",
45
- "@ditojs/utils": "^3.2.6"
46
+ "@ditojs/ui": "^3.3.0",
47
+ "@ditojs/utils": "^3.3.0",
48
+ "type-fest": "^5.10.0"
46
49
  },
47
50
  "peerDependencies": {
51
+ "@floating-ui/dom": "^1.8.0",
48
52
  "@kyvg/vue3-notification": "^3.4.2",
49
53
  "@tiptap/core": "^3.31.4",
50
54
  "@tiptap/extension-blockquote": "^3.31.4",
@@ -67,27 +71,26 @@
67
71
  "@tiptap/extension-subscript": "^3.31.4",
68
72
  "@tiptap/extension-superscript": "^3.31.4",
69
73
  "@tiptap/extension-text": "^3.31.4",
70
- "@tiptap/extension-text-style": "^3.31.4",
71
74
  "@tiptap/extension-underline": "^3.31.4",
72
75
  "@tiptap/extensions": "^3.31.4",
73
76
  "@tiptap/pm": "^3.31.4",
74
77
  "@tiptap/vue-3": "^3.31.4",
75
78
  "@vueuse/integrations": "^15.0.0",
76
79
  "codeflask": "^1.4.1",
77
- "focus-trap": "^8.2.2",
78
- "nanoid": "^6.0.1",
80
+ "focus-trap": "^8.2.3",
81
+ "nanoid": "^6.0.2",
79
82
  "sortablejs": "^1.15.7",
80
83
  "tippy.js": "^6.3.7",
81
84
  "tiptap-footnotes": "^3.0.1",
82
- "type-fest": "^5.10.0",
83
85
  "vue": "^3.5.43",
84
86
  "vue-color": "^3.3.3",
85
87
  "vue-multiselect": "^3.5.0",
86
- "vue-router": "^5.3.1",
88
+ "vue-router": "^5.4.0",
87
89
  "vue-upload-component": "^3.2.2"
88
90
  },
89
91
  "devDependencies": {
90
- "@ditojs/build": "^3.2.6",
92
+ "@ditojs/build": "^3.3.0",
93
+ "@floating-ui/dom": "^1.8.0",
91
94
  "@kyvg/vue3-notification": "^3.4.2",
92
95
  "@tiptap/core": "^3.31.4",
93
96
  "@tiptap/extension-blockquote": "^3.31.4",
@@ -110,26 +113,24 @@
110
113
  "@tiptap/extension-subscript": "^3.31.4",
111
114
  "@tiptap/extension-superscript": "^3.31.4",
112
115
  "@tiptap/extension-text": "^3.31.4",
113
- "@tiptap/extension-text-style": "^3.31.4",
114
116
  "@tiptap/extension-underline": "^3.31.4",
115
117
  "@tiptap/extensions": "^3.31.4",
116
118
  "@tiptap/pm": "^3.31.4",
117
119
  "@tiptap/vue-3": "^3.31.4",
118
120
  "@vueuse/integrations": "^15.0.0",
119
121
  "codeflask": "^1.4.1",
120
- "focus-trap": "^8.2.2",
121
- "nanoid": "^6.0.1",
122
+ "focus-trap": "^8.2.3",
123
+ "nanoid": "^6.0.2",
122
124
  "sortablejs": "^1.15.7",
123
125
  "tippy.js": "^6.3.7",
124
126
  "tiptap-footnotes": "^3.0.1",
125
- "type-fest": "^5.10.0",
126
127
  "typescript": "^6.0.3",
127
- "vite": "^8.3.2",
128
+ "vite": "^8.3.4",
128
129
  "vue": "^3.5.43",
129
130
  "vue-color": "^3.3.3",
130
131
  "vue-multiselect": "^3.5.0",
131
- "vue-router": "^5.3.1",
132
+ "vue-router": "^5.4.0",
132
133
  "vue-upload-component": "^3.2.2"
133
134
  },
134
- "gitHead": "88a3df9bbd036594e886ea6169cef3dfd4b10fe7"
135
+ "gitHead": "1ba6ba0a6642294959966a3488b0944c7c321b39"
135
136
  }
package/src/DitoAdmin.js CHANGED
@@ -11,16 +11,20 @@ import {
11
11
  import * as components from './components/index.js'
12
12
  import * as types from './types/index.js'
13
13
  import DitoRoot from './components/DitoRoot.vue'
14
+ import DitoView from './components/DitoView.vue'
14
15
  import DitoTypeComponent from './DitoTypeComponent.js'
15
16
  import ResizeDirective from './directives/resize.js'
16
17
  import { getResource } from './utils/resource.js'
17
18
  import { request, getApiUrl, isApiUrl } from './utils/request.js'
19
+ import { Session } from './utils/Session.js'
20
+ import { ViewRegistry } from './utils/ViewRegistry.js'
18
21
  import verbs from './verbs.js'
19
22
 
20
23
  export default class DitoAdmin {
21
24
  constructor(el, {
22
- // `dito` contains the base and api settings passed from `AdminController`
23
- dito = {},
25
+ // `dito` contains the base, api and settings passed from `AdminController`
26
+ // as `window.dito`, which is also the default:
27
+ dito: ditoObject = globalThis.dito ?? {},
24
28
  api,
25
29
  views = {},
26
30
  ...options
@@ -29,7 +33,7 @@ export default class DitoAdmin {
29
33
  // Merge in `api` settings as passed from `config.admin` and through the
30
34
  // `AdminController` with `api` values from from 'admin/index.js'
31
35
  // NOTE: `AdminController` provides `dito.api.base`
32
- this.api = api = assignDeeply({ base: '/' }, dito.api, api)
36
+ this.api = api = assignDeeply({ base: '/' }, ditoObject.api, api)
33
37
  this.options = options
34
38
 
35
39
  // Setup default api settings:
@@ -146,6 +150,35 @@ export default class DitoAdmin {
146
150
  el = document.querySelector(el)
147
151
  }
148
152
 
153
+ const router = createRouter({
154
+ // Start with a catch-all route, to be replaced by the actual routes once
155
+ // the schemas are loaded, to prevent vue-router from complaining, see
156
+ // `ViewRegistry.resolve()` for the actual route setup.
157
+ routes: [
158
+ {
159
+ name: 'catch-all',
160
+ path: '/:_(.*)',
161
+ components: {}
162
+ }
163
+ ],
164
+ history: createWebHistory(ditoObject.base),
165
+ linkActiveClass: '',
166
+ linkExactActiveClass: ''
167
+ })
168
+
169
+ const viewRegistry = new ViewRegistry({
170
+ api,
171
+ router,
172
+ unresolvedViews: views,
173
+ viewComponent: DitoView
174
+ })
175
+
176
+ const session = new Session({
177
+ api,
178
+ viewRegistry,
179
+ redirectAfterLogin: options.login?.redirectAfterLogin
180
+ })
181
+
149
182
  const app = (this.app = createApp({
150
183
  components: {
151
184
  DitoRoot,
@@ -163,12 +196,15 @@ export default class DitoAdmin {
163
196
  // https://github.com/vuejs/vue/issues/7017#issuecomment-480906691
164
197
  provide: {
165
198
  api,
199
+ session,
200
+ viewRegistry,
166
201
  // A default list of verbs are provided by $verbs() and can be
167
202
  // overridden at any point in the component hierarchy.
168
203
  $verbs: () => verbs,
204
+ // The views, once resolved for the logged in user, see `Session`:
205
+ $views: () => viewRegistry.views,
169
206
  // Provide defaults so DitoMixin can inject them for all components:
170
207
  // inject: [ '$isPopulated', '$schemaComponent', '$routeComponent' ]
171
- $views: () => {},
172
208
  $isPopulated: () => true,
173
209
  $parentComponent: () => null,
174
210
  $schemaComponent: () => null,
@@ -180,14 +216,14 @@ export default class DitoAdmin {
180
216
  $dialogComponent: () => null,
181
217
  $dataModel: () => null,
182
218
  $panelComponent: () => null,
183
- $tabComponent: () => null
219
+ $tabComponent: () => null,
220
+ $loadingTracker: () => null
184
221
  },
185
222
 
186
223
  render: () =>
187
224
  createElement(DitoRoot, {
188
225
  ref: 'root',
189
- class: dito.settings.rootClass,
190
- unresolvedViews: views,
226
+ class: ditoObject.settings?.rootClass,
191
227
  options
192
228
  })
193
229
  }))
@@ -203,23 +239,7 @@ export default class DitoAdmin {
203
239
 
204
240
  app.directive('resize', ResizeDirective)
205
241
 
206
- app.use(
207
- createRouter({
208
- // Start with a catch-all route, to be replaced by the actual routes
209
- // once the schemas are loaded, to prevent vue-router from complaining,
210
- // see: `resolveViews()` in `DitoRoot` for the actual route setup.
211
- routes: [
212
- {
213
- name: 'catch-all',
214
- path: '/:_(.*)',
215
- components: {}
216
- }
217
- ],
218
- history: createWebHistory(dito.base),
219
- linkActiveClass: '',
220
- linkExactActiveClass: ''
221
- })
222
- )
242
+ app.use(router)
223
243
 
224
244
  el.classList.add('dito-app')
225
245
  app.mount(el)
@@ -10,6 +10,7 @@ import {
10
10
  getLastDataPathIndex
11
11
  } from './utils/data.js'
12
12
  import { isNested } from './utils/schema/structure.js'
13
+ import { getTextFromHtml } from './utils/html.js'
13
14
 
14
15
  const { hasOwnProperty } = Object.prototype
15
16
 
@@ -54,7 +55,15 @@ function get(context, key, defaultValue) {
54
55
  }
55
56
 
56
57
  function set(context, key, value) {
57
- toObject(context)[key] = value
58
+ // Define the value on the context object itself: An assignment would be
59
+ // passed on to the component that the object inherits from, see
60
+ // `DitoContext` constructor, and change its state for all its contexts.
61
+ Object.defineProperty(toObject(context), key, {
62
+ value,
63
+ writable: true,
64
+ enumerable: true,
65
+ configurable: true
66
+ })
58
67
  }
59
68
 
60
69
  export default class DitoContext {
@@ -245,10 +254,18 @@ export default class DitoContext {
245
254
  return get(this, 'flattenedViews', null)
246
255
  }
247
256
 
257
+ // The label of the item as HTML, with escaped item values, see
258
+ // `getItemLabel()`.
248
259
  get itemLabel() {
249
260
  return get(this, 'itemLabel', null)
250
261
  }
251
262
 
263
+ // The text of `itemLabel`, with its escaped item values unescaped, e.g. for
264
+ // places that don't display HTML.
265
+ get itemLabelText() {
266
+ return getTextFromHtml(this.itemLabel)
267
+ }
268
+
252
269
  get formLabel() {
253
270
  return get(this, 'formLabel', null)
254
271
  }
@@ -326,12 +343,21 @@ export default class DitoContext {
326
343
  return get(this, 'wasNotified', false)
327
344
  }
328
345
 
346
+ // Whether the action of the component is running, e.g. the click handler of
347
+ // a button, which disables it. Handlers can set it to `false` to end it
348
+ // early, e.g. before opening a dialog. This changes the state of the
349
+ // component, which resets it when the action ends.
329
350
  get isRunning() {
330
351
  return get(this, 'isRunning', false)
331
352
  }
332
353
 
333
354
  set isRunning(value) {
334
- set(this, 'isRunning', value)
355
+ const { component } = this
356
+ if (component && 'isRunning' in component) {
357
+ component.isRunning = value
358
+ } else {
359
+ set(this, 'isRunning', value)
360
+ }
335
361
  }
336
362
 
337
363
  get query() {
@@ -23,6 +23,7 @@ export default {
23
23
  defaultVisible: true,
24
24
  defaultMultiple: false,
25
25
  generateLabel: true,
26
+ rendersOwnLabel: null,
26
27
  excludeValue: false,
27
28
  ignoreMissingValue: null,
28
29
  treatNullAsMissing: null,
package/src/appState.js CHANGED
@@ -3,10 +3,13 @@ import { parseUserAgent } from './utils/agent'
3
3
 
4
4
  export default reactive({
5
5
  title: '',
6
+ // The route components by route level, see `RouteMixin`:
6
7
  routeComponents: [],
7
8
  user: null,
8
9
  agent: parseUserAgent(navigator.userAgent || ''),
9
- loadCache: {}, // See TypeMixin.load()
10
- activeLabel: null,
11
- clipboardData: null
10
+ // The responses loaded with `cache: 'global'`, see `DitoMixin.load()`. Only
11
+ // `Session` replaces it, when the user changes:
12
+ loadCache: {},
13
+ // The modifier class of the page, e.g. for wide schemas, see `DitoSchema`:
14
+ pageClass: null
12
15
  })
@@ -1,47 +1,36 @@
1
1
  <template lang="pug">
2
2
  .dito-account
3
- a(
4
- role="button"
5
- v-bind="pulldownTriggerAttributes"
6
- @mousedown.stop="onPulldownMouseDown()"
3
+ DitoMenuButton.dito-account__button(
4
+ :items="items"
5
+ placement="bottom-left"
6
+ offset
7
+ @select="onSelectMenuItem"
7
8
  )
8
9
  span {{ user.username }}
9
- ul.dito-pulldown(
10
- role="menu"
11
- :class="{ 'dito-pulldown--open': pulldown.open }"
12
- )
13
- li(
14
- v-for="(label, value) of items"
15
- )
16
- a.dito-pulldown__item(
17
- role="menuitem"
18
- @mousedown.stop="onPulldownMouseDown(value)"
19
- @mouseup="onPulldownMouseUp(value)"
20
- ) {{ label }}
21
10
  </template>
22
11
 
23
12
  <script>
13
+ import { DitoMenuButton } from '@ditojs/ui/src'
24
14
  import DitoComponent from '../DitoComponent.js'
25
- import PulldownMixin from '../mixins/PulldownMixin.js'
26
15
 
27
16
  // @vue/component
28
17
  export default DitoComponent.component('DitoAccount', {
29
- mixins: [PulldownMixin],
18
+ components: { DitoMenuButton },
30
19
 
31
20
  data() {
32
21
  return {
33
- items: {
34
- settings: 'Settings',
35
- logout: 'Logout'
36
- }
22
+ items: [
23
+ { value: 'settings', label: 'Settings' },
24
+ { value: 'logout', label: 'Logout' }
25
+ ]
37
26
  }
38
27
  },
39
28
 
40
29
  methods: {
41
- onPulldownSelect(value) {
30
+ onSelectMenuItem({ value }) {
42
31
  switch (value) {
43
32
  case 'logout':
44
- this.rootComponent.logout()
33
+ this.session.logout()
45
34
  break
46
35
  case 'settings':
47
36
  console.info('TODO: Implement Settings')
@@ -56,11 +45,30 @@ export default DitoComponent.component('DitoAccount', {
56
45
  @import '../styles/_imports';
57
46
 
58
47
  .dito-account {
59
- position: relative;
60
48
  display: inline-block;
61
49
 
62
- .dito-pulldown {
63
- top: $pulldown-padding-ver;
50
+ // The menu button is a `DitoButton`, but shows the user name as plain text,
51
+ // so reset the button styles, including the ones of its states:
52
+ & &__button {
53
+ cursor: pointer;
54
+
55
+ &,
56
+ &:hover,
57
+ &:active,
58
+ &:focus {
59
+ min-width: 0;
60
+ min-height: 0;
61
+ padding: 0;
62
+ border: 0;
63
+ font: inherit;
64
+ color: inherit;
65
+ background: none;
66
+ box-shadow: none;
67
+ }
68
+
69
+ &:focus-visible {
70
+ box-shadow: $shadow-focus;
71
+ }
64
72
  }
65
73
  }
66
74
  </style>
@@ -13,7 +13,7 @@
13
13
  :parentContext="parentContext"
14
14
  )
15
15
  button.dito-affixes__clear(
16
- v-if="clearable"
16
+ v-if="shouldShowClearButton"
17
17
  type="button"
18
18
  title="Clear"
19
19
  :disabled="disabled"
@@ -43,7 +43,10 @@ export default DitoComponent.component('DitoAffixes', {
43
43
  mode: { type: String, default: null },
44
44
  position: { type: String, default: null },
45
45
  absolute: { type: Boolean, default: false },
46
+ // Whether the value can be cleared, and whether there's one to clear: The
47
+ // clear button only shows when both are true.
46
48
  clearable: { type: Boolean, default: false },
49
+ hasValue: { type: Boolean, default: false },
47
50
  disabled: { type: Boolean, default: false },
48
51
  inlineInfo: { type: String, default: null },
49
52
  parentContext: { type: Object, required: true }
@@ -86,10 +89,14 @@ export default DitoComponent.component('DitoAffixes', {
86
89
  }
87
90
  },
88
91
 
92
+ shouldShowClearButton() {
93
+ return this.clearable && this.hasValue
94
+ },
95
+
89
96
  hasContent() {
90
97
  return (
91
98
  this.visibleItems.length > 0 ||
92
- this.clearable ||
99
+ this.shouldShowClearButton ||
93
100
  this.inlineInfo ||
94
101
  hasSlotContent(this.$slots.prepend) ||
95
102
  hasSlotContent(this.$slots.append)
@@ -2,41 +2,45 @@
2
2
  .dito-clipboard.dito-buttons.dito-buttons--round(
3
3
  v-if="clipboard"
4
4
  )
5
- button.dito-button.dito-button--copy(
6
- ref="copyData"
7
- type="button"
5
+ DitoButton.dito-button--copy(
8
6
  title="Copy Data"
9
- :disabled="!copyEnabled"
7
+ :disabled="!hasData"
10
8
  @click="onCopy"
11
9
  )
12
- button.dito-button.dito-button--paste(
13
- type="button"
10
+ DitoButton.dito-button--paste(
14
11
  title="Paste Data"
15
- :disabled="!pasteEnabled"
12
+ :disabled="!isPasteEnabled"
16
13
  @click="onPaste"
17
14
  )
18
15
  </template>
19
16
 
20
17
  <script>
21
- import { isObject, clone, deindent } from '@ditojs/utils'
18
+ import { isObject, clone } from '@ditojs/utils'
19
+ import { DitoButton } from '@ditojs/ui/src'
22
20
  import DitoComponent from '../DitoComponent.js'
23
21
  import DomMixin from '../mixins/DomMixin.js'
24
22
  import DitoContext from '../DitoContext.js'
23
+ import {
24
+ getClipboardData,
25
+ hasClipboardData,
26
+ copyClipboardData,
27
+ readClipboardData
28
+ } from '../utils/clipboard.js'
25
29
 
26
30
  // @vue/component
27
31
  export default DitoComponent.component('DitoClipboard', {
28
32
  mixins: [DomMixin],
33
+ components: { DitoButton },
34
+
35
+ emits: ['paste'],
29
36
 
30
37
  props: {
31
38
  clipboard: { type: [Boolean, Object], required: true },
32
- schema: { type: Object, required: true }
33
- },
34
-
35
- data() {
36
- return {
37
- copyEnabled: false,
38
- pasteEnabled: false
39
- }
39
+ schema: { type: Object, required: true },
40
+ hasData: { type: Boolean, required: true },
41
+ // Returns the data to copy. A function rather than the data itself, so
42
+ // that it's only processed when copying.
43
+ getData: { type: Function, required: true }
40
44
  },
41
45
 
42
46
  computed: {
@@ -58,106 +62,68 @@ export default DitoComponent.component('DitoClipboard', {
58
62
  ? clipboardData =>
59
63
  paste.call(this, new DitoContext(this, { clipboardData }))
60
64
  : clipboardData => clipboardData
61
- }
62
- },
63
-
64
- watch: {
65
- // Check right away also in case there's already data (e.g. create form):
66
- 'parentComponent.hasData': {
67
- immediate: true,
68
- handler: 'updateCopy'
69
65
  },
70
66
 
71
- 'appState.clipboardData': {
72
- immediate: true,
73
- handler: 'updatePaste'
67
+ isPasteEnabled() {
68
+ return hasClipboardData(this.schema.name)
74
69
  }
75
70
  },
76
71
 
77
72
  mounted() {
78
- // Check clipboard content whenever something gets copied or the window gets
79
- // (re)activated, as those are the moments when the clipboard can change:
80
- this.domOn(document, { copy: this.updatePaste })
81
- this.domOn(window, { focus: this.updatePaste })
73
+ // Read the clipboard right away, and whenever something gets copied or the
74
+ // window gets (re)activated, as those are the moments when it can change:
75
+ this.updateClipboardData()
76
+ this.domOn(document, { copy: this.updateClipboardData })
77
+ this.domOn(window, { focus: this.updateClipboardData })
82
78
  },
83
79
 
84
80
  methods: {
85
- checkClipboardData(clipboardData) {
86
- const { $schema, ...data } = clipboardData || {}
87
- return $schema === this.schema.name ? data : null
88
- },
89
-
90
- async getClipboardData(report) {
91
- // Use the internal clipboard as fallback.
92
- let { clipboardData } = this.appState
93
- try {
94
- const json = await navigator.clipboard?.readText?.()
95
- if (json) {
96
- clipboardData = JSON.parse(json)
97
- }
98
- } catch (err) {
99
- if (report) {
100
- console.error(err, err.name, err.message)
101
- if (err.name === 'SyntaxError') {
102
- alert(deindent`
103
- The data in the clipboard appears to be malformed:
104
- ${err.message}
105
- `)
106
- }
107
- }
108
- }
109
- return this.checkClipboardData(clipboardData)
110
- },
111
-
112
- updateCopy() {
113
- this.copyEnabled = this.parentComponent.hasData
114
- },
115
-
116
- async updatePaste() {
117
- this.pasteEnabled = !!this.checkClipboardData(this.appState.clipboardData)
118
- if (!this.pasteEnabled && this.appState.agent.chrome) {
119
- // See if the clipboard content is valid JSON data that is compatible
120
- // with the current target schema, and only then activate the pasting:
121
- const data = await this.getClipboardData(false) // Don't report
122
- this.pasteEnabled = !!data
81
+ updateClipboardData() {
82
+ // Only Chrome reads the clipboard without asking for permission, so
83
+ // other browsers only read it when pasting.
84
+ if (this.appState.agent.chrome) {
85
+ // Errors are only reported when pasting.
86
+ readClipboardData().catch(() => {})
123
87
  }
124
88
  },
125
89
 
126
90
  async onCopy() {
127
- let data = this.parentComponent.clipboardData
128
91
  try {
129
- if (data) {
130
- data = {
131
- $schema: this.schema.name,
132
- ...this.copyData(data)
133
- }
134
- }
135
- // Keep an internal clipboard as fallback.
136
- this.appState.clipboardData = data
137
- this.pasteEnabled = true
138
- try {
139
- const json = JSON.stringify(data, null, 2)
140
- await navigator.clipboard?.writeText?.(json)
141
- } catch (err) {
142
- console.error(err, err.name, err.message)
143
- }
92
+ const data = this.getData()
93
+ await copyClipboardData(this.schema.name, data && this.copyData(data))
144
94
  } catch (error) {
145
- console.error(error)
146
- alert(error.message)
95
+ this.notifyClipboardError(error)
147
96
  }
148
97
  },
149
98
 
150
99
  async onPaste() {
151
- let data = await this.getClipboardData(true) // Report
152
100
  try {
153
- data = data && this.pasteData(data)
154
- if (data) {
155
- this.parentComponent.clipboardData = data
101
+ await readClipboardData()
102
+ } catch (error) {
103
+ if (error.name === 'SyntaxError') {
104
+ this.notifyClipboardError(error, [
105
+ 'The data in the clipboard appears to be malformed:',
106
+ error.message
107
+ ])
108
+ return
109
+ }
110
+ // Fall back to the data copied inside the admin.
111
+ console.error(error, error.name, error.message)
112
+ }
113
+ try {
114
+ const data = getClipboardData(this.schema.name)
115
+ const pastedData = data && this.pasteData(data)
116
+ if (pastedData) {
117
+ this.$emit('paste', pastedData)
156
118
  }
157
119
  } catch (error) {
158
- console.error(error)
159
- alert(error.message)
120
+ this.notifyClipboardError(error)
160
121
  }
122
+ },
123
+
124
+ notifyClipboardError(error, text = error.message) {
125
+ // Notifications log their error too.
126
+ this.notify({ type: 'error', error, title: 'Clipboard Error', text })
161
127
  }
162
128
  }
163
129
  })
@@ -0,0 +1,18 @@
1
+ <template lang="pug">
2
+ p.dito-confirm-message(
3
+ v-html="value"
4
+ )
5
+ </template>
6
+
7
+ <script>
8
+ // Displays the HTML message of the dialogs that `confirm()` shows, see
9
+ // `utils/dialogs.js`, as a `{ type: 'component' }` schema, which adds
10
+ // `DitoMixin` and `TypeMixin` to provide `value`.
11
+ export default {}
12
+ </script>
13
+
14
+ <style lang="scss">
15
+ .dito-confirm-message {
16
+ margin: 0;
17
+ }
18
+ </style>