@afeefa/vue-app 0.0.194 → 0.0.196
Sign up to get free protection for your applications and to get access to all the features.
- 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/flying-context/FlyingContextEvent.js +1 -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/components/FlyingContext.vue +8 -4
- package/src-admin/components/FlyingContextContainer.vue +14 -5
- 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>
|
@@ -78,10 +78,14 @@ export default class FlyingContext extends Mixins(CancelOnEscMixin) {
|
|
78
78
|
}
|
79
79
|
}
|
80
80
|
|
81
|
-
this.$
|
82
|
-
|
83
|
-
|
84
|
-
|
81
|
+
this.$events.dispatch(new FlyingContextEvent(FlyingContextEvent.START_HIDE_CONTEXT))
|
82
|
+
|
83
|
+
setTimeout(() => { // fade in then hide contents
|
84
|
+
this.$el.appendChild(this.getContent())
|
85
|
+
this.coe_unwatchCancel() // hide context -> do not watch esc any more
|
86
|
+
this.isVisible = false
|
87
|
+
this.$emit('hide')
|
88
|
+
}, 200)
|
85
89
|
}
|
86
90
|
}
|
87
91
|
|
@@ -44,6 +44,8 @@ export default class FlyingContextContainer extends Vue {
|
|
44
44
|
|
45
45
|
const sizeWatcher = new ResizeObserver(this.sizeChanged)
|
46
46
|
sizeWatcher.observe(this.getChildrenContainer())
|
47
|
+
|
48
|
+
this.$events.on(FlyingContextEvent.START_HIDE_CONTEXT, this.onStartHideContext)
|
47
49
|
}
|
48
50
|
|
49
51
|
/**
|
@@ -115,18 +117,25 @@ export default class FlyingContextContainer extends Vue {
|
|
115
117
|
el.style.overflowY = this.oldOverflowY
|
116
118
|
el.style.marginRight = 0
|
117
119
|
|
118
|
-
this.$el.style.left = '101vw' // set this if closing from outside e.g. via esc, which does not call this.hide()
|
120
|
+
this.$el.style.left = '101vw' // set this if closing from outside e.g. via context.esc, which does not call this.hide()
|
119
121
|
}
|
122
|
+
|
120
123
|
this.isClosing = false
|
121
124
|
}
|
122
125
|
|
126
|
+
onStartHideContext () {
|
127
|
+
// context will be removed in 200ms
|
128
|
+
// we start right now and slide out the container
|
129
|
+
// so that all the contents will last until moved out of screen
|
130
|
+
this.$el.style.left = '101vw'
|
131
|
+
}
|
132
|
+
|
123
133
|
hide () {
|
124
134
|
if (this.visible) {
|
125
|
-
|
135
|
+
// ignore resize watcher while closing, prevents flickering
|
126
136
|
this.isClosing = true
|
127
|
-
|
128
|
-
|
129
|
-
}, 200)
|
137
|
+
// say the context that it should try to remove
|
138
|
+
this.$events.dispatch(new FlyingContextEvent(FlyingContextEvent.HIDE_ALL))
|
130
139
|
}
|
131
140
|
}
|
132
141
|
|
@@ -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,
|