@ditojs/admin 3.2.7 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +6011 -5826
- package/dist/dito-admin.umd.js +3 -7
- package/package.json +18 -17
- package/src/DitoAdmin.js +44 -24
- package/src/DitoContext.js +28 -2
- package/src/DitoTypeComponent.js +1 -0
- package/src/appState.js +6 -3
- package/src/components/DitoAccount.vue +35 -27
- package/src/components/DitoAffixes.vue +9 -2
- package/src/components/DitoClipboard.vue +59 -93
- package/src/components/DitoConfirmMessage.vue +18 -0
- package/src/components/DitoContainer.vue +56 -18
- package/src/components/DitoCreateButton.vue +36 -84
- package/src/components/DitoDialog.vue +20 -15
- package/src/components/DitoDragHandle.vue +44 -0
- package/src/components/DitoDraggable.vue +43 -16
- package/src/components/DitoEditButtons.vue +48 -24
- package/src/components/DitoErrors.vue +9 -56
- package/src/components/DitoForm.vue +82 -42
- package/src/components/DitoInputAffixes.vue +50 -0
- package/src/components/DitoLabel.vue +1 -15
- package/src/components/DitoMenu.vue +45 -52
- package/src/components/DitoNotifications.vue +38 -14
- package/src/components/DitoOptionList.vue +81 -0
- package/src/components/DitoOptionsEditButtons.vue +57 -0
- package/src/components/DitoPagination.vue +1 -8
- package/src/components/DitoPane.vue +51 -39
- package/src/components/DitoPanel.vue +12 -26
- package/src/components/DitoPanels.vue +11 -5
- package/src/components/DitoResizeHandle.vue +47 -0
- package/src/components/DitoRoot.vue +113 -320
- package/src/components/DitoSchema.vue +72 -63
- package/src/components/DitoSchemaInlined.vue +8 -9
- package/src/components/DitoScopes.vue +14 -18
- package/src/components/DitoSidebar.vue +4 -29
- package/src/components/DitoSourceItem.vue +114 -0
- package/src/components/DitoTableCell.vue +4 -0
- package/src/components/DitoTableHead.vue +15 -17
- package/src/components/DitoTabs.vue +25 -0
- package/src/components/DitoTrail.vue +25 -25
- package/src/components/DitoTreeItem.vue +102 -141
- package/src/components/DitoUploadFile.vue +5 -2
- package/src/components/DitoView.vue +33 -24
- package/src/index.js +4 -1
- package/src/mixins/DitoMixin.js +77 -110
- package/src/mixins/DomMixin.js +12 -14
- package/src/mixins/EmitterMixin.js +3 -2
- package/src/mixins/ItemMixin.js +38 -94
- package/src/mixins/OptionsMixin.js +38 -25
- package/src/mixins/ResourceMixin.js +182 -111
- package/src/mixins/RouteMixin.js +61 -44
- package/src/mixins/SchemaParentMixin.js +4 -1
- package/src/mixins/SourceMixin.js +118 -100
- package/src/mixins/TypeMixin.js +42 -40
- package/src/mixins/ValidationMixin.js +16 -4
- package/src/mixins/ValidatorMixin.js +1 -1
- package/src/styles/_settings.scss +4 -0
- package/src/styles/_tippy.scss +0 -11
- package/src/styles/style.scss +0 -1
- package/src/types/DitoTypeButton.vue +11 -14
- package/src/types/DitoTypeCheckboxes.vue +35 -17
- package/src/types/DitoTypeCode.vue +19 -41
- package/src/types/DitoTypeColor.vue +67 -19
- package/src/types/DitoTypeDate.vue +11 -14
- package/src/types/DitoTypeList.vue +50 -62
- package/src/types/DitoTypeMarkup.vue +41 -54
- package/src/types/DitoTypeMultiselect.vue +82 -52
- package/src/types/DitoTypeNumber.vue +12 -15
- package/src/types/DitoTypeObject.vue +4 -38
- package/src/types/DitoTypeRadio.vue +19 -6
- package/src/types/DitoTypeSection.vue +23 -9
- package/src/types/DitoTypeSelect.vue +39 -23
- package/src/types/DitoTypeSlider.vue +9 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +11 -14
- package/src/types/DitoTypeTreeList.vue +46 -8
- package/src/types/DitoTypeUpload.vue +190 -67
- package/src/utils/DataModel.js +368 -146
- package/src/utils/FileDropTargets.js +97 -0
- package/src/utils/ItemStores.js +31 -0
- package/src/utils/ListQuery.js +115 -0
- package/src/utils/LoadingTracker.js +76 -0
- package/src/utils/SchemaGraph.js +3 -2
- package/src/utils/Session.js +151 -0
- package/src/utils/ViewRegistry.js +85 -0
- package/src/utils/accessor.js +12 -4
- package/src/utils/affixes.js +33 -0
- package/src/utils/changeOnceEdited.js +33 -0
- package/src/utils/clipboard.js +55 -0
- package/src/utils/data.js +10 -5
- package/src/utils/dialogs.js +59 -0
- package/src/utils/dragResize.js +61 -0
- package/src/utils/filter.js +36 -2
- package/src/utils/html.js +9 -0
- package/src/utils/list.js +10 -0
- package/src/utils/options.js +4 -0
- package/src/utils/registration.js +18 -0
- package/src/utils/request.js +108 -13
- package/src/utils/route.js +6 -17
- package/src/utils/schema/data.js +24 -2
- package/src/utils/schema/events.js +19 -0
- package/src/utils/schema/item.js +114 -0
- package/src/utils/schema/lookup.js +39 -14
- package/src/utils/schema/setup.js +5 -5
- package/src/utils/schema/validation.js +16 -2
- package/src/utils/uid.js +105 -8
- package/src/validators/_decimals.js +16 -5
- package/types/index.d.ts +168 -41
- package/src/mixins/LoadingMixin.js +0 -23
- package/src/mixins/PulldownMixin.js +0 -72
- package/src/styles/_pulldown.scss +0 -38
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
|
+
//- The input of the container refers to the errors by `id` through
|
|
3
|
+
//- `aria-describedby`, see `TypeMixin.attributes`.
|
|
2
4
|
.dito-errors(
|
|
3
5
|
v-if="errors"
|
|
6
|
+
:id="id"
|
|
4
7
|
role="alert"
|
|
5
8
|
)
|
|
6
9
|
ul
|
|
@@ -10,62 +13,13 @@
|
|
|
10
13
|
</template>
|
|
11
14
|
|
|
12
15
|
<script>
|
|
13
|
-
import tippy from 'tippy.js'
|
|
14
16
|
import DitoComponent from '../DitoComponent.js'
|
|
15
|
-
import { markRaw } from 'vue'
|
|
16
17
|
|
|
17
18
|
// @vue/component
|
|
18
19
|
export default DitoComponent.component('DitoErrors', {
|
|
19
20
|
props: {
|
|
21
|
+
id: { type: String, default: null },
|
|
20
22
|
errors: { type: Array, default: null }
|
|
21
|
-
},
|
|
22
|
-
|
|
23
|
-
data() {
|
|
24
|
-
return {
|
|
25
|
-
tip: null
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
watch: {
|
|
30
|
-
errors() {
|
|
31
|
-
this.$nextTick(this.updateErrors)
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
unmounted() {
|
|
36
|
-
this.tip?.destroy()
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
methods: {
|
|
40
|
-
updateErrors() {
|
|
41
|
-
let { tip } = this
|
|
42
|
-
tip?.hide()
|
|
43
|
-
if (this.errors) {
|
|
44
|
-
tip = this.tip ??= markRaw(tippy(this.$el.closest('.dito-container')))
|
|
45
|
-
tip.setProps({
|
|
46
|
-
content: this.errors.join('\n'),
|
|
47
|
-
theme: 'error',
|
|
48
|
-
trigger: 'manual',
|
|
49
|
-
appendTo: 'parent',
|
|
50
|
-
placement: 'bottom-start',
|
|
51
|
-
animation: 'shift-away-subtle',
|
|
52
|
-
popperOptions: {
|
|
53
|
-
modifiers: [
|
|
54
|
-
{
|
|
55
|
-
name: 'flip',
|
|
56
|
-
enabled: false
|
|
57
|
-
}
|
|
58
|
-
]
|
|
59
|
-
},
|
|
60
|
-
interactive: true,
|
|
61
|
-
hideOnClick: false,
|
|
62
|
-
offset: [3, 3], // 1/2 form-spacing
|
|
63
|
-
zIndex: 1
|
|
64
|
-
})
|
|
65
|
-
tip.popper.addEventListener('mousedown', () => tip.hide())
|
|
66
|
-
tip.show()
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
23
|
}
|
|
70
24
|
})
|
|
71
25
|
</script>
|
|
@@ -74,11 +28,10 @@ export default DitoComponent.component('DitoErrors', {
|
|
|
74
28
|
@import '../styles/_imports';
|
|
75
29
|
|
|
76
30
|
.dito-errors {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
31
|
+
// Take a line of its own also in compact containers with wrapping rows.
|
|
32
|
+
width: 100%;
|
|
33
|
+
margin-top: $form-spacing-half;
|
|
34
|
+
color: $color-error;
|
|
35
|
+
white-space: pre-line;
|
|
83
36
|
}
|
|
84
37
|
</style>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
DitoFormInner(
|
|
17
17
|
v-show="isActiveRoute"
|
|
18
18
|
:nested="isNestedRoute"
|
|
19
|
-
:label="
|
|
19
|
+
:label="itemLabelText"
|
|
20
20
|
)
|
|
21
21
|
//- Prevent implicit submission of the form, for example when typing enter
|
|
22
22
|
//- in an input field.
|
|
@@ -52,7 +52,13 @@
|
|
|
52
52
|
|
|
53
53
|
<script>
|
|
54
54
|
import { markRaw } from 'vue'
|
|
55
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
clone,
|
|
57
|
+
capitalize,
|
|
58
|
+
parseDataPath,
|
|
59
|
+
assignDeeply,
|
|
60
|
+
escapeHtml
|
|
61
|
+
} from '@ditojs/utils'
|
|
56
62
|
import DitoComponent from '../DitoComponent.js'
|
|
57
63
|
import RouteMixin from '../mixins/RouteMixin.js'
|
|
58
64
|
import ResourceMixin from '../mixins/ResourceMixin.js'
|
|
@@ -61,7 +67,11 @@ import { isObjectSource } from '../utils/schema/structure.js'
|
|
|
61
67
|
import { getButtonSchemas } from '../utils/schema/lookup.js'
|
|
62
68
|
import { getComponentPathByDataPath } from '../utils/schema/data.js'
|
|
63
69
|
import { resolvePath } from '../utils/path.js'
|
|
70
|
+
import { isPathWithin } from '../utils/route.js'
|
|
71
|
+
import { transferUids } from '../utils/uid.js'
|
|
64
72
|
import { DataModel } from '../utils/DataModel.js'
|
|
73
|
+
import { transientNote } from '../utils/dialogs.js'
|
|
74
|
+
import { getTextFromHtml } from '../utils/html.js'
|
|
65
75
|
|
|
66
76
|
// @vue/component
|
|
67
77
|
export default DitoComponent.component('DitoForm', {
|
|
@@ -298,6 +308,11 @@ export default DitoComponent.component('DitoForm', {
|
|
|
298
308
|
|
|
299
309
|
itemLabel() {
|
|
300
310
|
return this.getItemLabel(this.sourceSchema, this.data, { extended: true })
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// `itemLabel` is HTML, while the `aria-label` of the form is text:
|
|
314
|
+
itemLabelText() {
|
|
315
|
+
return getTextFromHtml(this.itemLabel)
|
|
301
316
|
}
|
|
302
317
|
},
|
|
303
318
|
|
|
@@ -326,8 +341,11 @@ export default DitoComponent.component('DitoForm', {
|
|
|
326
341
|
if (!this.isMutating) {
|
|
327
342
|
// Edit a copy of the inherited item, so that changes are only
|
|
328
343
|
// applied through setSourceData(). Copy it again whenever the item
|
|
329
|
-
// itself is replaced, e.g. after applying the changes.
|
|
330
|
-
|
|
344
|
+
// itself is replaced, e.g. after applying the changes. The copy
|
|
345
|
+
// keeps the uids of the new items, see `transferUids()`.
|
|
346
|
+
const clonedData = clone(item)
|
|
347
|
+
transferUids(item, clonedData)
|
|
348
|
+
this.clonedData = clonedData
|
|
331
349
|
}
|
|
332
350
|
if (
|
|
333
351
|
item === null &&
|
|
@@ -399,11 +417,31 @@ export default DitoComponent.component('DitoForm', {
|
|
|
399
417
|
}
|
|
400
418
|
},
|
|
401
419
|
|
|
420
|
+
// Returns the source component that loads the form's item through its
|
|
421
|
+
// resource in the parent route component, e.g. a list in a parent form, or
|
|
422
|
+
// `null` if it isn't rendered, e.g. the list of a view, which renders the
|
|
423
|
+
// form instead. The form's path is within the path of the source.
|
|
424
|
+
getParentSourceComponent() {
|
|
425
|
+
return (
|
|
426
|
+
this.parentRouteComponent?.mainSchemaComponent?.components.find(
|
|
427
|
+
component => (
|
|
428
|
+
component.sourceComponent === component &&
|
|
429
|
+
component.providesData &&
|
|
430
|
+
isPathWithin(this.path, component.path)
|
|
431
|
+
)
|
|
432
|
+
) ?? null
|
|
433
|
+
)
|
|
434
|
+
},
|
|
435
|
+
|
|
402
436
|
setSourceData(data) {
|
|
403
437
|
if (this.sourceData && this.sourceKey !== null) {
|
|
404
438
|
const { mainSchemaComponent } = this
|
|
405
|
-
|
|
406
|
-
|
|
439
|
+
const { localData } = mainSchemaComponent.filterData(data)
|
|
440
|
+
// Keep the uids of new items, which key their components and stores,
|
|
441
|
+
// see `transferUids()`. The applied data is processed from the form's
|
|
442
|
+
// data, which holds them since cloning the item.
|
|
443
|
+
transferUids(this.data, localData)
|
|
444
|
+
this.sourceData[this.sourceKey] = localData
|
|
407
445
|
mainSchemaComponent.onChange()
|
|
408
446
|
return true
|
|
409
447
|
}
|
|
@@ -469,6 +507,28 @@ export default DitoComponent.component('DitoForm', {
|
|
|
469
507
|
}
|
|
470
508
|
|
|
471
509
|
const getVerb = present => this.verbs[this.getSubmitVerb(present)]
|
|
510
|
+
// NOTE: The notifications pass `html`, as `itemLabel` is HTML with
|
|
511
|
+
// escaped item values, see `getItemLabel()`. Other values that they
|
|
512
|
+
// include need to be escaped too.
|
|
513
|
+
const notifySubmitError = (error, message = null) => {
|
|
514
|
+
const verb = getVerb(true)
|
|
515
|
+
const { itemLabel } = this
|
|
516
|
+
this.notify({
|
|
517
|
+
type: 'error',
|
|
518
|
+
error,
|
|
519
|
+
title: 'Request Error',
|
|
520
|
+
html: message
|
|
521
|
+
? [`Unable to ${verb} ${itemLabel}:`, escapeHtml(message)]
|
|
522
|
+
: `Unable to ${verb} ${itemLabel}.`
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
const callbacks = {
|
|
526
|
+
onSuccess: () => this.emitSchemaEvent(this.getSubmitVerb()),
|
|
527
|
+
onError: error =>
|
|
528
|
+
this.emitSchemaEvent('error', {
|
|
529
|
+
context: { error }
|
|
530
|
+
})
|
|
531
|
+
}
|
|
472
532
|
|
|
473
533
|
// Allow buttons to override both method and resource path to submit to:
|
|
474
534
|
let { method } = this
|
|
@@ -482,65 +542,45 @@ export default DitoComponent.component('DitoForm', {
|
|
|
482
542
|
let success
|
|
483
543
|
if (!buttonResource && this.isTransient) {
|
|
484
544
|
success = await this.submitTransient(button, resource, method, data, {
|
|
485
|
-
|
|
486
|
-
onError: error =>
|
|
487
|
-
this.emitSchemaEvent('error', {
|
|
488
|
-
context: { error }
|
|
489
|
-
}),
|
|
545
|
+
...callbacks,
|
|
490
546
|
notifySuccess: () => {
|
|
491
547
|
const verb = getVerb(false)
|
|
548
|
+
const { itemLabel } = this
|
|
492
549
|
this.notify({
|
|
493
550
|
type: 'info',
|
|
494
551
|
title: this.isCreating
|
|
495
552
|
? `Item ${capitalize(verb)}`
|
|
496
553
|
: `Change ${capitalize(verb)}`,
|
|
497
|
-
|
|
554
|
+
html: [
|
|
498
555
|
this.isCreating
|
|
499
|
-
? `${
|
|
500
|
-
: `Changes to ${
|
|
501
|
-
|
|
556
|
+
? `${itemLabel} was ${verb}.`
|
|
557
|
+
: `Changes to ${itemLabel} were ${verb}.`,
|
|
558
|
+
transientNote
|
|
502
559
|
]
|
|
503
560
|
})
|
|
504
561
|
},
|
|
505
|
-
notifyError: error =>
|
|
506
|
-
const verb = getVerb(true)
|
|
507
|
-
this.notify({
|
|
508
|
-
type: 'error',
|
|
509
|
-
error,
|
|
510
|
-
title: 'Request Error',
|
|
511
|
-
text: `Unable to ${verb} ${this.itemLabel}.`
|
|
512
|
-
})
|
|
513
|
-
}
|
|
562
|
+
notifyError: error => notifySubmitError(error)
|
|
514
563
|
})
|
|
515
564
|
} else {
|
|
516
565
|
success = await this.submitResource(button, resource, method, data, {
|
|
566
|
+
...callbacks,
|
|
517
567
|
setData: true,
|
|
518
|
-
onSuccess: () => this.emitSchemaEvent(this.getSubmitVerb()),
|
|
519
|
-
onError: error =>
|
|
520
|
-
this.emitSchemaEvent('error', {
|
|
521
|
-
context: { error }
|
|
522
|
-
}),
|
|
523
568
|
notifySuccess: () => {
|
|
524
569
|
const verb = getVerb(false)
|
|
525
570
|
this.notify({
|
|
526
571
|
type: 'success',
|
|
527
572
|
title: `Successfully ${capitalize(verb)}`,
|
|
528
|
-
|
|
573
|
+
html: `${this.itemLabel} was ${verb}.`
|
|
529
574
|
})
|
|
530
575
|
},
|
|
531
|
-
notifyError: error =>
|
|
532
|
-
|
|
533
|
-
this.notify({
|
|
534
|
-
type: 'error',
|
|
535
|
-
error,
|
|
536
|
-
title: 'Request Error',
|
|
537
|
-
text: [
|
|
538
|
-
`Unable to ${verb} ${this.itemLabel}${error ? ':' : ''}`,
|
|
539
|
-
error?.message || error
|
|
540
|
-
]
|
|
541
|
-
})
|
|
542
|
-
}
|
|
576
|
+
notifyError: error =>
|
|
577
|
+
notifySubmitError(error, error?.message || error)
|
|
543
578
|
})
|
|
579
|
+
if (success) {
|
|
580
|
+
// The item changed on the server, so the source that lists it needs
|
|
581
|
+
// to load it again.
|
|
582
|
+
this.getParentSourceComponent()?.reloadData()
|
|
583
|
+
}
|
|
544
584
|
}
|
|
545
585
|
if (success) {
|
|
546
586
|
this.resetValidation()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
//- The prefix or suffix of the input of a type component, see `schema.prefix`
|
|
3
|
+
//- and `schema.suffix`, and `getInputAffixesProps()` for their props. The
|
|
4
|
+
//- suffix also holds the clear button of clearable components, and their info
|
|
5
|
+
//- when they have no label to show it.
|
|
6
|
+
DitoAffixes(
|
|
7
|
+
:items="items"
|
|
8
|
+
:position="position"
|
|
9
|
+
mode="input"
|
|
10
|
+
:absolute="absolute"
|
|
11
|
+
:clearable="clearable"
|
|
12
|
+
:hasValue="hasValue"
|
|
13
|
+
:disabled="disabled"
|
|
14
|
+
:inlineInfo="inlineInfo"
|
|
15
|
+
:parentContext="parentContext"
|
|
16
|
+
@clear="$emit('clear')"
|
|
17
|
+
)
|
|
18
|
+
template(
|
|
19
|
+
v-if="$slots.append"
|
|
20
|
+
#append
|
|
21
|
+
)
|
|
22
|
+
slot(name="append")
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
import DitoComponent from '../DitoComponent.js'
|
|
27
|
+
import DitoAffixes from './DitoAffixes.vue'
|
|
28
|
+
|
|
29
|
+
// @vue/component
|
|
30
|
+
export default DitoComponent.component('DitoInputAffixes', {
|
|
31
|
+
components: { DitoAffixes },
|
|
32
|
+
emits: ['clear'],
|
|
33
|
+
|
|
34
|
+
props: {
|
|
35
|
+
position: {
|
|
36
|
+
type: String,
|
|
37
|
+
required: true,
|
|
38
|
+
validator: position => ['prefix', 'suffix'].includes(position)
|
|
39
|
+
},
|
|
40
|
+
items: { type: [String, Object, Array], default: null },
|
|
41
|
+
absolute: { type: Boolean, default: false },
|
|
42
|
+
disabled: { type: Boolean, default: false },
|
|
43
|
+
clearable: { type: Boolean, default: false },
|
|
44
|
+
hasValue: { type: Boolean, default: false },
|
|
45
|
+
inlineInfo: { type: String, default: null },
|
|
46
|
+
// The context of the type component, for the callbacks of the items.
|
|
47
|
+
parentContext: { type: Object, required: true }
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
</script>
|
|
@@ -4,7 +4,6 @@ component.dito-label(
|
|
|
4
4
|
:is="collapsible ? 'button' : 'div'"
|
|
5
5
|
:type="collapsible ? 'button' : null"
|
|
6
6
|
v-bind="attributes"
|
|
7
|
-
:class="{ 'dito-label--active': isActive }"
|
|
8
7
|
)
|
|
9
8
|
.dito-chevron(
|
|
10
9
|
v-if="collapsible"
|
|
@@ -76,16 +75,11 @@ export default DitoComponent.component('DitoLabel', {
|
|
|
76
75
|
'aria-expanded': !this.collapsed
|
|
77
76
|
}
|
|
78
77
|
: {}
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
isActive() {
|
|
82
|
-
return this.appState.activeLabel === this
|
|
83
78
|
}
|
|
84
79
|
},
|
|
85
80
|
|
|
86
81
|
methods: {
|
|
87
82
|
onClick() {
|
|
88
|
-
this.appState.activeLabel = this
|
|
89
83
|
this.$emit('open', this.collapsed)
|
|
90
84
|
}
|
|
91
85
|
}
|
|
@@ -108,14 +102,6 @@ export default DitoComponent.component('DitoLabel', {
|
|
|
108
102
|
min-height: $input-height;
|
|
109
103
|
margin-right: $form-spacing-half; // When inlined.
|
|
110
104
|
|
|
111
|
-
&:has(.dito-schema-header) {
|
|
112
|
-
// The container's label is used as teleport for a nested section or object
|
|
113
|
-
// label. Hide `&__inner`, as it is be duplicated inside the nested label.
|
|
114
|
-
> #{$self}__inner {
|
|
115
|
-
display: none;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
105
|
&:not(:has(+ .dito-tabs)) {
|
|
120
106
|
// Take full width but only if there aren't also tabs to be centered.
|
|
121
107
|
flex: 1;
|
|
@@ -199,7 +185,7 @@ export default DitoComponent.component('DitoLabel', {
|
|
|
199
185
|
}
|
|
200
186
|
}
|
|
201
187
|
|
|
202
|
-
&:focus
|
|
188
|
+
&:focus-visible {
|
|
203
189
|
.dito-chevron {
|
|
204
190
|
-webkit-text-stroke: $border-width $color-active;
|
|
205
191
|
}
|
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
|
-
ul.dito-menu
|
|
3
|
-
v-resize="onResize"
|
|
4
|
-
:style="{ '--width': width ? `${width}px` : null }"
|
|
5
|
-
)
|
|
2
|
+
ul.dito-menu
|
|
6
3
|
li.dito-menu__item(
|
|
7
4
|
v-for="item in items"
|
|
8
5
|
)
|
|
9
6
|
template(
|
|
10
7
|
v-if="shouldShowItem(item)"
|
|
11
8
|
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
RouterLink(
|
|
10
|
+
v-slot="{ href, navigate }"
|
|
11
|
+
:to="getItemRoute(item)"
|
|
12
|
+
custom
|
|
13
|
+
)
|
|
14
|
+
a.dito-menu__link(
|
|
15
|
+
:href="href"
|
|
16
|
+
:class="{ 'dito-menu__link--active': isActiveItem(item) }"
|
|
17
|
+
:aria-current="isActiveItem(item) ? 'page' : null"
|
|
18
|
+
@click="navigate"
|
|
19
|
+
) {{ getLabel(item) }}
|
|
18
20
|
DitoMenu.dito-menu__sub(
|
|
19
21
|
v-if="item.items"
|
|
20
22
|
:items="item.items"
|
|
21
|
-
:path="getItemPath(item
|
|
23
|
+
:path="getItemPath(item)"
|
|
22
24
|
)
|
|
23
25
|
</template>
|
|
24
26
|
|
|
25
27
|
<script>
|
|
26
28
|
import DitoComponent from '../DitoComponent.js'
|
|
29
|
+
import { isPathWithin } from '../utils/route.js'
|
|
30
|
+
import { getViewLinkPath } from '../utils/schema/lookup.js'
|
|
27
31
|
|
|
28
32
|
// @vue/component
|
|
29
33
|
export default DitoComponent.component('DitoMenu', {
|
|
@@ -38,12 +42,6 @@ export default DitoComponent.component('DitoMenu', {
|
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
44
|
|
|
41
|
-
data() {
|
|
42
|
-
return {
|
|
43
|
-
width: 0
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
|
|
47
45
|
methods: {
|
|
48
46
|
shouldShowItem(item) {
|
|
49
47
|
return (
|
|
@@ -54,36 +52,25 @@ export default DitoComponent.component('DitoMenu', {
|
|
|
54
52
|
)
|
|
55
53
|
},
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
this.width = width
|
|
60
|
-
}
|
|
55
|
+
getItemPath(item) {
|
|
56
|
+
return `${this.path}/${item.path}`
|
|
61
57
|
},
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
getItemHref(item) {
|
|
71
|
-
const path = this.getItemPath(item, true)
|
|
72
|
-
return path ? this.$router.resolve(path).href : null
|
|
59
|
+
getItemRoute(item) {
|
|
60
|
+
// `DitoRoot` only provides the views once their paths are set up.
|
|
61
|
+
// `force` reloads the view even if it is already the current route.
|
|
62
|
+
return {
|
|
63
|
+
path: getViewLinkPath(item, this.getItemPath(item)),
|
|
64
|
+
force: true
|
|
65
|
+
}
|
|
73
66
|
},
|
|
74
67
|
|
|
75
68
|
isActiveItem(item) {
|
|
69
|
+
const path = this.getItemPath(item)
|
|
76
70
|
return (
|
|
77
|
-
this.$route.path
|
|
71
|
+
isPathWithin(this.$route.path, path) ||
|
|
78
72
|
item.items && Object.values(item.items).some(this.isActiveItem)
|
|
79
73
|
)
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
onClickItem(item) {
|
|
83
|
-
const path = this.getItemPath(item, true)
|
|
84
|
-
if (path) {
|
|
85
|
-
this.$router.push({ path, force: true })
|
|
86
|
-
}
|
|
87
74
|
}
|
|
88
75
|
}
|
|
89
76
|
})
|
|
@@ -104,23 +91,33 @@ export default DitoComponent.component('DitoMenu', {
|
|
|
104
91
|
padding: 0 $menu-spacing;
|
|
105
92
|
|
|
106
93
|
&__item {
|
|
94
|
+
// Scope the anchor of the open sub-menu to its item, see below.
|
|
95
|
+
anchor-scope: --dito-menu-item;
|
|
96
|
+
|
|
107
97
|
&:has(#{$self}__sub):not(:has(#{$self}__link--active)) {
|
|
108
|
-
// Pop-out sub-menus on hover
|
|
109
|
-
|
|
98
|
+
// Pop-out sub-menus on hover and keyboard focus. They are anchored to
|
|
99
|
+
// their item instead of positioned relative to it, as the menu scrolls
|
|
100
|
+
// and would clip them.
|
|
101
|
+
&:hover,
|
|
102
|
+
&:focus-within {
|
|
103
|
+
anchor-name: --dito-menu-item;
|
|
104
|
+
|
|
110
105
|
> #{$self}__link {
|
|
111
106
|
background: $color-lightest;
|
|
112
107
|
}
|
|
113
108
|
|
|
114
|
-
#{$self}__sub {
|
|
109
|
+
> #{$self}__sub {
|
|
115
110
|
display: block;
|
|
116
111
|
position: absolute;
|
|
117
|
-
|
|
112
|
+
position-anchor: --dito-menu-item;
|
|
113
|
+
top: anchor(top);
|
|
114
|
+
left: calc(anchor(right) + 2 * $menu-spacing);
|
|
115
|
+
width: anchor-size(width);
|
|
118
116
|
z-index: $z-index-header;
|
|
119
|
-
|
|
120
|
-
translateY(-$item-height);
|
|
117
|
+
box-shadow: $shadow-window;
|
|
121
118
|
|
|
122
|
-
#{$self}__item:first-child {
|
|
123
|
-
#{$self}__link {
|
|
119
|
+
> #{$self}__item:first-child {
|
|
120
|
+
> #{$self}__link {
|
|
124
121
|
margin-top: 0;
|
|
125
122
|
}
|
|
126
123
|
}
|
|
@@ -136,10 +133,6 @@ export default DitoComponent.component('DitoMenu', {
|
|
|
136
133
|
opacity: 0;
|
|
137
134
|
}
|
|
138
135
|
}
|
|
139
|
-
|
|
140
|
-
#{$self}__sub {
|
|
141
|
-
box-shadow: $shadow-window;
|
|
142
|
-
}
|
|
143
136
|
}
|
|
144
137
|
}
|
|
145
138
|
}
|
|
@@ -151,7 +144,7 @@ export default DitoComponent.component('DitoMenu', {
|
|
|
151
144
|
border-radius: $border-radius;
|
|
152
145
|
margin-top: $menu-spacing;
|
|
153
146
|
|
|
154
|
-
&:focus
|
|
147
|
+
&:focus-visible {
|
|
155
148
|
box-shadow: $shadow-focus;
|
|
156
149
|
}
|
|
157
150
|
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
span
|
|
5
5
|
.dito-notifications__inner
|
|
6
6
|
VueNotifications(
|
|
7
|
-
ref="notifications"
|
|
8
7
|
classes="dito-notification"
|
|
9
8
|
:dangerouslySetInnerHtml="true"
|
|
10
9
|
position=""
|
|
@@ -14,16 +13,24 @@
|
|
|
14
13
|
|
|
15
14
|
<script>
|
|
16
15
|
import DitoComponent from '../DitoComponent.js'
|
|
17
|
-
import { asArray, stripHtml } from '@ditojs/utils'
|
|
16
|
+
import { asArray, isString, escapeHtml, stripHtml } from '@ditojs/utils'
|
|
17
|
+
|
|
18
|
+
// Detects opening tags like `<a href="…">` or `<b>`, which callers that still
|
|
19
|
+
// pass formatted text through the escaped `text` option would include:
|
|
20
|
+
function isLikelyMarkup(paragraph) {
|
|
21
|
+
return (
|
|
22
|
+
isString(paragraph) &&
|
|
23
|
+
/<[a-z][a-z0-9-]*(\s[^>]*)?\/?>/i.test(paragraph)
|
|
24
|
+
)
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
// @vue/component
|
|
20
28
|
export default DitoComponent.component('DitoNotifications', {
|
|
21
|
-
notifications() {
|
|
22
|
-
return this.isMounted && this.$refs.notifications
|
|
23
|
-
},
|
|
24
|
-
|
|
25
29
|
methods: {
|
|
26
|
-
|
|
30
|
+
// Shows a notification with the paragraphs of `text`, which are escaped,
|
|
31
|
+
// or else of `html`, which is rendered as it is, so it must only contain
|
|
32
|
+
// HTML from trusted sources, and escape all values that it includes.
|
|
33
|
+
notify({ type = 'info', title, text, html, error, duration } = {}) {
|
|
27
34
|
title ||= (
|
|
28
35
|
{
|
|
29
36
|
warning: 'Warning',
|
|
@@ -33,8 +40,23 @@ export default DitoComponent.component('DitoNotifications', {
|
|
|
33
40
|
}[type] ||
|
|
34
41
|
'Notification'
|
|
35
42
|
)
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
const isHtml = html != null
|
|
44
|
+
// Skip empty paragraphs, e.g. `transientNote` when it's `false`:
|
|
45
|
+
const paragraphs = asArray(isHtml ? html : text).filter(
|
|
46
|
+
paragraph => paragraph != null && paragraph !== false
|
|
47
|
+
)
|
|
48
|
+
if (!isHtml && paragraphs.some(isLikelyMarkup)) {
|
|
49
|
+
console.warn(
|
|
50
|
+
'notify(): `text` is escaped and seems to contain HTML markup. Use ' +
|
|
51
|
+
'the `html` option for formatted text, and escape all values that ' +
|
|
52
|
+
'it includes.'
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
const htmlParagraphs = isHtml
|
|
56
|
+
? paragraphs
|
|
57
|
+
: paragraphs.map(paragraph => escapeHtml(paragraph))
|
|
58
|
+
const content = `<p>${
|
|
59
|
+
htmlParagraphs.join('</p> <p>')
|
|
38
60
|
}</p>`.replace(/\n|\r\n|\r/g, '<br>')
|
|
39
61
|
const log = (
|
|
40
62
|
{
|
|
@@ -48,7 +70,7 @@ export default DitoComponent.component('DitoNotifications', {
|
|
|
48
70
|
// eslint-disable-next-line no-console
|
|
49
71
|
console[log](
|
|
50
72
|
...[
|
|
51
|
-
stripHtml(
|
|
73
|
+
isHtml ? stripHtml(content) : paragraphs.join('\n'),
|
|
52
74
|
...(type === 'error' && error ? [error] : [])
|
|
53
75
|
]
|
|
54
76
|
)
|
|
@@ -59,18 +81,20 @@ export default DitoComponent.component('DitoNotifications', {
|
|
|
59
81
|
// amount of milliseconds multiplied with the amount of characters
|
|
60
82
|
// displayed in the notification, plus 40 (40 + title + message):
|
|
61
83
|
const { durationFactor = 20 } = notifications
|
|
62
|
-
duration ??= (40 +
|
|
84
|
+
duration ??= (40 + content.length + title.length) * durationFactor
|
|
63
85
|
this.$notify({
|
|
64
86
|
type,
|
|
65
|
-
title,
|
|
66
|
-
text,
|
|
87
|
+
title: escapeHtml(title),
|
|
88
|
+
text: content,
|
|
67
89
|
duration: duration === 0 ? -1 : duration // < 0 -> <= 0 = sticky
|
|
68
90
|
})
|
|
69
91
|
}
|
|
70
92
|
},
|
|
71
93
|
|
|
72
94
|
destroyAll() {
|
|
73
|
-
|
|
95
|
+
// `VueNotifications` doesn't expose methods, but closes all its
|
|
96
|
+
// notifications when notified with `clean`:
|
|
97
|
+
this.$notify({ clean: true })
|
|
74
98
|
}
|
|
75
99
|
}
|
|
76
100
|
})
|