@afeefa/vue-app 0.0.194 → 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/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,
|