@afeefa/vue-app 0.0.121 → 0.0.123
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/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