@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,591 @@
1
+ import {
2
+ isObject,
3
+ isString,
4
+ isFunction,
5
+ equals,
6
+ labelize,
7
+ hyphenate,
8
+ format
9
+ } from '@ditojs/utils'
10
+ import appState from '../appState.js'
11
+ import DitoContext from '../DitoContext.js'
12
+ import EmitterMixin from './EmitterMixin.js'
13
+ import {
14
+ flattenViews,
15
+ getSchemaValue,
16
+ shouldRenderSchema
17
+ } from '../utils/schema.js'
18
+ import { getResource, getMemberResource } from '../utils/resource.js'
19
+ import { computed, reactive } from 'vue'
20
+
21
+ // @vue/component
22
+ export default {
23
+ mixins: [EmitterMixin],
24
+
25
+ inject: [
26
+ 'api',
27
+ '$verbs',
28
+ '$views',
29
+ '$isPopulated',
30
+ '$parentComponent',
31
+ '$schemaComponent',
32
+ '$routeComponent',
33
+ '$dataComponent',
34
+ '$sourceComponent',
35
+ '$resourceComponent',
36
+ '$dialogComponent',
37
+ '$panelComponent',
38
+ '$tabComponent'
39
+ ],
40
+
41
+ provide() {
42
+ const self = () => this
43
+ return this.providesData
44
+ ? {
45
+ $parentComponent: self,
46
+ $dataComponent: self
47
+ }
48
+ : {
49
+ $parentComponent: self
50
+ }
51
+ },
52
+
53
+ data() {
54
+ return {
55
+ appState,
56
+ isMounted: false,
57
+ overrides: null // See accessor.js
58
+ }
59
+ },
60
+
61
+ computed: {
62
+ providesData() {
63
+ // NOTE: This is overridden in ResourceMixin, used by lists.
64
+ return false
65
+ },
66
+
67
+ sourceSchema() {
68
+ return this.meta?.schema
69
+ },
70
+
71
+ user() {
72
+ return appState.user
73
+ },
74
+
75
+ // $verbs, $verbs and $isPopulated are defined as functions, to preserve
76
+ // reactiveness across provide/inject.
77
+ // See: https://github.com/vuejs/vue/issues/7017#issuecomment-480906691
78
+ verbs() {
79
+ return this.$verbs()
80
+ },
81
+
82
+ views() {
83
+ return this.$views()
84
+ },
85
+
86
+ flattenedViews() {
87
+ return flattenViews(this.views)
88
+ },
89
+
90
+ isPopulated() {
91
+ return this.$isPopulated()
92
+ },
93
+
94
+ locale() {
95
+ return this.api.locale
96
+ },
97
+
98
+ context() {
99
+ return new DitoContext(this, { nested: false })
100
+ },
101
+
102
+ rootComponent() {
103
+ return this.$root.$refs.root
104
+ },
105
+
106
+ // Use computed properties as links to injects, so DitoSchema can
107
+ // override the property and return `this` instead of the parent.
108
+ parentComponent() {
109
+ return this.$parentComponent()
110
+ },
111
+
112
+ schemaComponent() {
113
+ return this.$schemaComponent()
114
+ },
115
+
116
+ routeComponent() {
117
+ return this.$routeComponent()
118
+ },
119
+
120
+ formComponent() {
121
+ const component = this.routeComponent
122
+ return component?.isForm ? component : null
123
+ },
124
+
125
+ viewComponent() {
126
+ const component = this.routeComponent
127
+ return component?.isView ? component : null
128
+ },
129
+
130
+ // Returns the first route component in the chain of parents, including
131
+ // this current component, that is linked to a resource (and thus loads its
132
+ // own data and doesn't hold nested data).
133
+ dataComponent() {
134
+ return this.providesData ? this : this.$dataComponent()
135
+ },
136
+
137
+ sourceComponent() {
138
+ return this.$sourceComponent()
139
+ },
140
+
141
+ resourceComponent() {
142
+ return this.$resourceComponent()
143
+ },
144
+
145
+ dialogComponent() {
146
+ return this.$dialogComponent()
147
+ },
148
+
149
+ panelComponent() {
150
+ return this.$panelComponent()
151
+ },
152
+
153
+ tabComponent() {
154
+ return this.$tabComponent()
155
+ },
156
+
157
+ parentSchemaComponent() {
158
+ return getParentComponent(this, 'schemaComponent')
159
+ },
160
+
161
+ parentRouteComponent() {
162
+ return getParentComponent(this, 'routeComponent')
163
+ },
164
+
165
+ parentFormComponent() {
166
+ return getParentComponent(this, 'formComponent')
167
+ },
168
+
169
+ parentResourceComponent() {
170
+ return getParentComponent(this, 'resourceComponent')
171
+ },
172
+
173
+ // Returns the data of the first route component in the chain of parents
174
+ // that loads its own data from an associated API resource.
175
+ rootData() {
176
+ return this.dataComponent?.data
177
+ }
178
+ },
179
+
180
+ mounted() {
181
+ this.isMounted = true
182
+ },
183
+
184
+ beforeCreate() {
185
+ const uid = nextUid++
186
+ Object.defineProperty(this, '$uid', { get: () => uid })
187
+ },
188
+
189
+ methods: {
190
+ labelize,
191
+
192
+ // The state of components is only available during the life-cycle of a
193
+ // component. Some information we need available longer than that, e.g.
194
+ // `query` & `total` on TypeList, so that when the user navigates back from
195
+ // editing an item in the list, the state of the list is still the same.
196
+ // We can't store this in `data`, as this is already the pure data from the
197
+ // API server. That's what the `store` is for: Memory that's available as
198
+ // long as the current editing path is still valid. For type components,
199
+ // this memory is provided by the parent, see RouteMixin and DitoPane.
200
+ getStore(key) {
201
+ return this.store[key]
202
+ },
203
+
204
+ setStore(key, value) {
205
+ this.store[key] = value
206
+ return value
207
+ },
208
+
209
+ removeStore(key) {
210
+ delete this.store[key]
211
+ },
212
+
213
+ getStoreKeyByIndex(index) {
214
+ return this.store.$keysByIndex?.[index]
215
+ },
216
+
217
+ setStoreKeyByIndex(index, key) {
218
+ this.store.$keysByIndex ??= {}
219
+ this.store.$keysByIndex[index] = key
220
+ },
221
+
222
+ getChildStore(key, index) {
223
+ let store = this.getStore(key)
224
+ if (!store && index != null) {
225
+ // When storing, temporary ids change to permanent ones and thus the key
226
+ // can change. To still find the store, we reference by index as well,
227
+ // to be able to find the store again after the item was saved.
228
+ const oldKey = this.getStoreKeyByIndex(index)
229
+ store = this.getStore(oldKey)
230
+ if (store) {
231
+ this.setStore(key, store)
232
+ this.removeStore(oldKey)
233
+ }
234
+ }
235
+ if (!store) {
236
+ store = this.setStore(key, reactive({}))
237
+ }
238
+ if (index != null) {
239
+ // temporary uid keys will change between persistence, so we need to
240
+ // assign the key to the index even when the store already existed.
241
+ this.setStoreKeyByIndex(index, key)
242
+ }
243
+ return store
244
+ },
245
+
246
+ removeChildStore(key, index) {
247
+ // GEt the child-store first, so that indices can be transferred over
248
+ // temporary id changes during persistence.
249
+ this.getChildStore(key, index)
250
+ this.removeStore(key)
251
+ },
252
+
253
+ getSchemaValue(
254
+ keyOrDataPath,
255
+ {
256
+ type,
257
+ default: def,
258
+ schema = this.schema,
259
+ context = this.context,
260
+ callback = true
261
+ } = {}
262
+ ) {
263
+ return getSchemaValue(keyOrDataPath, {
264
+ type,
265
+ schema,
266
+ context,
267
+ callback,
268
+ default: isFunction(def) ? () => def.call(this) : def
269
+ })
270
+ },
271
+
272
+ getLabel(schema, name) {
273
+ return schema
274
+ ? this.getSchemaValue('label', { schema, type: [String, Object] }) ||
275
+ labelize(name || schema.name)
276
+ : labelize(name) || ''
277
+ },
278
+
279
+ getButtonAttributes(verb) {
280
+ return {
281
+ class: `dito-button--${verb}`,
282
+ title: labelize(verb)
283
+ }
284
+ },
285
+
286
+ // TODO: Rename *Link() to *Route().
287
+ getQueryLink(query) {
288
+ return {
289
+ query,
290
+ // Preserve hash for tabs:
291
+ hash: this.$route.hash
292
+ }
293
+ },
294
+
295
+ shouldRenderSchema(schema = null) {
296
+ return shouldRenderSchema(schema, this.context)
297
+ },
298
+
299
+ shouldShowSchema(schema = null) {
300
+ return this.getSchemaValue('visible', {
301
+ type: Boolean,
302
+ default: true,
303
+ schema
304
+ })
305
+ },
306
+
307
+ shouldDisableSchema(schema = null) {
308
+ return this.getSchemaValue('disabled', {
309
+ type: Boolean,
310
+ default: false,
311
+ schema
312
+ })
313
+ },
314
+
315
+ getResourcePath(resource) {
316
+ resource = getResource(resource, {
317
+ // Resources without a parent inherit the one from `dataComponent`
318
+ // automatically.
319
+ parent: this.dataComponent?.getResource({
320
+ method: resource?.method,
321
+ child: resource
322
+ }) ?? null
323
+ })
324
+ return this.api.resources.any(resource)
325
+ },
326
+
327
+ getResourceUrl(resource) {
328
+ const url = this.getResourcePath(resource)
329
+ return url ? this.api.getApiUrl({ url, query: resource.query }) : null
330
+ },
331
+
332
+ async sendRequest({
333
+ method,
334
+ url,
335
+ resource,
336
+ query,
337
+ data,
338
+ signal,
339
+ internal
340
+ }) {
341
+ url ||= this.getResourceUrl(resource)
342
+ method ||= resource?.method
343
+ const checkUser = !internal && this.api.isApiUrl(url)
344
+ if (checkUser) {
345
+ await this.rootComponent.ensureUser()
346
+ }
347
+ const response = await this.api.request({
348
+ method,
349
+ url,
350
+ query,
351
+ data,
352
+ signal
353
+ })
354
+ // Detect change of the own user, and fetch it again if it was changed.
355
+ if (
356
+ checkUser &&
357
+ method === 'patch' &&
358
+ equals(resource, getMemberResource(this.user.id, this.api.users))
359
+ ) {
360
+ await this.rootComponent.fetchUser()
361
+ }
362
+ return response
363
+ },
364
+
365
+ showDialog({ components, buttons, data, settings }) {
366
+ return this.rootComponent.showDialog({
367
+ components,
368
+ buttons,
369
+ data,
370
+ settings
371
+ })
372
+ },
373
+
374
+ request({ cache, ...options }) {
375
+ // Allow caching of loaded data on two levels:
376
+ // - 'global': cache globally, for the entire admin session
377
+ // - 'local': cache locally within the closest route component that is
378
+ // associated with a resource and loads its own data.
379
+ const cacheParent = (
380
+ cache &&
381
+ {
382
+ global: this.appState,
383
+ local: this.dataComponent
384
+ }[cache]
385
+ )
386
+ const loadCache = cacheParent?.loadCache
387
+ // Build a cache key from the config:
388
+ const cacheKey = (
389
+ loadCache &&
390
+ `${
391
+ options.method || 'get'
392
+ } ${
393
+ options.url
394
+ } ${
395
+ JSON.stringify(options.query || '')
396
+ } ${
397
+ JSON.stringify(options.data || '')
398
+ }`
399
+ )
400
+ if (loadCache && (cacheKey in loadCache)) {
401
+ return loadCache[cacheKey]
402
+ }
403
+ // NOTE: No await here, res is a promise that we can easily cache.
404
+ // That's fine because promises can be resolved over and over again.
405
+ const res = this.sendRequest(options)
406
+ .then(response => response.data)
407
+ .catch(error => {
408
+ // Convert axios errors to normal errors
409
+ const data = error.response?.data
410
+ throw data
411
+ ? Object.assign(new Error(data.message), data)
412
+ : error
413
+ })
414
+ if (loadCache) {
415
+ loadCache[cacheKey] = res
416
+ }
417
+ return res
418
+ },
419
+
420
+ format(value, {
421
+ locale = this.api.locale,
422
+ defaults = this.api.formats,
423
+ ...options
424
+ } = {}) {
425
+ return format(value, {
426
+ locale,
427
+ defaults,
428
+ ...options
429
+ })
430
+ },
431
+
432
+ async navigate(location) {
433
+ return this.$router.push(location)
434
+ },
435
+
436
+ download(options = {}) {
437
+ if (isString(options)) {
438
+ options = { url: options }
439
+ }
440
+ // See: https://stackoverflow.com/a/49917066/1163708
441
+ const a = document.createElement('a')
442
+ a.href = options.url?.startsWith('blob:')
443
+ ? options.url
444
+ : this.api.getApiUrl(options)
445
+ a.download = options.filename ?? null
446
+ const { body } = document
447
+ body.appendChild(a)
448
+ a.click()
449
+ body.removeChild(a)
450
+ },
451
+
452
+ notify(options) {
453
+ this.rootComponent.notify(options)
454
+ },
455
+
456
+ closeNotifications() {
457
+ this.rootComponent.closeNotifications()
458
+ },
459
+
460
+ setupSchemaFields() {
461
+ this.setupMethods()
462
+ this.setupComputed()
463
+ this.setupEvents()
464
+ },
465
+
466
+ setupMethods() {
467
+ for (const [key, value] of Object.entries(this.schema.methods || {})) {
468
+ if (isFunction(value)) {
469
+ this[key] = value
470
+ } else {
471
+ console.error(`Invalid method definition: ${key}: ${value}`)
472
+ }
473
+ }
474
+ },
475
+
476
+ setupComputed() {
477
+ const getComputedAccessor = ({ get, set }) => {
478
+ const getter = computed(() => get.call(this))
479
+ return {
480
+ get: () => getter.value,
481
+ set: set ? value => set.call(this, value) : undefined
482
+ }
483
+ }
484
+
485
+ for (const [key, item] of Object.entries(this.schema.computed || {})) {
486
+ const accessor = isFunction(item)
487
+ ? getComputedAccessor({ get: item })
488
+ : isObject(item) && isFunction(item.get)
489
+ ? getComputedAccessor(item)
490
+ : null
491
+ if (accessor) {
492
+ Object.defineProperty(this, key, accessor)
493
+ } else {
494
+ console.error(
495
+ `Invalid computed property definition: ${key}: ${item}`
496
+ )
497
+ }
498
+ }
499
+ },
500
+
501
+ setupEvents() {
502
+ const { watch, events } = this.schema
503
+ if (watch) {
504
+ const handlers = isFunction(watch) ? watch.call(this) : watch
505
+ if (isObject(handlers)) {
506
+ // Install the watch handlers in the next tick, so all components are
507
+ // initialized and we can check against their names.
508
+ this.$nextTick(() => {
509
+ for (const [key, callback] of Object.entries(handlers)) {
510
+ // Expand property names to 'data.property':
511
+ const expr = this.schemaComponent.getComponentByName(key)
512
+ ? `data.${key}`
513
+ : key
514
+ this.$watch(expr, callback)
515
+ }
516
+ })
517
+ }
518
+ }
519
+
520
+ const addEvent = (key, event, callback) => {
521
+ if (isFunction(callback)) {
522
+ this.on(hyphenate(event), callback)
523
+ } else {
524
+ console.error(`Invalid event definition: ${key}: ${callback}`)
525
+ }
526
+ }
527
+
528
+ if (events) {
529
+ for (const [key, value] of Object.entries(events)) {
530
+ addEvent(key, key, value)
531
+ }
532
+ }
533
+ // Also scan schema for `on[A-Z]`-style callbacks and add them
534
+ // TODO: Deprecate one format or the other, in favour of only one way of
535
+ // doing things. Decide which one to remove.
536
+ for (const [key, value] of Object.entries(this.schema)) {
537
+ if (/^on[A-Z]/.test(key)) {
538
+ addEvent(key, key.slice(2), value)
539
+ }
540
+ }
541
+ },
542
+
543
+ emitEvent(event, {
544
+ context = null,
545
+ parent = null
546
+ } = {}) {
547
+ const hasListeners = this.hasListeners(event)
548
+ const parentHasListeners = parent?.hasListeners(event)
549
+ if (hasListeners || parentHasListeners) {
550
+ const emitEvent = target =>
551
+ target.emit(event, (context = DitoContext.get(this, context)))
552
+
553
+ const handleParentListeners = result =>
554
+ // Don't bubble to parent if handled event returned `false`
555
+ parentHasListeners && result !== false
556
+ ? emitEvent(parent).then(() => result)
557
+ : result
558
+
559
+ const handleListeners = () =>
560
+ hasListeners
561
+ ? emitEvent(this).then(handleParentListeners)
562
+ : handleParentListeners(undefined)
563
+
564
+ return ['load', 'change'].includes(event)
565
+ ? // The effects of some events need time to propagate through Vue.
566
+ // Use $nextTick() to make sure our handlers see these changes.
567
+ // For example, `processedItem` is only correct after components
568
+ // that are newly rendered due to data changes have registered.
569
+ // NOTE: The result of `handleListeners()` makes it through the
570
+ // `$nextTick()` call and will be returned as expected.
571
+ this.$nextTick(handleListeners)
572
+ : handleListeners()
573
+ }
574
+ },
575
+
576
+ emitSchemaEvent(event, params) {
577
+ return this.schemaComponent.emitEvent(event, params)
578
+ }
579
+ }
580
+ }
581
+
582
+ let nextUid = 0
583
+
584
+ function getParentComponent(component, key) {
585
+ const current = component[key]
586
+ let parent = component.parentComponent
587
+ while (parent && parent[key] === current) {
588
+ parent = parent.parentComponent
589
+ }
590
+ return parent?.[key] ?? null
591
+ }
@@ -0,0 +1,29 @@
1
+ import { isObject } from '@ditojs/utils'
2
+ import { addEvents } from '@ditojs/ui/src'
3
+
4
+ // @vue/component
5
+ export default {
6
+ data() {
7
+ return {
8
+ domHandlers: []
9
+ }
10
+ },
11
+
12
+ unmounted() {
13
+ for (const { remove } of this.domHandlers) {
14
+ remove()
15
+ }
16
+ this.domHandlers = []
17
+ },
18
+
19
+ methods: {
20
+ domOn(element, type, handler) {
21
+ const result = addEvents(
22
+ element,
23
+ isObject(type) ? type : { [type]: handler }
24
+ )
25
+ this.domHandlers.push(result)
26
+ return result
27
+ }
28
+ }
29
+ }