@afeefa/vue-app 0.0.342 → 0.0.343
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 -56
- 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.343
|
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,22 @@ 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
|
-
// -
|
|
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. Zwei Sonderfälle halten das Feld zusätzlich sichtbar:
|
|
373
|
+
// - Schon getippt (`search`): das Feld muss bleiben, sonst lässt sich die Suche
|
|
374
|
+
// nicht mehr ändern/leeren.
|
|
375
|
+
// - Noch nichts geladen (`!loaded`): `totalCount` ist erst nach der ersten Seite
|
|
376
|
+
// bekannt. Bis dahin Feld zeigen, statt es nachträglich aufpoppen zu lassen.
|
|
361
377
|
get showSearchField () {
|
|
362
378
|
if (!this.isDynamic || !this.hasSearch) {
|
|
363
379
|
return false
|
|
364
380
|
}
|
|
365
|
-
|
|
381
|
+
if (!this.loaded) {
|
|
382
|
+
return true
|
|
383
|
+
}
|
|
384
|
+
return !!this.search || this.totalCount > this.pageSize
|
|
366
385
|
}
|
|
367
386
|
|
|
368
387
|
// Such-Label wie bei ASearchSelect: "Suche <label>" (z.B. "Suche SpraMi").
|
|
@@ -469,6 +488,13 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
469
488
|
this.searchResults = reset ? models : [...this.searchResults, ...models]
|
|
470
489
|
this.count = meta.count_search != null ? meta.count_search : this.searchResults.length
|
|
471
490
|
this.hasMore = this.searchResults.length < this.count
|
|
491
|
+
// Gesamtzahl OHNE Suchbegriff (aber mit aktiven Filtern) — steht in jeder
|
|
492
|
+
// Antwort als `count_filter`, auch während einer 0-Treffer-Suche. Steuert
|
|
493
|
+
// stabil, ob die Liste ein Suchfeld braucht (anders als `count`, das bei einer
|
|
494
|
+
// Suche auf die Trefferzahl fällt). Fallback auf count_all bzw. count.
|
|
495
|
+
this.totalCount = meta.count_filter != null
|
|
496
|
+
? meta.count_filter
|
|
497
|
+
: (meta.count_all != null ? meta.count_all : this.count)
|
|
472
498
|
this.loaded = true
|
|
473
499
|
|
|
474
500
|
this.isLoading = false
|
|
@@ -502,7 +528,9 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
502
528
|
// Eintrag zeigen. Bezieht Felder/Filter/Suche/Seite ein.
|
|
503
529
|
cacheKey (page) {
|
|
504
530
|
return buildRequestCacheKey(this.optionsRequest(), {
|
|
505
|
-
|
|
531
|
+
// Leere Suche als '' (nicht null) in den Key — sonst zeigt der Lookup auf
|
|
532
|
+
// einen anderen Eintrag als die main.js-Vorwärmung (die mit q:'' primt).
|
|
533
|
+
q: this.search || '',
|
|
506
534
|
page,
|
|
507
535
|
pageSize: this.pageSize
|
|
508
536
|
})
|
|
@@ -510,7 +538,7 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
510
538
|
|
|
511
539
|
// Suche + Seite als Filter (für Request und Cache-Key gleichermaßen).
|
|
512
540
|
pageFilters (page) {
|
|
513
|
-
return { q: this.search, page, page_size: this.pageSize }
|
|
541
|
+
return { q: this.search || '', page, page_size: this.pageSize }
|
|
514
542
|
}
|
|
515
543
|
|
|
516
544
|
// Baut die ListAction der Datenquelle und setzt Suche + Seite als Filter.
|
|
@@ -524,11 +552,28 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
524
552
|
|
|
525
553
|
// --- Auswahl-Helfer ------------------------------------------------------
|
|
526
554
|
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
555
|
+
// Mehrfachauswahl, die sich aber wie Single bedient: wenn nur EIN Eintrag
|
|
556
|
+
// wählbar ist (vollständig geladen, `displayItems.length === 1`), ist eine
|
|
557
|
+
// Draft-/Übernehmen-Bar sinnlos — ein Klick wählt und schließt sofort, kein
|
|
558
|
+
// Footer. Das **Wertformat** bleibt am Prop `multiple` hängen (toOuter/fromOuter
|
|
559
|
+
// unverändert, sonst bräche die Filter-Hülle, die ein Array erwartet) — nur das
|
|
560
|
+
// Bedien-VERHALTEN kippt.
|
|
561
|
+
get behavesSingle () {
|
|
562
|
+
return this.multiple && !this.hasMore && this.displayItems.length === 1
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Footer (X/OK) nur bei echter Mehrfachauswahl UND erst, wenn die Items geladen
|
|
566
|
+
// sind: bei dynamischer Quelle vor dem ersten Laden noch nicht (da ist weder die
|
|
567
|
+
// Trefferliste noch behavesSingle entschieden) — bei fester Liste sofort.
|
|
568
|
+
get showFooter () {
|
|
569
|
+
return this.multiple && !this.behavesSingle && (!this.isDynamic || this.loaded)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Die im Popup aktive Auswahl: Mehrfach → Draft (Arbeitskopie), Einfach (auch
|
|
573
|
+
// behavesSingle) → committed (Klick committet sofort, kein Draft nötig). Liste +
|
|
574
|
+
// Chips lesen hierüber, damit beide Modi denselben Code teilen.
|
|
530
575
|
get activeSelection () {
|
|
531
|
-
return this.multiple ? this.draft : this.committed
|
|
576
|
+
return this.multiple && !this.behavesSingle ? this.draft : this.committed
|
|
532
577
|
}
|
|
533
578
|
|
|
534
579
|
get sortedSelection () {
|
|
@@ -543,6 +588,18 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
543
588
|
return this.sortedSelection.map(e => e.model)
|
|
544
589
|
}
|
|
545
590
|
|
|
591
|
+
// Hover-Titel (natives title-Attribut) am Feld: ALLE gewählten Items, eins pro
|
|
592
|
+
// Zeile (das Feld selbst zeigt nur den ersten Namen + "+N"). Ausschluss mit
|
|
593
|
+
// "nicht " markiert, wie im Feld-Text. Leer → kein Tooltip.
|
|
594
|
+
get committedTooltip () {
|
|
595
|
+
if (!this.sortedSelection.length) {
|
|
596
|
+
return null
|
|
597
|
+
}
|
|
598
|
+
return this.sortedSelection
|
|
599
|
+
.map(e => (e.polarity === 'exclude' ? 'nicht ' : '') + this.itemTitle(e.model))
|
|
600
|
+
.join('\n')
|
|
601
|
+
}
|
|
602
|
+
|
|
546
603
|
// Polarität aus der committed-Auswahl (Feld-Chips zeigen den bestätigten Stand,
|
|
547
604
|
// nicht den Popup-Draft — anders als polarityOf, das den aktiven Draft liest).
|
|
548
605
|
committedPolarityOf (model) {
|
|
@@ -576,9 +633,10 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
576
633
|
|
|
577
634
|
// --- Tabs / Auswahl-Tab --------------------------------------------------
|
|
578
635
|
|
|
579
|
-
// Es gibt etwas Auswählbares zum Umschalten (Auswahl-Chip sichtbar).
|
|
636
|
+
// Es gibt etwas Auswählbares zum Umschalten (Auswahl-Chip sichtbar). Nicht bei
|
|
637
|
+
// behavesSingle (nur 1 Eintrag, kein Mehrfach-Bedienmodell).
|
|
580
638
|
get showTabs () {
|
|
581
|
-
return this.multiple && this.activeSelection.length > 0
|
|
639
|
+
return this.multiple && !this.behavesSingle && this.activeSelection.length > 0
|
|
582
640
|
}
|
|
583
641
|
|
|
584
642
|
// Models der aktuellen Auswahl (für die Auswahl-Ansicht).
|
|
@@ -628,15 +686,17 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
628
686
|
}
|
|
629
687
|
|
|
630
688
|
// Schreibt die neue Auswahl: Mehrfach → in den Draft, Popup bleibt offen;
|
|
631
|
-
// Einfach → sofort committen und schließen.
|
|
689
|
+
// Einfach (auch behavesSingle: nur 1 Eintrag) → sofort committen und schließen.
|
|
690
|
+
// Fokus zurück aufs Feld (wie der apply-Pfad bei Mehrfach), damit nach Klick/Enter/
|
|
691
|
+
// Space die Komponente fokussiert bleibt und nicht auf body fällt.
|
|
632
692
|
commitSelection (next) {
|
|
633
|
-
if (this.multiple) {
|
|
693
|
+
if (this.multiple && !this.behavesSingle) {
|
|
634
694
|
this.draft = next
|
|
635
695
|
return
|
|
636
696
|
}
|
|
637
697
|
this.committed = next
|
|
638
698
|
this.emitChange()
|
|
639
|
-
this.close()
|
|
699
|
+
this.close({ returnFocus: true })
|
|
640
700
|
}
|
|
641
701
|
|
|
642
702
|
withItem (selection, model, polarity) {
|
|
@@ -675,20 +735,13 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
675
735
|
// Tippen im Suchfeld wechselt zurück in die Trefferliste (falls gerade die
|
|
676
736
|
// Auswahl-Ansicht aktiv ist) — sonst sucht man, sieht aber die Auswahl.
|
|
677
737
|
onSearchInput (value) {
|
|
678
|
-
|
|
738
|
+
// Leere Suche ist intern `null` (a-text-field mit `clearable` emittiert beim
|
|
739
|
+
// ×-Klick ohnehin `null`). Alle Lese-Stellen nutzen `!this.search` (truthy),
|
|
740
|
+
// sind also null-fest; `q` im Request fällt damit weg statt leerem String.
|
|
741
|
+
this.search = value || null
|
|
679
742
|
this.activeTab = 'search'
|
|
680
743
|
}
|
|
681
744
|
|
|
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
745
|
applyIfMultiple () {
|
|
693
746
|
if (this.multiple) {
|
|
694
747
|
this.apply()
|
|
@@ -790,13 +843,13 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
790
843
|
// wirkt (§4). Die committed-Auswahl bleibt unberührt; nur der flüchtige Such-
|
|
791
844
|
// String/die Trefferliste werden neu aufgebaut. Der cacheResults-Cache greift
|
|
792
845
|
// davon unabhängig weiter (gleicher Key → aus dem Cache).
|
|
793
|
-
// search=
|
|
846
|
+
// search=null kann den @Watch('search') auslösen; der würde einen zweiten,
|
|
794
847
|
// debounced Request anstoßen. Daher den Watcher für diesen einen Reset
|
|
795
848
|
// unterdrücken (nur wenn search sich wirklich ändert) und EINMAL direkt laden.
|
|
796
849
|
if (this.isDynamic) {
|
|
797
850
|
if (this.search) {
|
|
798
851
|
this.suppressSearchWatch = true
|
|
799
|
-
this.search =
|
|
852
|
+
this.search = null
|
|
800
853
|
}
|
|
801
854
|
// Fokus erst NACH dem ersten Laden setzen: vorher ist `showSearchField`
|
|
802
855
|
// noch unentschieden (loaded=false ⇒ true), und bei einer einseitigen Liste
|
|
@@ -859,8 +912,10 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
859
912
|
return
|
|
860
913
|
}
|
|
861
914
|
|
|
862
|
-
// Mehrfachauswahl mit ungespeicherten Draft-Änderungen → rückfragen.
|
|
863
|
-
|
|
915
|
+
// Mehrfachauswahl mit ungespeicherten Draft-Änderungen → rückfragen. Nicht bei
|
|
916
|
+
// behavesSingle (nur 1 Item): der Klick committet dort sofort, es gibt keinen
|
|
917
|
+
// Draft zu verwerfen.
|
|
918
|
+
if (!skipDiscardCheck && this.multiple && !this.behavesSingle && this.isDirty) {
|
|
864
919
|
const result = await this.$events.dispatch(new DialogEvent(DialogEvent.SHOW, {
|
|
865
920
|
title: 'Änderungen verwerfen?',
|
|
866
921
|
message: 'Die Auswahl wurde geändert. Sollen die Änderungen verworfen werden?',
|
|
@@ -970,7 +1025,17 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
970
1025
|
}
|
|
971
1026
|
|
|
972
1027
|
coe_cancelOnEsc () {
|
|
973
|
-
// Esc
|
|
1028
|
+
// EINZIGER Esc-Pfad (window-keyup, kaskadierend). Mit Suchtext: nur leeren,
|
|
1029
|
+
// Popup bleibt offen (§14). Sonst: schließen, Fokus zurück. Früher gab es
|
|
1030
|
+
// zusätzlich einen @keydown.esc am Suchfeld → Esc leerte UND schloss, weil
|
|
1031
|
+
// keydown (Feld) und keyup (dieser Mixin) zwei getrennte Events sind.
|
|
1032
|
+
if (this.search) {
|
|
1033
|
+
// Suche leeren — der @Watch('search') lädt (debounced) neu. Das Suchfeld
|
|
1034
|
+
// bleibt dabei stabil sichtbar, weil showSearchField an `totalCount` hängt
|
|
1035
|
+
// (Gesamtzahl ohne Suchbegriff), nicht am wegfallenden Such-`count`.
|
|
1036
|
+
this.search = null
|
|
1037
|
+
return false // stop esc propagation
|
|
1038
|
+
}
|
|
974
1039
|
this.close({ returnFocus: true })
|
|
975
1040
|
return false // stop esc propagation
|
|
976
1041
|
}
|
|
@@ -1053,7 +1118,15 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
1053
1118
|
[class*=" select2Panel-"] {
|
|
1054
1119
|
.popup {
|
|
1055
1120
|
position: absolute;
|
|
1056
|
-
z-index:
|
|
1121
|
+
z-index: 300;
|
|
1122
|
+
|
|
1123
|
+
// Das Popup trägt die Fake-Klasse v-menu__content--active (Template), damit
|
|
1124
|
+
// Vuetify es im z-index-Stack mitzählt. Diese Klasse bringt aus Vuetify
|
|
1125
|
+
// jedoch `pointer-events: none` mit (VMenu.sass) — hier zurücksetzen, sonst
|
|
1126
|
+
// wäre das Popup unklickbar. Höhere Spezifität (.select2Panel .popup)
|
|
1127
|
+
// schlägt Vuetifys Single-Class-Regel.
|
|
1128
|
+
pointer-events: auto;
|
|
1129
|
+
|
|
1057
1130
|
background: white;
|
|
1058
1131
|
max-height: 40vh;
|
|
1059
1132
|
overflow: hidden;
|
|
@@ -1121,7 +1194,7 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
1121
1194
|
display: flex;
|
|
1122
1195
|
justify-content: flex-end;
|
|
1123
1196
|
gap: .5rem;
|
|
1124
|
-
margin-top:
|
|
1197
|
+
margin-top: .5rem;
|
|
1125
1198
|
}
|
|
1126
1199
|
|
|
1127
1200
|
// 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
|
|