@afeefa/vue-app 0.0.195 → 0.0.196
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/.afeefa/package/release/version.txt +1 -1
- package/package.json +1 -1
- package/src/components/ACategory.vue +112 -0
- package/src/components/APopup.vue +140 -0
- package/src/components/APopupMenu.vue +77 -0
- package/src/components/form/EditForm.vue +3 -0
- package/src/components/form/fields/FormFieldCategory.vue +194 -0
- package/src/components/index.js +2 -0
- package/src/components/popup-menu/ItemRenderer.vue +42 -0
- package/src/components/popup-menu/PopupMenuList.vue +194 -0
- package/src/components/search-select/SearchSelectList.vue +20 -2
- package/src-admin/components/App.vue +9 -0
- package/src-admin/config/vuetify.js +2 -0
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.196
|
package/package.json
CHANGED
@@ -0,0 +1,112 @@
|
|
1
|
+
<template>
|
2
|
+
<div :class="['a-category', {dark: theme === 'dark'}]">
|
3
|
+
<div
|
4
|
+
class="border"
|
5
|
+
:style="{background: borderColor}"
|
6
|
+
/>
|
7
|
+
|
8
|
+
<div
|
9
|
+
:class="['content', {hasRemove: $has.remove}]"
|
10
|
+
:title="category.title"
|
11
|
+
>
|
12
|
+
<div
|
13
|
+
v-for="parentCategory in parentCategories"
|
14
|
+
:key="parentCategory.id"
|
15
|
+
class="parent"
|
16
|
+
>
|
17
|
+
{{ parentCategory.title }}
|
18
|
+
</div>
|
19
|
+
|
20
|
+
<div>{{ category.title }}</div>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<div
|
24
|
+
v-if="$has.remove"
|
25
|
+
class="remove"
|
26
|
+
>
|
27
|
+
<a-icon
|
28
|
+
small
|
29
|
+
class="iconButton"
|
30
|
+
@click="$emit('remove')"
|
31
|
+
>
|
32
|
+
$closeCircleIcon
|
33
|
+
</a-icon>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
</template>
|
37
|
+
|
38
|
+
<script>
|
39
|
+
import { Component, Vue } from '@a-vue'
|
40
|
+
|
41
|
+
@Component({
|
42
|
+
props: ['category', 'theme', 'color']
|
43
|
+
})
|
44
|
+
export default class ACategory extends Vue {
|
45
|
+
$hasOptions = [{remove: false}]
|
46
|
+
|
47
|
+
get borderColor () {
|
48
|
+
return this.color || this.category.color || 'gray'
|
49
|
+
}
|
50
|
+
|
51
|
+
get parentCategories () {
|
52
|
+
const parents = []
|
53
|
+
let parent = this.category.parent
|
54
|
+
while (parent) {
|
55
|
+
parents.unshift(parent)
|
56
|
+
parent = parent.parent
|
57
|
+
}
|
58
|
+
return parents
|
59
|
+
}
|
60
|
+
}
|
61
|
+
</script>
|
62
|
+
|
63
|
+
<style lang="scss" scoped>
|
64
|
+
.a-category {
|
65
|
+
background: white;
|
66
|
+
white-space: nowrap;
|
67
|
+
font-size: 1rem;
|
68
|
+
display: flex;
|
69
|
+
align-items: stretch;
|
70
|
+
padding-right: .2rem;
|
71
|
+
|
72
|
+
&.dark {
|
73
|
+
background: #F6F6F6;
|
74
|
+
}
|
75
|
+
|
76
|
+
.border {
|
77
|
+
flex: 0 0 5px;
|
78
|
+
}
|
79
|
+
|
80
|
+
.content {
|
81
|
+
padding: .4rem .5rem;
|
82
|
+
display: flex;
|
83
|
+
justify-content: space-around;
|
84
|
+
flex-direction: column;
|
85
|
+
line-height: 1;
|
86
|
+
color: rgba(0, 0, 0, .87);
|
87
|
+
|
88
|
+
max-width: 200px;
|
89
|
+
|
90
|
+
> * {
|
91
|
+
overflow: hidden;
|
92
|
+
text-overflow: ellipsis;
|
93
|
+
}
|
94
|
+
|
95
|
+
.parent {
|
96
|
+
font-size: .8rem;
|
97
|
+
color: #999999;
|
98
|
+
line-height: 1;
|
99
|
+
}
|
100
|
+
|
101
|
+
&.hasRemove {
|
102
|
+
padding-right: .5rem;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
.remove {
|
107
|
+
align-self: center;
|
108
|
+
margin-top: -.1rem;
|
109
|
+
padding-right: .6rem;
|
110
|
+
}
|
111
|
+
}
|
112
|
+
</style>
|
@@ -0,0 +1,140 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="popup">
|
3
|
+
<slot />
|
4
|
+
</div>
|
5
|
+
</template>
|
6
|
+
|
7
|
+
<script>
|
8
|
+
import { Component, Mixins, Watch } from '@a-vue'
|
9
|
+
import { CancelOnEscMixin } from '@a-vue/services/escape/CancelOnEscMixin'
|
10
|
+
import { UsesPositionServiceMixin } from '../services/position/UsesPositionServiceMixin'
|
11
|
+
import { positionService, PositionConfig } from '../services/PositionService'
|
12
|
+
|
13
|
+
@Component({
|
14
|
+
props: ['position']
|
15
|
+
})
|
16
|
+
export default class APopup extends Mixins(CancelOnEscMixin, UsesPositionServiceMixin) {
|
17
|
+
popupTrigger = null
|
18
|
+
currentPosition = null
|
19
|
+
|
20
|
+
created () {
|
21
|
+
window.addEventListener('mousedown', this.onClickOutside)
|
22
|
+
window.addEventListener('wheel', this.onScroll, {passive: false})
|
23
|
+
window.addEventListener('touchmove', this.onScroll)
|
24
|
+
window.addEventListener('keydown', this.onScroll)
|
25
|
+
|
26
|
+
this.currentPosition = (this.position || new PositionConfig())
|
27
|
+
.setTarget(this)
|
28
|
+
.onAnchorEl(anchorEl => {
|
29
|
+
this.popupTrigger = anchorEl
|
30
|
+
})
|
31
|
+
|
32
|
+
this.urp_registerPositionWatcher(this.currentPosition)
|
33
|
+
|
34
|
+
this.coe_watchCancel()
|
35
|
+
}
|
36
|
+
|
37
|
+
mounted () {
|
38
|
+
this.getContainer().appendChild(this.$el)
|
39
|
+
}
|
40
|
+
|
41
|
+
destroyed () {
|
42
|
+
window.removeEventListener('mousedown', this.onClickOutside)
|
43
|
+
window.removeEventListener('wheel', this.onScroll)
|
44
|
+
window.removeEventListener('touchmove', this.onScroll)
|
45
|
+
window.removeEventListener('keydown', this.onScroll)
|
46
|
+
|
47
|
+
const container = this.getContainer()
|
48
|
+
if (container.contains(this.$el)) {
|
49
|
+
container.removeChild(this.$el)
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
@Watch('position')
|
54
|
+
positionChanged () {
|
55
|
+
this.currentPosition.update(this.position)
|
56
|
+
}
|
57
|
+
|
58
|
+
getContainer () {
|
59
|
+
return document.getElementById('popupContainer')
|
60
|
+
}
|
61
|
+
|
62
|
+
coe_cancelOnEsc () {
|
63
|
+
this.close()
|
64
|
+
return false
|
65
|
+
}
|
66
|
+
|
67
|
+
onScroll (event) {
|
68
|
+
// ignore esc event
|
69
|
+
if (event instanceof KeyboardEvent) {
|
70
|
+
const key = event.key || event.keyCode
|
71
|
+
if (key === 'Escape' || key === 'Esc' || key === 27) {
|
72
|
+
return
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
// ignore child popup scroll / event
|
77
|
+
if (this.targetIsIncludedPopup(event.target)) {
|
78
|
+
return
|
79
|
+
}
|
80
|
+
|
81
|
+
if (this.$el.contains(event.target)) {
|
82
|
+
if (!(event instanceof KeyboardEvent)) { // no keydown, must be wheel/touchmove
|
83
|
+
const scrollParent = positionService.getScrollParent(event.target, true)
|
84
|
+
if (!this.$el.contains(scrollParent)) { // do not scroll at all if target is not scrollable
|
85
|
+
event.preventDefault()
|
86
|
+
}
|
87
|
+
}
|
88
|
+
return
|
89
|
+
}
|
90
|
+
|
91
|
+
this.close()
|
92
|
+
}
|
93
|
+
|
94
|
+
targetIsIncludedPopup (target) {
|
95
|
+
const popups = this.getContainer().children
|
96
|
+
let foundMyself = false
|
97
|
+
for (const popup of popups) {
|
98
|
+
if (foundMyself) {
|
99
|
+
if (popup.contains(target)) {
|
100
|
+
return true
|
101
|
+
}
|
102
|
+
}
|
103
|
+
foundMyself = popup === this.$el
|
104
|
+
}
|
105
|
+
return false
|
106
|
+
}
|
107
|
+
|
108
|
+
close () {
|
109
|
+
this.$emit('close')
|
110
|
+
}
|
111
|
+
|
112
|
+
onClickOutside (e) {
|
113
|
+
// popup clicked
|
114
|
+
if (this.$el.contains(e.target)) {
|
115
|
+
return
|
116
|
+
}
|
117
|
+
|
118
|
+
// trigger clicked
|
119
|
+
if (this.popupTrigger.contains(e.target)) {
|
120
|
+
return
|
121
|
+
}
|
122
|
+
|
123
|
+
// check included popup clicked
|
124
|
+
if (this.targetIsIncludedPopup(e.target)) {
|
125
|
+
return
|
126
|
+
}
|
127
|
+
|
128
|
+
this.close()
|
129
|
+
}
|
130
|
+
}
|
131
|
+
</script>
|
132
|
+
|
133
|
+
<style lang="scss" scoped>
|
134
|
+
.popup {
|
135
|
+
position: absolute;
|
136
|
+
background-color: white;
|
137
|
+
padding: 1rem;
|
138
|
+
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .2), 0 2px 10px 0 rgba(0, 0, 0, .2);
|
139
|
+
}
|
140
|
+
</style>
|
@@ -0,0 +1,77 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="popupMenu">
|
3
|
+
<div
|
4
|
+
class="trigger"
|
5
|
+
@click="open"
|
6
|
+
>
|
7
|
+
<slot />
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<popup-menu-list
|
11
|
+
v-if="visible"
|
12
|
+
:items="items"
|
13
|
+
:position="position"
|
14
|
+
v-bind="$attrs"
|
15
|
+
@close="close"
|
16
|
+
@click="click"
|
17
|
+
>
|
18
|
+
<template #item="{ item }">
|
19
|
+
<component
|
20
|
+
:is="itemRenderer || ItemRenderer"
|
21
|
+
:item="item"
|
22
|
+
/>
|
23
|
+
</template>
|
24
|
+
</popup-menu-list>
|
25
|
+
</div>
|
26
|
+
</template>
|
27
|
+
|
28
|
+
|
29
|
+
<script>
|
30
|
+
import { PositionConfig } from '../services/PositionService'
|
31
|
+
import { Component, Vue } from '@a-vue'
|
32
|
+
import PopupMenuList from './popup-menu/PopupMenuList'
|
33
|
+
import ItemRenderer from './popup-menu/ItemRenderer'
|
34
|
+
|
35
|
+
@Component({
|
36
|
+
props: ['items', 'itemRenderer'],
|
37
|
+
components: {
|
38
|
+
PopupMenuList
|
39
|
+
}
|
40
|
+
})
|
41
|
+
export default class APopupMenu extends Vue {
|
42
|
+
visible = false
|
43
|
+
ItemRenderer = ItemRenderer
|
44
|
+
|
45
|
+
open () {
|
46
|
+
this.visible = true
|
47
|
+
}
|
48
|
+
|
49
|
+
click (item) {
|
50
|
+
this.$emit('select', item)
|
51
|
+
this.close()
|
52
|
+
}
|
53
|
+
|
54
|
+
close () {
|
55
|
+
this.visible = false
|
56
|
+
}
|
57
|
+
|
58
|
+
get position () {
|
59
|
+
const position = new PositionConfig()
|
60
|
+
.setAnchor(this, '.trigger')
|
61
|
+
.targetLeft().anchorRight()
|
62
|
+
.targetMiddle().anchorMiddle()
|
63
|
+
return position
|
64
|
+
}
|
65
|
+
}
|
66
|
+
</script>
|
67
|
+
|
68
|
+
|
69
|
+
<style lang="scss" scoped>
|
70
|
+
#popupContainer .selectPopup {
|
71
|
+
padding: .5rem;
|
72
|
+
}
|
73
|
+
|
74
|
+
.trigger {
|
75
|
+
display: inline-block;
|
76
|
+
}
|
77
|
+
</style>
|
@@ -19,6 +19,7 @@
|
|
19
19
|
|
20
20
|
<script>
|
21
21
|
import { Component, Vue, Watch } from '@a-vue'
|
22
|
+
// import { getDiff } from 'json-difference'
|
22
23
|
|
23
24
|
@Component({
|
24
25
|
props: [
|
@@ -102,6 +103,8 @@ export default class EditForm extends Vue {
|
|
102
103
|
}
|
103
104
|
// console.log(this.lastJson)
|
104
105
|
// console.log(this.json)
|
106
|
+
// console.log(JSON.stringify(getDiff(this.lastJson, this.json)))
|
107
|
+
|
105
108
|
return this.json !== this.lastJson
|
106
109
|
}
|
107
110
|
|
@@ -0,0 +1,194 @@
|
|
1
|
+
<template>
|
2
|
+
<div v-if="categories_">
|
3
|
+
<v-input
|
4
|
+
ref="validationInput"
|
5
|
+
:value="model[name]"
|
6
|
+
:rules="validationRules"
|
7
|
+
hide-details="auto"
|
8
|
+
>
|
9
|
+
<div>
|
10
|
+
<v-label v-if="false">
|
11
|
+
<a-row gap="2">
|
12
|
+
<strong v-if="false">{{ label }}</strong>
|
13
|
+
|
14
|
+
<a-popup-menu
|
15
|
+
v-if="selectedCategories.length && isMultiple"
|
16
|
+
:items="categories"
|
17
|
+
:canSelectParent="false"
|
18
|
+
@select="add"
|
19
|
+
>
|
20
|
+
<a-icon
|
21
|
+
size="1.3rem"
|
22
|
+
class="contextButton mt-n1"
|
23
|
+
title="Bearbeiten"
|
24
|
+
>
|
25
|
+
$addIcon
|
26
|
+
</a-icon>
|
27
|
+
</a-popup-menu>
|
28
|
+
</a-row>
|
29
|
+
</v-label>
|
30
|
+
|
31
|
+
<div
|
32
|
+
v-if="selectedCategories.length"
|
33
|
+
class="categories"
|
34
|
+
>
|
35
|
+
<a-category
|
36
|
+
v-for="category in selectedCategories"
|
37
|
+
:key="category.id"
|
38
|
+
:category="{...category, parent: {title: label}}"
|
39
|
+
theme="dark"
|
40
|
+
:has="{remove: true}"
|
41
|
+
:color="color"
|
42
|
+
@remove="remove(category)"
|
43
|
+
/>
|
44
|
+
</div>
|
45
|
+
|
46
|
+
<a-popup-menu
|
47
|
+
v-if="!selectedCategories.length"
|
48
|
+
:items="categories_"
|
49
|
+
:canSelectParent="false"
|
50
|
+
class="mt-2"
|
51
|
+
@select="add"
|
52
|
+
>
|
53
|
+
<a-icon-button
|
54
|
+
small
|
55
|
+
icon="$plusIcon"
|
56
|
+
:text="label"
|
57
|
+
/>
|
58
|
+
</a-popup-menu>
|
59
|
+
|
60
|
+
<a-popup-menu
|
61
|
+
v-else-if="isMultiple && selectedCategories.length < maxItems"
|
62
|
+
:items="categories_"
|
63
|
+
:canSelectParent="false"
|
64
|
+
class="mt-1"
|
65
|
+
@select="add"
|
66
|
+
>
|
67
|
+
<a>{{ label }} hinzufügen</a>
|
68
|
+
</a-popup-menu>
|
69
|
+
</div>
|
70
|
+
</v-input>
|
71
|
+
|
72
|
+
<div
|
73
|
+
v-if="errorMessages.length"
|
74
|
+
class="mt-4 error--text"
|
75
|
+
>
|
76
|
+
{{ errorMessages[0] }}
|
77
|
+
</div>
|
78
|
+
</div>
|
79
|
+
</template>
|
80
|
+
|
81
|
+
<script>
|
82
|
+
import { Component, Mixins } from '@a-vue'
|
83
|
+
import { FormFieldMixin } from '../FormFieldMixin'
|
84
|
+
import { ListAction } from '@a-vue/api-resources/ApiActions'
|
85
|
+
|
86
|
+
@Component({
|
87
|
+
props: [
|
88
|
+
'color',
|
89
|
+
{
|
90
|
+
getChildren: {
|
91
|
+
default: () => i => i.children || []
|
92
|
+
}
|
93
|
+
},
|
94
|
+
'categories'
|
95
|
+
]
|
96
|
+
})
|
97
|
+
export default class FormFieldCategory extends Mixins(FormFieldMixin) {
|
98
|
+
categories_ = null
|
99
|
+
errorMessages = []
|
100
|
+
|
101
|
+
async created () {
|
102
|
+
if (this.categories) {
|
103
|
+
this.initCategories(this.categories)
|
104
|
+
} else {
|
105
|
+
await this.loadCategories()
|
106
|
+
}
|
107
|
+
|
108
|
+
this.$nextTick(() => { // wait for root v-if
|
109
|
+
this.$refs.validationInput.validate(true)
|
110
|
+
})
|
111
|
+
}
|
112
|
+
|
113
|
+
mounted () {
|
114
|
+
|
115
|
+
}
|
116
|
+
|
117
|
+
add (category) {
|
118
|
+
if (this.isMultiple) {
|
119
|
+
this.remove(category) // add again at end
|
120
|
+
this.model[this.name].push(category)
|
121
|
+
} else {
|
122
|
+
this.model[this.name] = category
|
123
|
+
}
|
124
|
+
|
125
|
+
this.$emit('add', category)
|
126
|
+
}
|
127
|
+
|
128
|
+
remove (category) {
|
129
|
+
if (this.isMultiple) {
|
130
|
+
this.model[this.name] = this.selectedCategories.filter(c => c.id !== category.id)
|
131
|
+
} else {
|
132
|
+
this.model[this.name] = null
|
133
|
+
}
|
134
|
+
|
135
|
+
this.$emit('remove', category)
|
136
|
+
}
|
137
|
+
|
138
|
+
get selectedCategories () {
|
139
|
+
if (this.isMultiple) {
|
140
|
+
return this.model[this.name]
|
141
|
+
} else {
|
142
|
+
return [this.model[this.name]].filter(c => c)
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
get isMultiple () {
|
147
|
+
return this.field.getRelatedType().isList
|
148
|
+
}
|
149
|
+
|
150
|
+
async loadCategories () {
|
151
|
+
const request = this.field.createOptionsRequest()
|
152
|
+
const {models} = await ListAction.fromRequest(request).load()
|
153
|
+
this.initCategories(models)
|
154
|
+
}
|
155
|
+
|
156
|
+
initCategories (models) {
|
157
|
+
this.categories_ = models.map(m => {
|
158
|
+
let children = this.getChildren(m).map(c => ({
|
159
|
+
...c,
|
160
|
+
color: this.color
|
161
|
+
}))
|
162
|
+
|
163
|
+
if (children.length === 1) {
|
164
|
+
m = children[0]
|
165
|
+
children = []
|
166
|
+
}
|
167
|
+
|
168
|
+
return {
|
169
|
+
...m,
|
170
|
+
color: this.color,
|
171
|
+
children
|
172
|
+
}
|
173
|
+
})
|
174
|
+
}
|
175
|
+
|
176
|
+
get maxItems () {
|
177
|
+
return this.validator?.getParam('max') || 100000
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
181
|
+
</script>
|
182
|
+
|
183
|
+
|
184
|
+
<style lang="scss" scoped>
|
185
|
+
:deep() .v-messages__message {
|
186
|
+
color: #EE5252;
|
187
|
+
}
|
188
|
+
|
189
|
+
.categories {
|
190
|
+
display: flex;
|
191
|
+
flex-wrap: wrap;
|
192
|
+
gap: .5rem;
|
193
|
+
}
|
194
|
+
</style>
|
package/src/components/index.js
CHANGED
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
|
2
2
|
|
3
3
|
import EditForm from './form/EditForm'
|
4
4
|
import EditModal from './form/EditModal'
|
5
|
+
import FormFieldCategory from './form/fields/FormFieldCategory'
|
5
6
|
import FormFieldCheckbox from './form/fields/FormFieldCheckbox'
|
6
7
|
import FormFieldDate from './form/fields/FormFieldDate'
|
7
8
|
import FormFieldRadioGroup from './form/fields/FormFieldRadioGroup'
|
@@ -25,6 +26,7 @@ Vue.component('FormFieldText', FormFieldText)
|
|
25
26
|
Vue.component('FormFieldTextArea', FormFieldTextArea)
|
26
27
|
Vue.component('FormFieldRichTextArea', FormFieldRichTextArea)
|
27
28
|
Vue.component('FormFieldRadioGroup', FormFieldRadioGroup)
|
29
|
+
Vue.component('FormFieldCategory', FormFieldCategory)
|
28
30
|
Vue.component('FormFieldCheckbox', FormFieldCheckbox)
|
29
31
|
Vue.component('FormFieldDate', FormFieldDate)
|
30
32
|
Vue.component('FormFieldTime', FormFieldTime)
|
@@ -0,0 +1,42 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="itemRenderer">
|
3
|
+
<div
|
4
|
+
class="border"
|
5
|
+
:style="{background: color}"
|
6
|
+
/>
|
7
|
+
<div class="content">
|
8
|
+
{{ item.title }}
|
9
|
+
</div>
|
10
|
+
</div>
|
11
|
+
</template>
|
12
|
+
|
13
|
+
<script>
|
14
|
+
import { Component, Vue } from '@a-vue'
|
15
|
+
|
16
|
+
@Component({
|
17
|
+
props: ['item']
|
18
|
+
})
|
19
|
+
export default class ItemRenderer extends Vue {
|
20
|
+
get color () {
|
21
|
+
return this.item.color || 'gray'
|
22
|
+
}
|
23
|
+
}
|
24
|
+
</script>
|
25
|
+
|
26
|
+
<style lang="scss" scoped>
|
27
|
+
.itemRenderer {
|
28
|
+
min-width: 200px;
|
29
|
+
line-height: 1.2;
|
30
|
+
display: flex;
|
31
|
+
font-size: .9rem;
|
32
|
+
|
33
|
+
.border {
|
34
|
+
flex: 0 0 4px;
|
35
|
+
margin-right: .5rem;
|
36
|
+
}
|
37
|
+
|
38
|
+
.content {
|
39
|
+
padding: .2rem .2rem;
|
40
|
+
}
|
41
|
+
}
|
42
|
+
</style>
|
@@ -0,0 +1,194 @@
|
|
1
|
+
<template>
|
2
|
+
<a-popup
|
3
|
+
:class="['popupMenuList', 'level-' + nestLevel]"
|
4
|
+
:position="position"
|
5
|
+
@close="$emit('close')"
|
6
|
+
>
|
7
|
+
<template v-if="parent">
|
8
|
+
<div
|
9
|
+
:class="['item parent', {selected: selectedItem === parent}]"
|
10
|
+
@click="click(parent)"
|
11
|
+
>
|
12
|
+
<div class="content">
|
13
|
+
<slot
|
14
|
+
name="item"
|
15
|
+
:item="parent"
|
16
|
+
/>
|
17
|
+
</div>
|
18
|
+
</div>
|
19
|
+
|
20
|
+
<hr>
|
21
|
+
</template>
|
22
|
+
|
23
|
+
<input
|
24
|
+
v-if="items.length > 20"
|
25
|
+
v-model="filterTerm"
|
26
|
+
type="text"
|
27
|
+
class="inputElement inputElement--filter"
|
28
|
+
placeholder="Filtern"
|
29
|
+
autofocus
|
30
|
+
>
|
31
|
+
|
32
|
+
<div
|
33
|
+
v-for="item in filteredItems"
|
34
|
+
:key="item.id"
|
35
|
+
@click="click(item)"
|
36
|
+
>
|
37
|
+
<div
|
38
|
+
:class="['item', {isFolder: item.children?.length, selected: selectedItem === item}]"
|
39
|
+
:data-id="item.id"
|
40
|
+
>
|
41
|
+
<div class="content">
|
42
|
+
<slot
|
43
|
+
name="item"
|
44
|
+
:item="item"
|
45
|
+
/>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<a-icon
|
49
|
+
v-if="item.children?.length"
|
50
|
+
icon="chevron-right"
|
51
|
+
class="more"
|
52
|
+
>
|
53
|
+
$chevronRightIcon
|
54
|
+
</a-icon>
|
55
|
+
</div>
|
56
|
+
|
57
|
+
<popup-menu-list
|
58
|
+
v-if="item.children?.length && selectedItem === item"
|
59
|
+
:parent="canSelectParent && item"
|
60
|
+
:items="item.children"
|
61
|
+
:level="nestLevel + 1"
|
62
|
+
:position="getPosition(item)"
|
63
|
+
@close="closeNested"
|
64
|
+
@click="clickNested"
|
65
|
+
>
|
66
|
+
<template #item="{ item: subItem }">
|
67
|
+
<slot
|
68
|
+
name="item"
|
69
|
+
:item="subItem"
|
70
|
+
/>
|
71
|
+
</template>
|
72
|
+
</popup-menu-list>
|
73
|
+
</div>
|
74
|
+
<p
|
75
|
+
v-if="filteredItems.length === 0"
|
76
|
+
class="noItems"
|
77
|
+
>
|
78
|
+
Keine Einträge
|
79
|
+
</p>
|
80
|
+
</a-popup>
|
81
|
+
</template>
|
82
|
+
|
83
|
+
|
84
|
+
<script>
|
85
|
+
import { Component, Vue } from '@a-vue'
|
86
|
+
import { PositionConfig } from '../../services/PositionService'
|
87
|
+
|
88
|
+
@Component({
|
89
|
+
props: ['items', 'parent', 'level', 'position', {canSelectParent: true}],
|
90
|
+
name: 'popup-menu-list'
|
91
|
+
})
|
92
|
+
export default class PopupMenuList extends Vue {
|
93
|
+
selectedItem = null
|
94
|
+
filterTerm = ''
|
95
|
+
|
96
|
+
get nestLevel () {
|
97
|
+
return this.level || 0
|
98
|
+
}
|
99
|
+
|
100
|
+
get filteredItems () {
|
101
|
+
if (!this.filterTerm) {
|
102
|
+
return this.items
|
103
|
+
} else {
|
104
|
+
return this.items.filter(item => {
|
105
|
+
return item.title.toLowerCase().includes(this.filterTerm.toLowerCase())
|
106
|
+
})
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
click (item) {
|
111
|
+
if (item === this.parent) { // parent item
|
112
|
+
this.$emit('click', item)
|
113
|
+
} else if (item.children?.length) { // container
|
114
|
+
this.openNested(item)
|
115
|
+
} else { // single item
|
116
|
+
this.$emit('click', item)
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
clickNested (item) {
|
121
|
+
this.$emit('click', item)
|
122
|
+
}
|
123
|
+
|
124
|
+
openNested (item) {
|
125
|
+
this.selectedItem = item
|
126
|
+
}
|
127
|
+
|
128
|
+
closeNested () {
|
129
|
+
this.selectedItem = null
|
130
|
+
}
|
131
|
+
|
132
|
+
getPosition (item) {
|
133
|
+
// console.log('getPosition this.' + `.item[data-id="${item.id}"]`)
|
134
|
+
const position = new PositionConfig()
|
135
|
+
.setAnchor(this, `.item[data-id="${item.id}"]`)
|
136
|
+
.targetLeft().anchorRight()
|
137
|
+
.targetMiddle().anchorMiddle()
|
138
|
+
.diffX('.2rem')
|
139
|
+
return position
|
140
|
+
}
|
141
|
+
}
|
142
|
+
</script>
|
143
|
+
|
144
|
+
|
145
|
+
<style lang="scss" scoped>
|
146
|
+
#popupContainer .popupMenuList {
|
147
|
+
padding: .3rem .5rem;
|
148
|
+
max-height: 600px;
|
149
|
+
overflow-y: auto;
|
150
|
+
}
|
151
|
+
|
152
|
+
.noItems {
|
153
|
+
font-size: .8rem;
|
154
|
+
font-weight:bold;
|
155
|
+
margin: .5em 0;
|
156
|
+
white-space: nowrap;
|
157
|
+
}
|
158
|
+
|
159
|
+
.inputElement--filter {
|
160
|
+
font-size: .8rem;
|
161
|
+
margin-bottom: .5em;
|
162
|
+
padding: .5em;
|
163
|
+
}
|
164
|
+
|
165
|
+
.item {
|
166
|
+
display: flex;
|
167
|
+
align-items: center;
|
168
|
+
margin: .2rem 0;
|
169
|
+
|
170
|
+
cursor: pointer;
|
171
|
+
user-select: none;
|
172
|
+
|
173
|
+
&:hover,
|
174
|
+
&.selected {
|
175
|
+
background-color: #ECECEC;
|
176
|
+
}
|
177
|
+
|
178
|
+
&.isFolder {
|
179
|
+
color: #666666;
|
180
|
+
}
|
181
|
+
|
182
|
+
.more {
|
183
|
+
text-align: right;
|
184
|
+
margin-left: 1rem;
|
185
|
+
color: #999999;
|
186
|
+
font-size: .8rem;
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
hr {
|
191
|
+
border: none;
|
192
|
+
border-top: 1px solid #DDDDDD;
|
193
|
+
}
|
194
|
+
</style>
|
@@ -153,15 +153,33 @@ export default class SearchSelectList extends Mixins(ListViewMixin) {
|
|
153
153
|
}
|
154
154
|
|
155
155
|
findActiveIndexForLocalSearch () {
|
156
|
+
let firstMatchingIndex = -1
|
157
|
+
|
156
158
|
for (const [index, model] of this.models_.entries()) {
|
157
159
|
const regex = new RegExp(`^${this.localSearchKey}`, 'i')
|
158
160
|
if (model.getTitle().match(regex)) {
|
159
|
-
if (this.activeModelIndex
|
161
|
+
if (this.activeModelIndex < 0) { // return first item found if nothing selected before
|
162
|
+
return index
|
163
|
+
}
|
164
|
+
|
165
|
+
if (this.activeModelIndex === index) { // ignore selected item
|
160
166
|
continue
|
161
167
|
}
|
162
|
-
|
168
|
+
|
169
|
+
if (firstMatchingIndex < 0) { // save first item to later jump to, if no item after selected can be found
|
170
|
+
firstMatchingIndex = index
|
171
|
+
}
|
172
|
+
|
173
|
+
if (index > this.activeModelIndex) { // return first item after selected
|
174
|
+
return index
|
175
|
+
}
|
163
176
|
}
|
164
177
|
}
|
178
|
+
|
179
|
+
if (firstMatchingIndex >= 0) { // jump to first item of list that matchs
|
180
|
+
return firstMatchingIndex
|
181
|
+
}
|
182
|
+
|
165
183
|
return this.activeModelIndex
|
166
184
|
}
|
167
185
|
}
|
@@ -35,6 +35,8 @@
|
|
35
35
|
<a-save-indicator />
|
36
36
|
|
37
37
|
<flying-context-container />
|
38
|
+
|
39
|
+
<div id="popupContainer" />
|
38
40
|
</div>
|
39
41
|
</template>
|
40
42
|
|
@@ -128,4 +130,11 @@ export default class App extends Vue {
|
|
128
130
|
#v-main.marginRight {
|
129
131
|
margin-right: 60px;
|
130
132
|
}
|
133
|
+
|
134
|
+
#popupContainer {
|
135
|
+
position: absolute;
|
136
|
+
top: 0;
|
137
|
+
left: 0;
|
138
|
+
z-index: 400;
|
139
|
+
}
|
131
140
|
</style>
|
@@ -8,6 +8,7 @@ import {
|
|
8
8
|
mdiCheckBold,
|
9
9
|
mdiChevronRight,
|
10
10
|
mdiClose,
|
11
|
+
mdiCloseCircle,
|
11
12
|
mdiCloseThick,
|
12
13
|
mdiCurrencyEur,
|
13
14
|
mdiDelete,
|
@@ -44,6 +45,7 @@ export default new Vuetify({
|
|
44
45
|
alarmIcon: mdiAlarmLightOutline,
|
45
46
|
alertIcon: mdiAlert,
|
46
47
|
closeIcon: mdiClose,
|
48
|
+
closeCircleIcon: mdiCloseCircle,
|
47
49
|
closeBoldIcon: mdiCloseThick,
|
48
50
|
dotsVerticalIcon: mdiDotsVertical,
|
49
51
|
dotsHorizontalIcon: mdiDotsHorizontal,
|