@afeefa/vue-app 0.0.121 → 0.0.123
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/ASearchSelect.vue +87 -18
- package/src/components/ATableRow.vue +7 -0
- package/src/components/ATextField.vue +4 -4
- package/src/components/form/EditForm.vue +2 -2
- package/src/components/form/fields/FormFieldSearchSelect.vue +94 -31
- package/src/components/list/ListViewMixin.js +12 -6
- package/src/components/list/filters/ListFilterSearch.vue +6 -0
- package/src/components/search-select/SearchSelectList.vue +71 -4
- package/src-admin/components/FlyingContext.vue +9 -2
- package/src-admin/components/FlyingContextContainer.vue +12 -2
- package/src-admin/components/controls/SearchSelectFormField.vue +0 -1
- package/src-admin/components/list/ListView.vue +10 -0
- package/src-admin/styles.scss +4 -0
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.123
|
package/package.json
CHANGED
@@ -2,12 +2,13 @@
|
|
2
2
|
<div :class="['a-search-select', widthClass]">
|
3
3
|
<div
|
4
4
|
class="activator"
|
5
|
-
style="width: max-content;"
|
6
5
|
@click="open"
|
7
6
|
>
|
8
7
|
<slot
|
9
8
|
v-if="!autoOpen"
|
10
9
|
name="activator"
|
10
|
+
:open="open"
|
11
|
+
:close="close"
|
11
12
|
>
|
12
13
|
<a-icon class="contextButton">
|
13
14
|
$dotsHorizontalIcon
|
@@ -15,6 +16,12 @@
|
|
15
16
|
</slot>
|
16
17
|
</div>
|
17
18
|
|
19
|
+
<v-overlay
|
20
|
+
:value="isOpen"
|
21
|
+
:z-index="299"
|
22
|
+
:opacity="0"
|
23
|
+
/>
|
24
|
+
|
18
25
|
<div :class="panelCssClass">
|
19
26
|
<div
|
20
27
|
v-if="isOpen"
|
@@ -24,7 +31,7 @@
|
|
24
31
|
<div class="background elevation-6" />
|
25
32
|
|
26
33
|
<search-select-filters
|
27
|
-
v-if="filtersInitialized"
|
34
|
+
v-if="filtersInitialized && showFilters"
|
28
35
|
:filters="filters"
|
29
36
|
:count="count"
|
30
37
|
>
|
@@ -32,6 +39,9 @@
|
|
32
39
|
name="filters"
|
33
40
|
:filters="filters"
|
34
41
|
:count="count"
|
42
|
+
:onSearchInputKey="{
|
43
|
+
'keydown': searchFilterKeyDown
|
44
|
+
}"
|
35
45
|
/>
|
36
46
|
</search-select-filters>
|
37
47
|
|
@@ -39,7 +49,7 @@
|
|
39
49
|
absolute
|
40
50
|
top
|
41
51
|
left
|
42
|
-
class="loadingIndicator"
|
52
|
+
:class="['loadingIndicator', {showFilters}]"
|
43
53
|
:isLoading="isLoading"
|
44
54
|
/>
|
45
55
|
</div>
|
@@ -48,21 +58,23 @@
|
|
48
58
|
<div :class="listCssClass">
|
49
59
|
<search-select-list
|
50
60
|
v-if="isOpen"
|
61
|
+
ref="list"
|
51
62
|
:listAction="listAction"
|
52
63
|
:q="q"
|
53
64
|
:selectedItems="selectedItems"
|
54
65
|
:events="false"
|
55
66
|
:history="false"
|
56
67
|
:filterSource="filterSource"
|
57
|
-
:loadOnlyIfKeyword="
|
68
|
+
:loadOnlyIfKeyword="loadOnlyIfKeyword"
|
58
69
|
:filters.sync="filters"
|
59
70
|
:count.sync="count"
|
60
71
|
:isLoading.sync="isLoading"
|
61
72
|
:style="cwm_widthStyle"
|
73
|
+
@onLoad="onLoad"
|
74
|
+
@enter="selectItem"
|
75
|
+
@backtab="setFocusToSearchInput"
|
62
76
|
>
|
63
77
|
<template #header>
|
64
|
-
<div />
|
65
|
-
|
66
78
|
<slot name="header" />
|
67
79
|
</template>
|
68
80
|
|
@@ -101,9 +113,13 @@ import { ComponentWidthMixin } from './mixins/ComponentWidthMixin'
|
|
101
113
|
props: [
|
102
114
|
'listAction',
|
103
115
|
'q',
|
104
|
-
'width',
|
105
|
-
'loadOnlyIfKeyword',
|
106
116
|
{
|
117
|
+
diffXControls: '-1rem',
|
118
|
+
diffYControls: '-1rem',
|
119
|
+
getSearchInput: {
|
120
|
+
default: () => () => {}
|
121
|
+
},
|
122
|
+
loadOnlyIfKeyword: false,
|
107
123
|
autoOpen: false,
|
108
124
|
closeOnSelect: true,
|
109
125
|
selectedItems: []
|
@@ -123,6 +139,7 @@ export default class ASearchSelect extends Mixins(ComponentWidthMixin, UsesPosit
|
|
123
139
|
isLoading = false
|
124
140
|
filters = []
|
125
141
|
count = 0
|
142
|
+
showFilters = false
|
126
143
|
|
127
144
|
mounted () {
|
128
145
|
if (this.autoOpen) {
|
@@ -173,10 +190,6 @@ export default class ASearchSelect extends Mixins(ComponentWidthMixin, UsesPosit
|
|
173
190
|
return container.querySelector('.' + this.listCssClass)
|
174
191
|
}
|
175
192
|
|
176
|
-
get _loadOnlyIfKeyword () {
|
177
|
-
return this.loadOnlyIfKeyword === undefined || this.loadOnlyIfKeyword
|
178
|
-
}
|
179
|
-
|
180
193
|
positionize () {
|
181
194
|
const position = new PositionConfig()
|
182
195
|
.setAnchor(this, '.activator')
|
@@ -186,7 +199,7 @@ export default class ASearchSelect extends Mixins(ComponentWidthMixin, UsesPosit
|
|
186
199
|
)
|
187
200
|
.anchorTop().targetTop()
|
188
201
|
.anchorLeft().targetLeft()
|
189
|
-
.diffX(
|
202
|
+
.diffX(this.diffXControls).diffY(this.diffYControls)
|
190
203
|
.onPosition(this.onListPositionChanged)
|
191
204
|
|
192
205
|
this.urp_registerPositionWatcher(position)
|
@@ -216,6 +229,10 @@ export default class ASearchSelect extends Mixins(ComponentWidthMixin, UsesPosit
|
|
216
229
|
}
|
217
230
|
|
218
231
|
open () {
|
232
|
+
this.showFilters = false
|
233
|
+
|
234
|
+
this.$emit('beforeOpen')
|
235
|
+
|
219
236
|
window.addEventListener('mousedown', this.onClickOutside)
|
220
237
|
|
221
238
|
const container = this.getContainer()
|
@@ -253,6 +270,13 @@ export default class ASearchSelect extends Mixins(ComponentWidthMixin, UsesPosit
|
|
253
270
|
this.$emit('close')
|
254
271
|
}
|
255
272
|
|
273
|
+
selectItem (model) {
|
274
|
+
if (this.closeOnSelect) {
|
275
|
+
this.close()
|
276
|
+
}
|
277
|
+
this.$emit('select', model)
|
278
|
+
}
|
279
|
+
|
256
280
|
selectHandler (model) {
|
257
281
|
return event => {
|
258
282
|
if (this.closeOnSelect) {
|
@@ -266,6 +290,7 @@ export default class ASearchSelect extends Mixins(ComponentWidthMixin, UsesPosit
|
|
266
290
|
|
267
291
|
coe_cancelOnEsc () {
|
268
292
|
this.close()
|
293
|
+
return false // stop esc propagation
|
269
294
|
}
|
270
295
|
|
271
296
|
onClickOutside (e) {
|
@@ -300,12 +325,42 @@ export default class ASearchSelect extends Mixins(ComponentWidthMixin, UsesPosit
|
|
300
325
|
const controls = this.popUp.querySelector('.controls')
|
301
326
|
const list = this.listPopUp.querySelector('.searchSelectList')
|
302
327
|
const padding = '.5rem'
|
328
|
+
const vPadding = this.showFilters ? '.5rem' : '0px'
|
303
329
|
|
304
330
|
const top = Math.min(0, list.offsetTop - controls.offsetTop)
|
305
331
|
background.style.left = `calc(0px - ${padding})`
|
306
|
-
background.style.top = `calc(${top}px - ${
|
332
|
+
background.style.top = `calc(${top}px - ${vPadding})`
|
307
333
|
background.style.width = `calc(${list.offsetWidth}px + 2 * ${padding})`
|
308
|
-
background.style.height = `calc(${controls.clientHeight}px + ${list.clientHeight}px +
|
334
|
+
background.style.height = `calc(${controls.clientHeight}px + ${list.clientHeight}px + 2 * ${padding} + ${vPadding})`
|
335
|
+
}
|
336
|
+
|
337
|
+
onLoad ({models, meta}) {
|
338
|
+
this.showFilters = meta.used_filters.page_size < meta.count_all
|
339
|
+
if (!this.showFilters) {
|
340
|
+
this.setFocusToList()
|
341
|
+
}
|
342
|
+
}
|
343
|
+
|
344
|
+
setFocusToList () {
|
345
|
+
this.$refs.list.setFocus()
|
346
|
+
}
|
347
|
+
|
348
|
+
setFocusToSearchInput () {
|
349
|
+
const searchInput = this.getSearchInput()
|
350
|
+
if (searchInput) {
|
351
|
+
searchInput.setFocus(true)
|
352
|
+
}
|
353
|
+
}
|
354
|
+
|
355
|
+
setWidth (width) {
|
356
|
+
this.cwm_width_ = width
|
357
|
+
}
|
358
|
+
|
359
|
+
searchFilterKeyDown (event) {
|
360
|
+
if (['Tab', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
|
361
|
+
this.setFocusToList()
|
362
|
+
event.preventDefault()
|
363
|
+
}
|
309
364
|
}
|
310
365
|
}
|
311
366
|
</script>
|
@@ -315,16 +370,23 @@ export default class ASearchSelect extends Mixins(ComponentWidthMixin, UsesPosit
|
|
315
370
|
.background {
|
316
371
|
background: white;
|
317
372
|
position: absolute;
|
318
|
-
z-index:
|
373
|
+
z-index: 300;
|
319
374
|
}
|
320
375
|
|
321
376
|
.controls {
|
322
377
|
width: 400px;
|
323
378
|
position: absolute;
|
324
|
-
z-index:
|
379
|
+
z-index: 301;
|
325
380
|
display: block;
|
381
|
+
|
326
382
|
padding: 0 .5rem;
|
327
383
|
|
384
|
+
> .searchSelectFilters {
|
385
|
+
padding: .5rem 0;
|
386
|
+
position: relative;
|
387
|
+
z-index: 302;
|
388
|
+
}
|
389
|
+
|
328
390
|
:deep(.a-row) {
|
329
391
|
overflow: unset;
|
330
392
|
}
|
@@ -344,6 +406,7 @@ export default class ASearchSelect extends Mixins(ComponentWidthMixin, UsesPosit
|
|
344
406
|
&:not(.selected) {
|
345
407
|
cursor: pointer;
|
346
408
|
}
|
409
|
+
|
347
410
|
&.selected {
|
348
411
|
pointer-events: none;
|
349
412
|
}
|
@@ -351,7 +414,13 @@ export default class ASearchSelect extends Mixins(ComponentWidthMixin, UsesPosit
|
|
351
414
|
}
|
352
415
|
|
353
416
|
.loadingIndicator {
|
354
|
-
|
417
|
+
z-index: 303;
|
418
|
+
margin: 0 -.5rem;
|
355
419
|
width: calc(100% + 1rem);
|
420
|
+
transition: none;
|
421
|
+
|
422
|
+
&.showFilters {
|
423
|
+
margin-top: -.5rem;
|
424
|
+
}
|
356
425
|
}
|
357
426
|
</style>
|
@@ -46,6 +46,7 @@ export default class ATableRow extends Vue {
|
|
46
46
|
> * {
|
47
47
|
padding: .15rem;
|
48
48
|
padding-right: 1.5rem;
|
49
|
+
|
49
50
|
&:last-child {
|
50
51
|
padding-right: .1rem;
|
51
52
|
}
|
@@ -54,8 +55,10 @@ export default class ATableRow extends Vue {
|
|
54
55
|
|
55
56
|
&.border {
|
56
57
|
border-bottom: 1px solid #E5E5E5;
|
58
|
+
|
57
59
|
> * {
|
58
60
|
padding: .4rem 1.5rem .4rem .4rem;
|
61
|
+
|
59
62
|
&:last-child {
|
60
63
|
padding-right: .4rem;
|
61
64
|
}
|
@@ -70,6 +73,10 @@ export default class ATableRow extends Vue {
|
|
70
73
|
background: #F4F4F4;
|
71
74
|
}
|
72
75
|
|
76
|
+
&.active {
|
77
|
+
background: #EEEEFF;
|
78
|
+
}
|
79
|
+
|
73
80
|
&:last-child {
|
74
81
|
border: none;
|
75
82
|
}
|
@@ -153,8 +153,8 @@ export default class ATextField extends Mixins(ComponentWidthMixin) {
|
|
153
153
|
this.setFocus()
|
154
154
|
}
|
155
155
|
|
156
|
-
setFocus () {
|
157
|
-
const focus = this.focus ||
|
156
|
+
setFocus (force = false) {
|
157
|
+
const focus = this.focus || force // set focus if this.focus or else if forced from outside
|
158
158
|
if (focus) {
|
159
159
|
// if run in a v-dialog, the dialog background would
|
160
160
|
// steal the focus without requestAnimationFrame
|
@@ -165,8 +165,8 @@ export default class ATextField extends Mixins(ComponentWidthMixin) {
|
|
165
165
|
}
|
166
166
|
|
167
167
|
get validationRules () {
|
168
|
-
if (this
|
169
|
-
return this
|
168
|
+
if (this.rules) {
|
169
|
+
return this.rules
|
170
170
|
}
|
171
171
|
const label = this.$attrs.label
|
172
172
|
return (this.validator && this.validator.getRules(label)) || []
|
@@ -1,5 +1,6 @@
|
|
1
1
|
<template>
|
2
2
|
<v-form
|
3
|
+
ref="form"
|
3
4
|
v-model="valid"
|
4
5
|
autocomplete="off"
|
5
6
|
@submit.prevent
|
@@ -33,7 +34,6 @@ export default class EditForm extends Vue {
|
|
33
34
|
lastJson = null
|
34
35
|
forcedUnchange = false
|
35
36
|
|
36
|
-
|
37
37
|
created () {
|
38
38
|
this.reset()
|
39
39
|
}
|
@@ -94,8 +94,8 @@ export default class EditForm extends Vue {
|
|
94
94
|
if (this.forcedUnchange) {
|
95
95
|
return false
|
96
96
|
}
|
97
|
-
// console.log(this.json)
|
98
97
|
// console.log(this.lastJson)
|
98
|
+
// console.log(this.json)
|
99
99
|
return this.json !== this.lastJson
|
100
100
|
}
|
101
101
|
|
@@ -1,42 +1,70 @@
|
|
1
1
|
<template>
|
2
|
-
<
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
2
|
+
<a-search-select
|
3
|
+
ref="select"
|
4
|
+
:listAction="listAction"
|
5
|
+
:selectedItems="selectedItems"
|
6
|
+
:getSearchInput="() => $refs.searchInput"
|
7
|
+
diffXControls="-.5rem"
|
8
|
+
diffYControls="-.5rem"
|
9
|
+
@select="itemSelected"
|
10
|
+
@close="focusInput"
|
11
|
+
@beforeOpen="calculateSelectorSize"
|
12
|
+
>
|
13
|
+
<template #activator="{open}">
|
14
|
+
<a-text-field
|
15
|
+
ref="input"
|
16
|
+
v-model="inputModel"
|
17
|
+
readonly
|
18
|
+
:label="label"
|
19
|
+
:rules="validationRules"
|
20
|
+
placeholder="Mausklick oder Space-Taste zum Auswählen"
|
21
|
+
@keydown.space.prevent="open"
|
22
|
+
@keydown.down.prevent="open"
|
23
|
+
@keydown.enter.prevent="open"
|
24
|
+
/>
|
25
|
+
</template>
|
26
|
+
|
27
|
+
<template #filters="{onSearchInputKey}">
|
28
|
+
<a-row gap="4">
|
29
|
+
<list-filter-search
|
30
|
+
ref="searchInput"
|
31
|
+
:focus="true"
|
32
|
+
tabindex="1"
|
33
|
+
maxWidth="100%"
|
34
|
+
:label="'Suche ' + label"
|
35
|
+
v-on="onSearchInputKey"
|
17
36
|
/>
|
18
|
-
</template>
|
19
37
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
:model="model"
|
24
|
-
:on="on"
|
38
|
+
<list-filter-page
|
39
|
+
:has="{page_size: false, page_number: true}"
|
40
|
+
:totalVisible="0"
|
25
41
|
/>
|
26
|
-
</
|
27
|
-
</
|
28
|
-
|
29
|
-
<
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
42
|
+
</a-row>
|
43
|
+
</template>
|
44
|
+
|
45
|
+
<template #row="{ model, on }">
|
46
|
+
<v-icon
|
47
|
+
:color="model.getIcon().color"
|
48
|
+
size="1.5rem"
|
49
|
+
class="mr-2"
|
50
|
+
v-on="on"
|
51
|
+
v-text="model.getIcon().icon"
|
52
|
+
/>
|
53
|
+
|
54
|
+
<div
|
55
|
+
style="width:100%;"
|
56
|
+
v-on="on"
|
57
|
+
>
|
58
|
+
{{ model.getTitle() }}
|
59
|
+
</div>
|
60
|
+
</template>
|
61
|
+
</a-search-select>
|
35
62
|
</template>
|
36
63
|
|
37
64
|
<script>
|
38
65
|
import { Component, Mixins } from '@a-vue'
|
39
66
|
import { FormFieldMixin } from '../FormFieldMixin'
|
67
|
+
import { ListAction } from '@a-vue/api-resources/ApiActions'
|
40
68
|
|
41
69
|
@Component({
|
42
70
|
props: ['value', 'q', 'listConfig']
|
@@ -44,8 +72,43 @@ import { FormFieldMixin } from '../FormFieldMixin'
|
|
44
72
|
export default class FormFieldSearchSelect extends Mixins(FormFieldMixin) {
|
45
73
|
mounted () {
|
46
74
|
if (this.validator) {
|
47
|
-
this.$refs.input.validate(
|
75
|
+
this.$refs.input.validate()
|
48
76
|
}
|
49
77
|
}
|
78
|
+
|
79
|
+
get inputModel () {
|
80
|
+
return (this.model[this.name] && this.model[this.name].getTitle()) || null
|
81
|
+
}
|
82
|
+
|
83
|
+
calculateSelectorSize () {
|
84
|
+
const input = this.$refs.input.$el
|
85
|
+
const inputWidth = input.offsetWidth
|
86
|
+
this.$refs.select.setWidth(`calc(${inputWidth}px + 1rem)`)
|
87
|
+
}
|
88
|
+
|
89
|
+
get selectedItems () {
|
90
|
+
return [this.model[this.name]].filter(a => a)
|
91
|
+
}
|
92
|
+
|
93
|
+
get listAction () {
|
94
|
+
return ListAction.fromRequest(this.field.getOptionsRequest())
|
95
|
+
}
|
96
|
+
|
97
|
+
itemSelected (model) {
|
98
|
+
this.model[this.name] = model
|
99
|
+
this.$emit('select', model)
|
100
|
+
}
|
101
|
+
|
102
|
+
focusInput () {
|
103
|
+
this.$refs.input.setFocus(true)
|
104
|
+
}
|
50
105
|
}
|
51
106
|
</script>
|
107
|
+
|
108
|
+
|
109
|
+
<style lang="scss" scoped>
|
110
|
+
.selectedItem,
|
111
|
+
:deep(.a-table-row) {
|
112
|
+
cursor: pointer;
|
113
|
+
}
|
114
|
+
</style>
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { Component, Vue, Watch } from '@a-vue'
|
2
2
|
import { ListAction } from '@a-vue/api-resources/ApiActions'
|
3
|
+
import { sleep } from '@a-vue/utils/timeout'
|
3
4
|
import { ListViewModel } from '@afeefa/api-resources-client'
|
4
5
|
|
5
6
|
import { CurrentRouteFilterSource } from './CurrentRouteFilterSource'
|
@@ -10,10 +11,10 @@ import { FilterSourceType } from './FilterSourceType'
|
|
10
11
|
'models', 'meta', // given, if already loaded
|
11
12
|
'listAction',
|
12
13
|
'filterHistoryKey',
|
13
|
-
'loadOnlyIfKeyword',
|
14
14
|
'checkBeforeLoad',
|
15
15
|
{
|
16
16
|
filterSource: FilterSourceType.QUERY_STRING,
|
17
|
+
loadOnlyIfKeyword: false,
|
17
18
|
events: true,
|
18
19
|
history: true
|
19
20
|
}
|
@@ -75,6 +76,8 @@ export class ListViewMixin extends Vue {
|
|
75
76
|
models: this.models_,
|
76
77
|
meta: this.meta_
|
77
78
|
})
|
79
|
+
|
80
|
+
this._listLoaded()
|
78
81
|
} else {
|
79
82
|
this.load()
|
80
83
|
}
|
@@ -96,6 +99,9 @@ export class ListViewMixin extends Vue {
|
|
96
99
|
_filtersInitialized () {
|
97
100
|
}
|
98
101
|
|
102
|
+
_listLoaded () {
|
103
|
+
}
|
104
|
+
|
99
105
|
filtersChanged () {
|
100
106
|
this.load()
|
101
107
|
}
|
@@ -116,10 +122,6 @@ export class ListViewMixin extends Vue {
|
|
116
122
|
return this.meta_.count_search || 0
|
117
123
|
}
|
118
124
|
|
119
|
-
get _loadOnlyIfKeyword () {
|
120
|
-
return this.loadOnlyIfKeyword
|
121
|
-
}
|
122
|
-
|
123
125
|
async load () {
|
124
126
|
if (this.checkBeforeLoad) {
|
125
127
|
const canLoad = await this.checkBeforeLoad()
|
@@ -131,7 +133,7 @@ export class ListViewMixin extends Vue {
|
|
131
133
|
}
|
132
134
|
}
|
133
135
|
|
134
|
-
if (this.
|
136
|
+
if (this.loadOnlyIfKeyword && !this.filters.q.value) {
|
135
137
|
this.models_ = []
|
136
138
|
this.meta_ = {}
|
137
139
|
this.$emit('update:count', 0)
|
@@ -148,6 +150,8 @@ export class ListViewMixin extends Vue {
|
|
148
150
|
.dispatchGlobalLoadingEvents(this.events)
|
149
151
|
.load()
|
150
152
|
|
153
|
+
// await sleep(1)
|
154
|
+
|
151
155
|
if (!models) { // error happened
|
152
156
|
this.isLoading = false
|
153
157
|
this.$emit('update:isLoading', false)
|
@@ -170,5 +174,7 @@ export class ListViewMixin extends Vue {
|
|
170
174
|
models,
|
171
175
|
meta
|
172
176
|
})
|
177
|
+
|
178
|
+
this._listLoaded()
|
173
179
|
}
|
174
180
|
}
|
@@ -1,5 +1,6 @@
|
|
1
1
|
<template>
|
2
2
|
<a-text-field
|
3
|
+
ref="input"
|
3
4
|
v-model="filter.value"
|
4
5
|
:maxWidth="$attrs.maxWidth || maxWidth_"
|
5
6
|
:label="label || 'Suche'"
|
@@ -8,6 +9,7 @@
|
|
8
9
|
clearable
|
9
10
|
hide-details
|
10
11
|
@keyup.esc="clearValue"
|
12
|
+
v-on="$listeners"
|
11
13
|
/>
|
12
14
|
</template>
|
13
15
|
|
@@ -27,5 +29,9 @@ export default class ListFilterSearch extends Mixins(ListFilterMixin) {
|
|
27
29
|
this.filter.value = null
|
28
30
|
}
|
29
31
|
}
|
32
|
+
|
33
|
+
setFocus (force) {
|
34
|
+
this.$refs.input.setFocus(force)
|
35
|
+
}
|
30
36
|
}
|
31
37
|
</script>
|
@@ -1,5 +1,14 @@
|
|
1
1
|
<template>
|
2
|
-
<div
|
2
|
+
<div
|
3
|
+
:class="['searchSelectList', {isLoading}]"
|
4
|
+
tabindex="0"
|
5
|
+
@keydown.down.prevent="keydown"
|
6
|
+
@keydown.up.prevent="keyup"
|
7
|
+
@keydown.enter="keyenter"
|
8
|
+
@keydown.tab.prevent.exact="keyenter"
|
9
|
+
@keydown.space.prevent="keyenter"
|
10
|
+
@keydown.tab.shift.prevent="$emit('backtab')"
|
11
|
+
>
|
3
12
|
<template v-if="models_.length">
|
4
13
|
<a-table v-bind="$attrs">
|
5
14
|
<a-table-header
|
@@ -10,10 +19,10 @@
|
|
10
19
|
</a-table-header>
|
11
20
|
|
12
21
|
<a-table-row
|
13
|
-
v-for="model in models_"
|
22
|
+
v-for="(model, index) in models_"
|
14
23
|
:key="model.id"
|
15
24
|
small
|
16
|
-
:class="{selected: isSelected(model)}"
|
25
|
+
:class="['row-' + index, {selected: isSelected(model), active: activeModelIndex === index}]"
|
17
26
|
>
|
18
27
|
<slot
|
19
28
|
name="row"
|
@@ -46,13 +55,15 @@ import { ListViewMixin } from '@a-vue/components/list/ListViewMixin'
|
|
46
55
|
props: ['q', 'selectedItems']
|
47
56
|
})
|
48
57
|
export default class SearchSelectList extends Mixins(ListViewMixin) {
|
58
|
+
activeModelIndex = -1
|
59
|
+
|
49
60
|
get hasHeader () {
|
50
61
|
return this.$slots.header && this.$slots.header.length > 1
|
51
62
|
}
|
52
63
|
|
53
64
|
get showNotFound () {
|
54
65
|
if (!this.models_.length && !this.isLoading) {
|
55
|
-
if (this.
|
66
|
+
if (this.loadOnlyIfKeyword && !this.filters.q.value) {
|
56
67
|
return false
|
57
68
|
}
|
58
69
|
return true
|
@@ -68,11 +79,67 @@ export default class SearchSelectList extends Mixins(ListViewMixin) {
|
|
68
79
|
this.filters.q.value = this.q
|
69
80
|
}
|
70
81
|
}
|
82
|
+
|
83
|
+
setFocus () {
|
84
|
+
this.$el.focus()
|
85
|
+
|
86
|
+
if (this.models_.length) {
|
87
|
+
this.activeModelIndex = Math.max(0, this.findActiveIndexForSelectedModel())
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
keydown () {
|
92
|
+
this.activeModelIndex++
|
93
|
+
if (this.activeModelIndex > this.models_.length - 1) {
|
94
|
+
this.activeModelIndex = 0
|
95
|
+
}
|
96
|
+
|
97
|
+
const row = this.$el.querySelector('.row-' + this.activeModelIndex)
|
98
|
+
if (row) {
|
99
|
+
row.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
keyup () {
|
104
|
+
this.activeModelIndex--
|
105
|
+
if (this.activeModelIndex < 0) {
|
106
|
+
this.activeModelIndex = this.models_.length - 1
|
107
|
+
}
|
108
|
+
|
109
|
+
const row = this.$el.querySelector('.row-' + this.activeModelIndex)
|
110
|
+
if (row) {
|
111
|
+
row.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
keyenter () {
|
116
|
+
const model = this.models_[this.activeModelIndex]
|
117
|
+
if (model) {
|
118
|
+
this.$emit('enter', model)
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
_listLoaded () {
|
123
|
+
this.activeModelIndex = this.findActiveIndexForSelectedModel()
|
124
|
+
}
|
125
|
+
|
126
|
+
findActiveIndexForSelectedModel () {
|
127
|
+
for (const [index, model] of this.models_.entries()) {
|
128
|
+
if (this.isSelected(model)) {
|
129
|
+
return index
|
130
|
+
}
|
131
|
+
}
|
132
|
+
return -1
|
133
|
+
}
|
71
134
|
}
|
72
135
|
</script>
|
73
136
|
|
74
137
|
|
75
138
|
<style scoped lang="scss">
|
139
|
+
.searchSelectList {
|
140
|
+
outline: none;
|
141
|
+
}
|
142
|
+
|
76
143
|
.isLoading {
|
77
144
|
opacity: .6;
|
78
145
|
}
|
@@ -13,7 +13,7 @@ import { randomCssClass } from '@a-vue/utils/random'
|
|
13
13
|
import { CancelOnEscMixin } from '@a-vue/services/escape/CancelOnEscMixin'
|
14
14
|
|
15
15
|
@Component({
|
16
|
-
props: [{show: false}]
|
16
|
+
props: [{show: false}, 'beforeClose']
|
17
17
|
})
|
18
18
|
export default class FlyingContext extends Mixins(CancelOnEscMixin) {
|
19
19
|
isVisible = false
|
@@ -62,8 +62,15 @@ export default class FlyingContext extends Mixins(CancelOnEscMixin) {
|
|
62
62
|
this.$events.off(FlyingContextEvent.HIDE_ALL, this.onHide)
|
63
63
|
}
|
64
64
|
|
65
|
-
onHide () {
|
65
|
+
async onHide () {
|
66
66
|
if (this.isVisible) {
|
67
|
+
if (this.beforeClose) {
|
68
|
+
const result = await this.beforeClose()
|
69
|
+
if (!result) {
|
70
|
+
return
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
67
74
|
this.$el.appendChild(this.getContent())
|
68
75
|
this.coe_unwatchCancel() // hide context -> do not watch esc any more
|
69
76
|
this.isVisible = false
|
@@ -45,6 +45,12 @@ export default class FlyingContextContainer extends Vue {
|
|
45
45
|
domChanged () {
|
46
46
|
const container = this.getChildrenContainer()
|
47
47
|
this.visible = !!container.children.length
|
48
|
+
|
49
|
+
if (this.visible) {
|
50
|
+
document.documentElement.style.overflow = 'hidden'
|
51
|
+
} else {
|
52
|
+
document.documentElement.style.overflow = 'auto'
|
53
|
+
}
|
48
54
|
}
|
49
55
|
|
50
56
|
hide () {
|
@@ -58,6 +64,10 @@ export default class FlyingContextContainer extends Vue {
|
|
58
64
|
}
|
59
65
|
|
60
66
|
onClickOutside (e) {
|
67
|
+
if (!this.visible) {
|
68
|
+
return
|
69
|
+
}
|
70
|
+
|
61
71
|
// check if trigger is clicked
|
62
72
|
let parent = e.target
|
63
73
|
while (parent) {
|
@@ -100,8 +110,8 @@ export default class FlyingContextContainer extends Vue {
|
|
100
110
|
}
|
101
111
|
|
102
112
|
.v-navigation-drawer__border {
|
103
|
-
|
104
|
-
|
113
|
+
background-color: rgba(0, 0, 0, .12);
|
114
|
+
left: 0;
|
105
115
|
}
|
106
116
|
|
107
117
|
.closeButton {
|
package/src-admin/styles.scss
CHANGED