@afeefa/vue-app 0.0.342 → 0.0.344
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/ASelect2.vue +129 -57
- package/src/components/list/ListFilterBar.vue +6 -5
- package/src/components/list/ListFilterMixin.js +9 -1
- package/src/components/list/filters/ListFilterSelect2.vue +59 -23
- package/src/components/select2/Select2Chip.vue +4 -1
- package/src/components/select2/Select2List.vue +32 -0
- package/src/services/select-cache/Select2Cache.js +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.344
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
<v-select
|
|
15
15
|
ref="field"
|
|
16
16
|
class="field"
|
|
17
|
+
:title="committedTooltip"
|
|
17
18
|
:value="committedModels"
|
|
18
19
|
:items="committedModels"
|
|
19
20
|
multiple
|
|
@@ -52,19 +53,26 @@
|
|
|
52
53
|
</template>
|
|
53
54
|
</v-select>
|
|
54
55
|
|
|
55
|
-
<!-- z-index
|
|
56
|
-
|
|
56
|
+
<!-- z-index wie ASearchSelect (Overlay 299, Popup 300): über dem
|
|
57
|
+
flying-context (200), damit das Popup nicht dahinter verschwindet. -->
|
|
57
58
|
<v-overlay
|
|
58
59
|
:value="isOpen"
|
|
59
|
-
:z-index="
|
|
60
|
+
:z-index="299"
|
|
60
61
|
:opacity="0"
|
|
61
62
|
/>
|
|
62
63
|
|
|
63
64
|
<!-- Ein-Popup: ein Panel mit (optionalem) Tabs + Suchfeld + Trefferliste -->
|
|
64
65
|
<div :class="panelCssClass">
|
|
66
|
+
<!-- v-menu__content--active markiert das Popup als Stack-Teilnehmer für
|
|
67
|
+
Vuetify: dessen getMaxZIndex() (mixins/stackable) scannt nur Elemente
|
|
68
|
+
mit dieser bzw. der v-dialog-Klasse und liest deren z-index. Da das
|
|
69
|
+
Popup ein eigenes div (kein echtes v-menu) ist, „faken" wir die Klasse
|
|
70
|
+
hier — sie hat in Vuetify keine weitere Wirkung außer dem Stack-Scan.
|
|
71
|
+
Folge: ein aus dem Popup geöffneter Dialog bekommt max(200, 300)+2 =
|
|
72
|
+
302 und liegt korrekt ÜBER dem Popup (wie ein Dialog aus EditModal). -->
|
|
65
73
|
<div
|
|
66
74
|
v-if="isOpen"
|
|
67
|
-
:class="['popup elevation-6', 'open-' + openDirection]"
|
|
75
|
+
:class="['popup elevation-6 v-menu__content--active', 'open-' + openDirection]"
|
|
68
76
|
:style="cwm_popupWidthStyle"
|
|
69
77
|
>
|
|
70
78
|
<!-- Linearer Lade-Spinner am oberen Popup-Rand (wie Select1), während
|
|
@@ -91,7 +99,6 @@
|
|
|
91
99
|
@input="onSearchInput"
|
|
92
100
|
@focus="activeTab = 'search'"
|
|
93
101
|
@keydown.native.down.prevent="focusList"
|
|
94
|
-
@keydown.native.esc.prevent="onSearchEsc"
|
|
95
102
|
@keydown.native.enter.ctrl.prevent="applyIfMultiple"
|
|
96
103
|
>
|
|
97
104
|
<!-- Anzahl-Chip innen rechts im Suchfeld: schaltet auf die
|
|
@@ -123,6 +130,7 @@
|
|
|
123
130
|
:getIcon="getIcon"
|
|
124
131
|
:isItemDisabled="isItemDisabled"
|
|
125
132
|
:allowExclude="allowExclude"
|
|
133
|
+
:showCheckbox="multiple && !behavesSingle"
|
|
126
134
|
:isLoading="isLoading"
|
|
127
135
|
:hasMore="hasMore"
|
|
128
136
|
@toggle="onRowClick"
|
|
@@ -154,6 +162,7 @@
|
|
|
154
162
|
:getSubtitle="getSubtitle"
|
|
155
163
|
:getIcon="getIcon"
|
|
156
164
|
:allowExclude="allowExclude"
|
|
165
|
+
:showCheckbox="multiple && !behavesSingle"
|
|
157
166
|
@toggle="onRowClick"
|
|
158
167
|
@exclude="onExcludeClick"
|
|
159
168
|
>
|
|
@@ -167,19 +176,28 @@
|
|
|
167
176
|
</template>
|
|
168
177
|
</select2-list>
|
|
169
178
|
|
|
170
|
-
<!-- Footer (nur Mehrfachauswahl): Übernehmen / Verwerfen
|
|
179
|
+
<!-- Footer (nur echte Mehrfachauswahl): Übernehmen / Verwerfen. Entfällt
|
|
180
|
+
bei behavesSingle (nur 1 Eintrag) — dann committet der Klick sofort.
|
|
181
|
+
Erst NACH dem Laden zeigen: vorher steht nicht fest, ob es bei 1 Eintrag
|
|
182
|
+
bleibt (behavesSingle) — sonst flackerte der Footer beim Öffnen. -->
|
|
171
183
|
<div
|
|
172
|
-
v-if="
|
|
184
|
+
v-if="showFooter"
|
|
173
185
|
class="footer"
|
|
174
186
|
>
|
|
187
|
+
<!-- Kompakt (passt auch ins schmale Popup, §1): Verwerfen = X-Icon,
|
|
188
|
+
Übernehmen = "OK". -->
|
|
175
189
|
<v-btn
|
|
176
190
|
x-small
|
|
177
191
|
text
|
|
192
|
+
icon
|
|
178
193
|
class="footerBtn"
|
|
179
194
|
:tabindex="-1"
|
|
195
|
+
title="Verwerfen"
|
|
180
196
|
@click="discardAndClose"
|
|
181
197
|
>
|
|
182
|
-
|
|
198
|
+
<v-icon small>
|
|
199
|
+
$closeIcon
|
|
200
|
+
</v-icon>
|
|
183
201
|
</v-btn>
|
|
184
202
|
<v-btn
|
|
185
203
|
x-small
|
|
@@ -187,13 +205,7 @@
|
|
|
187
205
|
color="green white--text"
|
|
188
206
|
@click="apply"
|
|
189
207
|
>
|
|
190
|
-
|
|
191
|
-
left
|
|
192
|
-
class="mr-1"
|
|
193
|
-
>
|
|
194
|
-
$checkIcon
|
|
195
|
-
</v-icon>
|
|
196
|
-
Übernehmen
|
|
208
|
+
OK
|
|
197
209
|
</v-btn>
|
|
198
210
|
</div>
|
|
199
211
|
</div>
|
|
@@ -259,7 +271,7 @@ import Select2List from './select2/Select2List'
|
|
|
259
271
|
// auf [popupMinWidth, popupMaxWidth] (px) begrenzt. null = keine Grenze.
|
|
260
272
|
// Eigene Namen statt minWidth/maxWidth, weil ComponentWidthMixin maxWidth
|
|
261
273
|
// bereits für die FELD-Breite belegt (Kollision vermeiden).
|
|
262
|
-
// Default
|
|
274
|
+
// Default 150px Mindestbreite, damit schmale Filter-Felder kein Mini-Popup
|
|
263
275
|
// bekommen.
|
|
264
276
|
popupMinWidth: 150,
|
|
265
277
|
popupMaxWidth: null
|
|
@@ -285,13 +297,16 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
285
297
|
// Mehrfachauswahl-Tabs (Etappe 3)
|
|
286
298
|
activeTab = 'search'
|
|
287
299
|
|
|
288
|
-
// Dynamische Suche (Etappe 2)
|
|
289
|
-
search =
|
|
300
|
+
// Dynamische Suche (Etappe 2). Leer = `null` (nicht ''), siehe onSearchInput.
|
|
301
|
+
search = null
|
|
290
302
|
searchResults = []
|
|
291
303
|
page = 1
|
|
292
304
|
hasMore = false
|
|
293
305
|
count = null
|
|
294
|
-
//
|
|
306
|
+
// Gesamtzahl der ungefilterten Liste (Trefferzahl ohne Suchbegriff) — steuert
|
|
307
|
+
// showSearchField stabil, unabhängig vom aktuellen Such-`count`.
|
|
308
|
+
totalCount = null
|
|
309
|
+
// Wurde mindestens eine Seite geladen? Erst danach ist `totalCount` bekannt
|
|
295
310
|
// (steuert, ob das Suchfeld bei einseitigen Listen entfällt — siehe showSearchField).
|
|
296
311
|
loaded = false
|
|
297
312
|
isLoading = false
|
|
@@ -351,18 +366,21 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
351
366
|
// Liste passt nicht auf eine Seite (`hasMore`). Bei einer einseitigen Liste
|
|
352
367
|
// (z.B. 8 Auftragsstatus) ist Suchen sinnlos → kein Suchfeld.
|
|
353
368
|
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
// - Noch nichts geladen (`!loaded`):
|
|
359
|
-
//
|
|
360
|
-
//
|
|
369
|
+
// Maßstab ist die GESAMTzahl der ungefilterten Liste (`totalCount`), nicht der
|
|
370
|
+
// aktuelle Such-`count` und nicht `hasMore`: beide fallen während einer Suche
|
|
371
|
+
// (0 Treffer → hasMore false) und flackern beim Reload. `totalCount` bleibt
|
|
372
|
+
// stabil. Sonderfälle:
|
|
373
|
+
// - Noch nichts geladen (`!loaded`): KEIN Feld — die Items erscheinen ohnehin
|
|
374
|
+
// erst mit der ersten Antwort; das Feld kommt dann (falls nötig) zusammen mit
|
|
375
|
+
// der Liste. Vorher kurz eins zu zeigen, das bei kurzen Listen gleich wieder
|
|
376
|
+
// verschwindet, wäre Flackern.
|
|
377
|
+
// - Schon getippt (`search`): das Feld muss bleiben, sonst lässt sich die Suche
|
|
378
|
+
// nicht mehr ändern/leeren.
|
|
361
379
|
get showSearchField () {
|
|
362
|
-
if (!this.isDynamic || !this.hasSearch) {
|
|
380
|
+
if (!this.isDynamic || !this.hasSearch || !this.loaded) {
|
|
363
381
|
return false
|
|
364
382
|
}
|
|
365
|
-
return this.
|
|
383
|
+
return !!this.search || this.totalCount > this.pageSize
|
|
366
384
|
}
|
|
367
385
|
|
|
368
386
|
// Such-Label wie bei ASearchSelect: "Suche <label>" (z.B. "Suche SpraMi").
|
|
@@ -469,6 +487,13 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
469
487
|
this.searchResults = reset ? models : [...this.searchResults, ...models]
|
|
470
488
|
this.count = meta.count_search != null ? meta.count_search : this.searchResults.length
|
|
471
489
|
this.hasMore = this.searchResults.length < this.count
|
|
490
|
+
// Gesamtzahl OHNE Suchbegriff (aber mit aktiven Filtern) — steht in jeder
|
|
491
|
+
// Antwort als `count_filter`, auch während einer 0-Treffer-Suche. Steuert
|
|
492
|
+
// stabil, ob die Liste ein Suchfeld braucht (anders als `count`, das bei einer
|
|
493
|
+
// Suche auf die Trefferzahl fällt). Fallback auf count_all bzw. count.
|
|
494
|
+
this.totalCount = meta.count_filter != null
|
|
495
|
+
? meta.count_filter
|
|
496
|
+
: (meta.count_all != null ? meta.count_all : this.count)
|
|
472
497
|
this.loaded = true
|
|
473
498
|
|
|
474
499
|
this.isLoading = false
|
|
@@ -502,7 +527,9 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
502
527
|
// Eintrag zeigen. Bezieht Felder/Filter/Suche/Seite ein.
|
|
503
528
|
cacheKey (page) {
|
|
504
529
|
return buildRequestCacheKey(this.optionsRequest(), {
|
|
505
|
-
|
|
530
|
+
// Leere Suche als '' (nicht null) in den Key — sonst zeigt der Lookup auf
|
|
531
|
+
// einen anderen Eintrag als die main.js-Vorwärmung (die mit q:'' primt).
|
|
532
|
+
q: this.search || '',
|
|
506
533
|
page,
|
|
507
534
|
pageSize: this.pageSize
|
|
508
535
|
})
|
|
@@ -510,7 +537,7 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
510
537
|
|
|
511
538
|
// Suche + Seite als Filter (für Request und Cache-Key gleichermaßen).
|
|
512
539
|
pageFilters (page) {
|
|
513
|
-
return { q: this.search, page, page_size: this.pageSize }
|
|
540
|
+
return { q: this.search || '', page, page_size: this.pageSize }
|
|
514
541
|
}
|
|
515
542
|
|
|
516
543
|
// Baut die ListAction der Datenquelle und setzt Suche + Seite als Filter.
|
|
@@ -524,11 +551,28 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
524
551
|
|
|
525
552
|
// --- Auswahl-Helfer ------------------------------------------------------
|
|
526
553
|
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
554
|
+
// Mehrfachauswahl, die sich aber wie Single bedient: wenn nur EIN Eintrag
|
|
555
|
+
// wählbar ist (vollständig geladen, `displayItems.length === 1`), ist eine
|
|
556
|
+
// Draft-/Übernehmen-Bar sinnlos — ein Klick wählt und schließt sofort, kein
|
|
557
|
+
// Footer. Das **Wertformat** bleibt am Prop `multiple` hängen (toOuter/fromOuter
|
|
558
|
+
// unverändert, sonst bräche die Filter-Hülle, die ein Array erwartet) — nur das
|
|
559
|
+
// Bedien-VERHALTEN kippt.
|
|
560
|
+
get behavesSingle () {
|
|
561
|
+
return this.multiple && !this.hasMore && this.displayItems.length === 1
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Footer (X/OK) nur bei echter Mehrfachauswahl UND erst, wenn die Items geladen
|
|
565
|
+
// sind: bei dynamischer Quelle vor dem ersten Laden noch nicht (da ist weder die
|
|
566
|
+
// Trefferliste noch behavesSingle entschieden) — bei fester Liste sofort.
|
|
567
|
+
get showFooter () {
|
|
568
|
+
return this.multiple && !this.behavesSingle && (!this.isDynamic || this.loaded)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Die im Popup aktive Auswahl: Mehrfach → Draft (Arbeitskopie), Einfach (auch
|
|
572
|
+
// behavesSingle) → committed (Klick committet sofort, kein Draft nötig). Liste +
|
|
573
|
+
// Chips lesen hierüber, damit beide Modi denselben Code teilen.
|
|
530
574
|
get activeSelection () {
|
|
531
|
-
return this.multiple ? this.draft : this.committed
|
|
575
|
+
return this.multiple && !this.behavesSingle ? this.draft : this.committed
|
|
532
576
|
}
|
|
533
577
|
|
|
534
578
|
get sortedSelection () {
|
|
@@ -543,6 +587,18 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
543
587
|
return this.sortedSelection.map(e => e.model)
|
|
544
588
|
}
|
|
545
589
|
|
|
590
|
+
// Hover-Titel (natives title-Attribut) am Feld: ALLE gewählten Items, eins pro
|
|
591
|
+
// Zeile (das Feld selbst zeigt nur den ersten Namen + "+N"). Ausschluss mit
|
|
592
|
+
// "nicht " markiert, wie im Feld-Text. Leer → kein Tooltip.
|
|
593
|
+
get committedTooltip () {
|
|
594
|
+
if (!this.sortedSelection.length) {
|
|
595
|
+
return null
|
|
596
|
+
}
|
|
597
|
+
return this.sortedSelection
|
|
598
|
+
.map(e => (e.polarity === 'exclude' ? 'nicht ' : '') + this.itemTitle(e.model))
|
|
599
|
+
.join('\n')
|
|
600
|
+
}
|
|
601
|
+
|
|
546
602
|
// Polarität aus der committed-Auswahl (Feld-Chips zeigen den bestätigten Stand,
|
|
547
603
|
// nicht den Popup-Draft — anders als polarityOf, das den aktiven Draft liest).
|
|
548
604
|
committedPolarityOf (model) {
|
|
@@ -576,9 +632,10 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
576
632
|
|
|
577
633
|
// --- Tabs / Auswahl-Tab --------------------------------------------------
|
|
578
634
|
|
|
579
|
-
// Es gibt etwas Auswählbares zum Umschalten (Auswahl-Chip sichtbar).
|
|
635
|
+
// Es gibt etwas Auswählbares zum Umschalten (Auswahl-Chip sichtbar). Nicht bei
|
|
636
|
+
// behavesSingle (nur 1 Eintrag, kein Mehrfach-Bedienmodell).
|
|
580
637
|
get showTabs () {
|
|
581
|
-
return this.multiple && this.activeSelection.length > 0
|
|
638
|
+
return this.multiple && !this.behavesSingle && this.activeSelection.length > 0
|
|
582
639
|
}
|
|
583
640
|
|
|
584
641
|
// Models der aktuellen Auswahl (für die Auswahl-Ansicht).
|
|
@@ -628,15 +685,17 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
628
685
|
}
|
|
629
686
|
|
|
630
687
|
// Schreibt die neue Auswahl: Mehrfach → in den Draft, Popup bleibt offen;
|
|
631
|
-
// Einfach → sofort committen und schließen.
|
|
688
|
+
// Einfach (auch behavesSingle: nur 1 Eintrag) → sofort committen und schließen.
|
|
689
|
+
// Fokus zurück aufs Feld (wie der apply-Pfad bei Mehrfach), damit nach Klick/Enter/
|
|
690
|
+
// Space die Komponente fokussiert bleibt und nicht auf body fällt.
|
|
632
691
|
commitSelection (next) {
|
|
633
|
-
if (this.multiple) {
|
|
692
|
+
if (this.multiple && !this.behavesSingle) {
|
|
634
693
|
this.draft = next
|
|
635
694
|
return
|
|
636
695
|
}
|
|
637
696
|
this.committed = next
|
|
638
697
|
this.emitChange()
|
|
639
|
-
this.close()
|
|
698
|
+
this.close({ returnFocus: true })
|
|
640
699
|
}
|
|
641
700
|
|
|
642
701
|
withItem (selection, model, polarity) {
|
|
@@ -675,20 +734,13 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
675
734
|
// Tippen im Suchfeld wechselt zurück in die Trefferliste (falls gerade die
|
|
676
735
|
// Auswahl-Ansicht aktiv ist) — sonst sucht man, sieht aber die Auswahl.
|
|
677
736
|
onSearchInput (value) {
|
|
678
|
-
|
|
737
|
+
// Leere Suche ist intern `null` (a-text-field mit `clearable` emittiert beim
|
|
738
|
+
// ×-Klick ohnehin `null`). Alle Lese-Stellen nutzen `!this.search` (truthy),
|
|
739
|
+
// sind also null-fest; `q` im Request fällt damit weg statt leerem String.
|
|
740
|
+
this.search = value || null
|
|
679
741
|
this.activeTab = 'search'
|
|
680
742
|
}
|
|
681
743
|
|
|
682
|
-
// Esc im Suchfeld: mit Text → Suche leeren (offen bleiben); leer → schließen.
|
|
683
|
-
onSearchEsc () {
|
|
684
|
-
if (this.search) {
|
|
685
|
-
this.search = ''
|
|
686
|
-
return
|
|
687
|
-
}
|
|
688
|
-
// Esc = bewusst abbrechen, aber im Feld bleiben (Fokus zurück).
|
|
689
|
-
this.close({ returnFocus: true })
|
|
690
|
-
}
|
|
691
|
-
|
|
692
744
|
applyIfMultiple () {
|
|
693
745
|
if (this.multiple) {
|
|
694
746
|
this.apply()
|
|
@@ -790,13 +842,13 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
790
842
|
// wirkt (§4). Die committed-Auswahl bleibt unberührt; nur der flüchtige Such-
|
|
791
843
|
// String/die Trefferliste werden neu aufgebaut. Der cacheResults-Cache greift
|
|
792
844
|
// davon unabhängig weiter (gleicher Key → aus dem Cache).
|
|
793
|
-
// search=
|
|
845
|
+
// search=null kann den @Watch('search') auslösen; der würde einen zweiten,
|
|
794
846
|
// debounced Request anstoßen. Daher den Watcher für diesen einen Reset
|
|
795
847
|
// unterdrücken (nur wenn search sich wirklich ändert) und EINMAL direkt laden.
|
|
796
848
|
if (this.isDynamic) {
|
|
797
849
|
if (this.search) {
|
|
798
850
|
this.suppressSearchWatch = true
|
|
799
|
-
this.search =
|
|
851
|
+
this.search = null
|
|
800
852
|
}
|
|
801
853
|
// Fokus erst NACH dem ersten Laden setzen: vorher ist `showSearchField`
|
|
802
854
|
// noch unentschieden (loaded=false ⇒ true), und bei einer einseitigen Liste
|
|
@@ -859,8 +911,10 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
859
911
|
return
|
|
860
912
|
}
|
|
861
913
|
|
|
862
|
-
// Mehrfachauswahl mit ungespeicherten Draft-Änderungen → rückfragen.
|
|
863
|
-
|
|
914
|
+
// Mehrfachauswahl mit ungespeicherten Draft-Änderungen → rückfragen. Nicht bei
|
|
915
|
+
// behavesSingle (nur 1 Item): der Klick committet dort sofort, es gibt keinen
|
|
916
|
+
// Draft zu verwerfen.
|
|
917
|
+
if (!skipDiscardCheck && this.multiple && !this.behavesSingle && this.isDirty) {
|
|
864
918
|
const result = await this.$events.dispatch(new DialogEvent(DialogEvent.SHOW, {
|
|
865
919
|
title: 'Änderungen verwerfen?',
|
|
866
920
|
message: 'Die Auswahl wurde geändert. Sollen die Änderungen verworfen werden?',
|
|
@@ -970,7 +1024,17 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
970
1024
|
}
|
|
971
1025
|
|
|
972
1026
|
coe_cancelOnEsc () {
|
|
973
|
-
// Esc
|
|
1027
|
+
// EINZIGER Esc-Pfad (window-keyup, kaskadierend). Mit Suchtext: nur leeren,
|
|
1028
|
+
// Popup bleibt offen (§14). Sonst: schließen, Fokus zurück. Früher gab es
|
|
1029
|
+
// zusätzlich einen @keydown.esc am Suchfeld → Esc leerte UND schloss, weil
|
|
1030
|
+
// keydown (Feld) und keyup (dieser Mixin) zwei getrennte Events sind.
|
|
1031
|
+
if (this.search) {
|
|
1032
|
+
// Suche leeren — der @Watch('search') lädt (debounced) neu. Das Suchfeld
|
|
1033
|
+
// bleibt dabei stabil sichtbar, weil showSearchField an `totalCount` hängt
|
|
1034
|
+
// (Gesamtzahl ohne Suchbegriff), nicht am wegfallenden Such-`count`.
|
|
1035
|
+
this.search = null
|
|
1036
|
+
return false // stop esc propagation
|
|
1037
|
+
}
|
|
974
1038
|
this.close({ returnFocus: true })
|
|
975
1039
|
return false // stop esc propagation
|
|
976
1040
|
}
|
|
@@ -1053,7 +1117,15 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
1053
1117
|
[class*=" select2Panel-"] {
|
|
1054
1118
|
.popup {
|
|
1055
1119
|
position: absolute;
|
|
1056
|
-
z-index:
|
|
1120
|
+
z-index: 300;
|
|
1121
|
+
|
|
1122
|
+
// Das Popup trägt die Fake-Klasse v-menu__content--active (Template), damit
|
|
1123
|
+
// Vuetify es im z-index-Stack mitzählt. Diese Klasse bringt aus Vuetify
|
|
1124
|
+
// jedoch `pointer-events: none` mit (VMenu.sass) — hier zurücksetzen, sonst
|
|
1125
|
+
// wäre das Popup unklickbar. Höhere Spezifität (.select2Panel .popup)
|
|
1126
|
+
// schlägt Vuetifys Single-Class-Regel.
|
|
1127
|
+
pointer-events: auto;
|
|
1128
|
+
|
|
1057
1129
|
background: white;
|
|
1058
1130
|
max-height: 40vh;
|
|
1059
1131
|
overflow: hidden;
|
|
@@ -1121,7 +1193,7 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
1121
1193
|
display: flex;
|
|
1122
1194
|
justify-content: flex-end;
|
|
1123
1195
|
gap: .5rem;
|
|
1124
|
-
margin-top:
|
|
1196
|
+
margin-top: .5rem;
|
|
1125
1197
|
}
|
|
1126
1198
|
|
|
1127
1199
|
// Größe zwischen x-small (20px) und small (28px): Vuetify bietet dazwischen
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
<select2-chip
|
|
114
114
|
v-for="filter in selectedFilters"
|
|
115
115
|
:key="filter.name"
|
|
116
|
+
:tooltip="filter.title"
|
|
116
117
|
@remove="resetFilter(filter.name)"
|
|
117
118
|
>
|
|
118
119
|
{{ filter.label }}: <b>{{ filter.value }}</b>
|
|
@@ -186,11 +187,11 @@ export default class ListFilterBar extends Vue {
|
|
|
186
187
|
return Object.values(this.filters).filter(f => !f.hasDefaultValueSet()).length - minus
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
listFilterChanged ({ payload: { name, label, value } }) {
|
|
190
|
-
this.setSelectedFilter(name, label, value)
|
|
190
|
+
listFilterChanged ({ payload: { name, label, value, title } }) {
|
|
191
|
+
this.setSelectedFilter(name, label, value, title)
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
setSelectedFilter (name, label, value) {
|
|
194
|
+
setSelectedFilter (name, label, value, title = null) {
|
|
194
195
|
const hasValue = !this.filters[name].hasDefaultValueSet()
|
|
195
196
|
|
|
196
197
|
if (!hasValue) {
|
|
@@ -199,12 +200,12 @@ export default class ListFilterBar extends Vue {
|
|
|
199
200
|
if (this.selectedFilters.some(f => f.name === name)) {
|
|
200
201
|
this.selectedFilters = this.selectedFilters.map(f => {
|
|
201
202
|
if (f.name === name) {
|
|
202
|
-
return { name, label, value }
|
|
203
|
+
return { name, label, value, title }
|
|
203
204
|
}
|
|
204
205
|
return f
|
|
205
206
|
})
|
|
206
207
|
} else {
|
|
207
|
-
this.selectedFilters.push({ name, label, value })
|
|
208
|
+
this.selectedFilters.push({ name, label, value, title })
|
|
208
209
|
}
|
|
209
210
|
}
|
|
210
211
|
}
|
|
@@ -6,14 +6,22 @@ import { ListFilterEvent } from '@a-vue/events'
|
|
|
6
6
|
})
|
|
7
7
|
export class ListFilterMixin extends Vue {
|
|
8
8
|
displayValue = null
|
|
9
|
+
// Optionaler vollständiger Tooltip-Text für den Bar-Chip (Hover) — der Chip
|
|
10
|
+
// selbst zeigt den gekürzten `displayValue`. Filter, die das nicht setzen,
|
|
11
|
+
// bekommen keinen Tooltip. Mehrere Einträge bricht man mit \n um.
|
|
12
|
+
displayTitle = null
|
|
9
13
|
name_ = null
|
|
10
14
|
|
|
15
|
+
// Nur auf displayValue lauschen — displayTitle wird immer GEMEINSAM mit
|
|
16
|
+
// displayValue gesetzt (s. ListFilterSelect2), ein eigener Watcher darauf würde
|
|
17
|
+
// dasselbe Event nur ein zweites Mal feuern. Der title wird hier mitgelesen.
|
|
11
18
|
@Watch('displayValue')
|
|
12
19
|
displayValueChanged () {
|
|
13
20
|
this.$events.dispatch(new ListFilterEvent(ListFilterEvent.CHANGE, {
|
|
14
21
|
name: this.name,
|
|
15
22
|
label: this.label,
|
|
16
|
-
value: this.displayValue
|
|
23
|
+
value: this.displayValue,
|
|
24
|
+
title: this.displayTitle
|
|
17
25
|
}))
|
|
18
26
|
}
|
|
19
27
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
:placeholder="placeholder"
|
|
7
7
|
:getIcon="getIcon"
|
|
8
8
|
:specialItems="specialItems"
|
|
9
|
-
multiple
|
|
9
|
+
:multiple="filter.multiple"
|
|
10
10
|
:allowExclude="allowExclude"
|
|
11
11
|
clearable
|
|
12
12
|
v-bind="$attrs"
|
|
@@ -59,14 +59,15 @@ export default class ListFilterSelect2 extends Mixins(ListFilterMixin) {
|
|
|
59
59
|
}
|
|
60
60
|
this.selectValue = await this.loadSelectValue(this.filter.value)
|
|
61
61
|
// Bar-Chip (eingeklappte Leiste) auch beim initialen Laden mit lesbaren
|
|
62
|
-
// Namen befüllen — nicht erst nach einer Änderung.
|
|
62
|
+
// Namen befüllen — nicht erst nach einer Änderung. displayTitle ZUERST, damit
|
|
63
|
+
// es beim displayValue-Watcher (ListFilterMixin) schon aktuell ist.
|
|
64
|
+
this.displayTitle = this.toDisplayTitle(this.selectValue)
|
|
63
65
|
this.displayValue = this.toDisplayString(this.selectValue)
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
// Token-Array (Filterwert, z.B. ['2', 'n-5']) → {model, polarity}[] für ASelect2.
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// die List-Action der Resource hat i.d.R. keinen id-Filter, daher get statt list.
|
|
69
|
+
// Echte Models werden GEBÜNDELT in EINEM list-Request geladen (filter[id]=[…],
|
|
70
|
+
// §4) statt je ID einzeln; Sonder-Items lokal aus den Filter-Options aufgelöst.
|
|
70
71
|
async loadSelectValue (value) {
|
|
71
72
|
const tokens = Array.isArray(value) ? value : []
|
|
72
73
|
if (!tokens.length) {
|
|
@@ -75,29 +76,39 @@ export default class ListFilterSelect2 extends Mixins(ListFilterMixin) {
|
|
|
75
76
|
|
|
76
77
|
const specials = this.specialItems
|
|
77
78
|
const entries = tokens.map(t => this.parseToken(t))
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
|
|
80
|
+
// Echte IDs (keine Sonder-Items) gebündelt nachladen, dann je Token zuordnen.
|
|
81
|
+
const realIds = entries
|
|
82
|
+
.filter(e => !specials.some(s => String(s.id) === String(e.id)))
|
|
83
|
+
.map(e => e.id)
|
|
84
|
+
const modelsById = await this.loadModelsByIds(realIds)
|
|
85
|
+
|
|
86
|
+
return entries
|
|
87
|
+
.map(entry => {
|
|
82
88
|
const special = specials.find(s => String(s.id) === String(entry.id))
|
|
83
89
|
if (special) {
|
|
84
90
|
return { model: special, polarity: entry.polarity }
|
|
85
91
|
}
|
|
86
|
-
const model =
|
|
92
|
+
const model = modelsById[String(entry.id)]
|
|
87
93
|
return model ? { model, polarity: entry.polarity } : null
|
|
88
94
|
})
|
|
89
|
-
|
|
90
|
-
return loaded.filter(Boolean)
|
|
95
|
+
.filter(e => e)
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
// Lädt mehrere Models in EINEM list-Request über den server-seitigen id-Filter
|
|
99
|
+
// (whereIn). Rückgabe: Map id→Model. Setzt voraus, dass die Resource den
|
|
100
|
+
// id-Filter kennt (z.B. AppointmentTypeResource, AssignmentContextResource).
|
|
101
|
+
async loadModelsByIds (ids) {
|
|
102
|
+
if (!ids.length) {
|
|
103
|
+
return {}
|
|
104
|
+
}
|
|
105
|
+
const request = this.filter.createOptionsRequest().addFilters({ id: ids })
|
|
106
|
+
const result = await ApiAction.fromRequest(request).hideError().execute()
|
|
107
|
+
const models = (result && result.models) || []
|
|
108
|
+
return models.reduce((map, model) => {
|
|
109
|
+
map[String(model.id)] = model
|
|
110
|
+
return map
|
|
111
|
+
}, {})
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
optionsRequest () {
|
|
@@ -147,13 +158,17 @@ export default class ListFilterSelect2 extends Mixins(ListFilterMixin) {
|
|
|
147
158
|
}
|
|
148
159
|
}
|
|
149
160
|
|
|
150
|
-
// {model, polarity}
|
|
161
|
+
// {model, polarity} (Einfach) bzw. {model, polarity}[] (Mehrfach) von ASelect2
|
|
162
|
+
// → Token-Array (Filterwert). Einfachauswahl emittiert KEIN Array (api §1), daher
|
|
163
|
+
// hier auf Array normalisieren — die Hülle arbeitet intern einheitlich mit Arrays.
|
|
151
164
|
onChange (value) {
|
|
152
165
|
this.filterChangedFromInside = true
|
|
153
166
|
|
|
154
|
-
const
|
|
167
|
+
const selection = this.toSelectionArray(value)
|
|
168
|
+
const tokens = selection.map(e => this.toToken(e.model.id, e.polarity))
|
|
155
169
|
this.filter.value = tokens.length ? tokens : null
|
|
156
|
-
this.selectValue =
|
|
170
|
+
this.selectValue = selection
|
|
171
|
+
this.displayTitle = this.toDisplayTitle(this.selectValue)
|
|
157
172
|
this.displayValue = this.toDisplayString(this.selectValue)
|
|
158
173
|
|
|
159
174
|
this.$nextTick(() => {
|
|
@@ -161,6 +176,15 @@ export default class ListFilterSelect2 extends Mixins(ListFilterMixin) {
|
|
|
161
176
|
})
|
|
162
177
|
}
|
|
163
178
|
|
|
179
|
+
// ASelect2-Außenwert → immer Array: Mehrfach ist schon ein Array, Einfach ein
|
|
180
|
+
// einzelnes {model, polarity} (oder null bei leerer Auswahl).
|
|
181
|
+
toSelectionArray (value) {
|
|
182
|
+
if (!value) {
|
|
183
|
+
return []
|
|
184
|
+
}
|
|
185
|
+
return Array.isArray(value) ? value : [value]
|
|
186
|
+
}
|
|
187
|
+
|
|
164
188
|
// 'n-5' → {id: '5', polarity: 'exclude'}; '2' → {id: '2', polarity: 'include'}.
|
|
165
189
|
parseToken (token) {
|
|
166
190
|
const str = String(token)
|
|
@@ -187,5 +211,17 @@ export default class ListFilterSelect2 extends Mixins(ListFilterMixin) {
|
|
|
187
211
|
const rest = selection.length - 1
|
|
188
212
|
return rest > 0 ? `${truncated} +${rest}` : truncated
|
|
189
213
|
}
|
|
214
|
+
|
|
215
|
+
// Vollständige Liste für den Bar-Chip-Tooltip (Hover): ALLE Items, eins pro
|
|
216
|
+
// Zeile, Ausschluss mit "nicht " markiert. Der Chip selbst zeigt nur die kurze
|
|
217
|
+
// toDisplayString-Form.
|
|
218
|
+
toDisplayTitle (selection) {
|
|
219
|
+
if (!selection || !selection.length) {
|
|
220
|
+
return null
|
|
221
|
+
}
|
|
222
|
+
return selection
|
|
223
|
+
.map(e => (e.polarity === 'exclude' ? 'nicht ' : '') + e.model.getTitle())
|
|
224
|
+
.join('\n')
|
|
225
|
+
}
|
|
190
226
|
}
|
|
191
227
|
</script>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
:close="closable && !disabled"
|
|
6
6
|
:color="chipColor"
|
|
7
7
|
:text-color="chipTextColor"
|
|
8
|
-
:title="title"
|
|
8
|
+
:title="tooltip || title"
|
|
9
9
|
@click:close="$emit('remove')"
|
|
10
10
|
>
|
|
11
11
|
<span class="label">
|
|
@@ -28,6 +28,9 @@ import { Component, Vue } from '@a-vue'
|
|
|
28
28
|
'getTitle',
|
|
29
29
|
'color',
|
|
30
30
|
'textColor',
|
|
31
|
+
// Expliziter Hover-Tooltip (Vorrang vor dem aus `item` abgeleiteten Titel) —
|
|
32
|
+
// z.B. der Bar-Chip zeigt darin alle gewählten Items, eins pro Zeile.
|
|
33
|
+
'tooltip',
|
|
31
34
|
{
|
|
32
35
|
polarity: 'include',
|
|
33
36
|
disabled: false,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
@keydown.up.prevent="moveActive(-1)"
|
|
9
9
|
@keydown.enter.ctrl.prevent="$emit('apply')"
|
|
10
10
|
@keydown.enter.exact.prevent="activateEnter"
|
|
11
|
+
@keydown.space.exact.prevent="activateEnter"
|
|
11
12
|
@keydown.enter.shift.prevent="activateExclude"
|
|
12
13
|
@keydown.tab.shift.prevent="$emit('backtab')"
|
|
13
14
|
>
|
|
@@ -40,6 +41,18 @@
|
|
|
40
41
|
:selected="isSelected(model)"
|
|
41
42
|
:polarity="polarityOf(model)"
|
|
42
43
|
>
|
|
44
|
+
<!-- Checkbox vorn (nur Mehrfachauswahl): zeigt den gewählt-Zustand.
|
|
45
|
+
Reine Anzeige — der Klick auf die ganze Zeile toggelt. -->
|
|
46
|
+
<v-simple-checkbox
|
|
47
|
+
v-if="showCheckbox"
|
|
48
|
+
class="rowCheckbox"
|
|
49
|
+
:value="isSelected(model)"
|
|
50
|
+
:disabled="rowDisabled(model)"
|
|
51
|
+
dense
|
|
52
|
+
hide-details
|
|
53
|
+
@input="onRowClick(model)"
|
|
54
|
+
/>
|
|
55
|
+
|
|
43
56
|
<a-icon
|
|
44
57
|
v-if="getIcon"
|
|
45
58
|
:icon="getIcon(model)"
|
|
@@ -126,6 +139,10 @@ import { Component, Vue, Watch } from '@a-vue'
|
|
|
126
139
|
'isItemDisabled',
|
|
127
140
|
{
|
|
128
141
|
allowExclude: false,
|
|
142
|
+
// Checkbox vorn pro Zeile: macht den Auswahl-Zustand bei Mehrfachauswahl
|
|
143
|
+
// klar sichtbar (besonders bei nur 1 Eintrag). Nur Anzeige des „gewählt"-
|
|
144
|
+
// Zustands — die Polarität (include/exclude) zeigt weiter der Nicht-Button.
|
|
145
|
+
showCheckbox: false,
|
|
129
146
|
isLoading: false,
|
|
130
147
|
hasMore: false,
|
|
131
148
|
// Anzahl der oben angepinnten Sonder-Items (§5): die ersten N Zeilen
|
|
@@ -328,6 +345,21 @@ export default class Select2List extends Vue {
|
|
|
328
345
|
visibility: visible;
|
|
329
346
|
}
|
|
330
347
|
|
|
348
|
+
// Checkbox vorn (Mehrfachauswahl): kompakt, kein Eigen-Margin (die Zeile bringt
|
|
349
|
+
// den gap mit). flex-shrink 0, damit sie bei langen Titeln nicht gequetscht wird.
|
|
350
|
+
|
|
351
|
+
.rowCheckbox {
|
|
352
|
+
flex: 0 0 auto;
|
|
353
|
+
margin: 0;
|
|
354
|
+
|
|
355
|
+
// Vuetify setzt margin-right: 8px aufs Selection-Control — die Zeile bringt
|
|
356
|
+
// den gap schon mit, daher hier weg.
|
|
357
|
+
|
|
358
|
+
:deep(.v-input--selection-controls__input) {
|
|
359
|
+
margin-right: 0;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
331
363
|
// Oben angepinnte Sonder-Items (§5): bleiben beim Scrollen sichtbar. Weißer
|
|
332
364
|
// Hintergrund, damit durchscrollende Treffer nicht durchscheinen.
|
|
333
365
|
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// vorkommen, nur einmal laden statt pro Komponente. Singleton im Stil von
|
|
5
5
|
// positionService — kein Vue-Plugin, keine Vue.prototype-Erweiterung.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
7
|
+
// Bei cacheResults=true läuft JEDE Anfrage über den Cache — jede Seite, auch mit
|
|
8
|
+
// Suchbegriff (q/page sind Teil des Keys, siehe buildRequestCacheKey).
|
|
8
9
|
// Zeitbasierte Invalidierung (Default 30 Min), keine aktive Invalidierung.
|
|
9
10
|
// inFlight-Dedup verhindert parallele Doppel-Requests auf denselben Key.
|
|
10
11
|
|