iobroker.alias-workbench 0.8.0

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +516 -0
  3. package/admin/alias-workbench.png +0 -0
  4. package/admin/css/werkbank.css +916 -0
  5. package/admin/detector.js +1 -0
  6. package/admin/geraetewissen/tasmota-befehle.json +116 -0
  7. package/admin/i18n/de.json +552 -0
  8. package/admin/i18n/en.json +552 -0
  9. package/admin/js/aufzaehlungen.js +332 -0
  10. package/admin/js/basis.js +36 -0
  11. package/admin/js/baum.js +453 -0
  12. package/admin/js/detail.js +335 -0
  13. package/admin/js/einstellungen.js +40 -0
  14. package/admin/js/entwurf.js +908 -0
  15. package/admin/js/enums.js +23 -0
  16. package/admin/js/ergebnis.js +934 -0
  17. package/admin/js/erkennung.js +126 -0
  18. package/admin/js/katalog.js +313 -0
  19. package/admin/js/mqtt.js +885 -0
  20. package/admin/js/musternamen.js +94 -0
  21. package/admin/js/musterwahl.js +334 -0
  22. package/admin/js/objekte.js +243 -0
  23. package/admin/js/pruefungen.js +130 -0
  24. package/admin/js/quellentausch.js +570 -0
  25. package/admin/js/rollenwahl.js +199 -0
  26. package/admin/js/schreiben.js +1537 -0
  27. package/admin/js/sprache.js +137 -0
  28. package/admin/js/start.js +39 -0
  29. package/admin/js/verbindung.js +10 -0
  30. package/admin/js/vorlagen.js +572 -0
  31. package/admin/js/vorlagenblatt.js +1907 -0
  32. package/admin/js/vorschlagen.js +391 -0
  33. package/admin/js/werte.js +193 -0
  34. package/admin/js/zielleiste.js +339 -0
  35. package/admin/js/zuordnung.js +695 -0
  36. package/admin/js/zustand.js +47 -0
  37. package/admin/js/zustandsliste.js +515 -0
  38. package/admin/jsonConfig.json +249 -0
  39. package/admin/tab.html +148 -0
  40. package/admin/vorlagen/hm-dimmer.json +204 -0
  41. package/admin/vorlagen/hm-fensterkontakt-hm.json +176 -0
  42. package/admin/vorlagen/hm-fensterkontakt-hmip.json +191 -0
  43. package/admin/vorlagen/hm-heizgruppe.json +214 -0
  44. package/admin/vorlagen/hm-heizkoerper.json +236 -0
  45. package/admin/vorlagen/hm-rollladen.json +190 -0
  46. package/admin/vorlagen/hm-schaltaktor-lampe.json +197 -0
  47. package/admin/vorlagen/hm-schaltaktor-mehrfach.json +203 -0
  48. package/admin/vorlagen/hm-schaltaktor-steckdose.json +197 -0
  49. package/admin/vorlagen/hm-temperatur-feuchte.json +166 -0
  50. package/admin/vorlagen/hm-wandthermostat.json +228 -0
  51. package/admin/vorlagen/index.json +21 -0
  52. package/admin/vorlagen/messpunkt.json +225 -0
  53. package/admin/vorlagen/tasmota-lampe.json +263 -0
  54. package/admin/vorlagen/tasmota-mehrfach.json +203 -0
  55. package/admin/vorlagen/tasmota-rgb.json +239 -0
  56. package/admin/vorlagen/tasmota-steckdose.json +265 -0
  57. package/io-package.json +122 -0
  58. package/package.json +48 -0
@@ -0,0 +1,453 @@
1
+ /* Der Baum links: Klappstand, Expertenmodus, Aufbau, Modi.
2
+
3
+ Der Klappstand liegt im localStorage des Browsers - er ist Ansichts-
4
+ sache, keine Konfiguration. Der Expertenmodus folgt woertlich der Regel
5
+ des Objektbrowsers im Admin. */
6
+
7
+ import { S } from './zustand.js';
8
+ import { $, el } from './basis.js';
9
+ import { tr, txt, expertenVorgabe } from './sprache.js';
10
+ import './enums.js';
11
+ import { waehle } from './entwurf.js';
12
+ import { zeichneErgebnis } from './ergebnis.js';
13
+ import { zeichneVorlagenListe } from './vorlagenblatt.js';
14
+
15
+ var KLAPP_SCHLUESSEL = 'alias-workbench.baum';
16
+
17
+
18
+ (function ladeKlappstand() {
19
+ try {
20
+ var roh = window.localStorage.getItem(KLAPP_SCHLUESSEL);
21
+ if (!roh) { return; }
22
+ var d = JSON.parse(roh);
23
+ if (d && typeof d === 'object') {
24
+ S.aufgeklappt = d.knoten || {};
25
+ S.alleKlapp = (d.alle === true || d.alle === false) ? d.alle : null;
26
+ }
27
+ } catch { /* kaputter oder gesperrter Speicher: dann eben Vorgabe */ }
28
+ })();
29
+
30
+ export function merkeKlappstand() {
31
+ try {
32
+ window.localStorage.setItem(KLAPP_SCHLUESSEL,
33
+ JSON.stringify({ knoten: S.aufgeklappt, alle: S.alleKlapp }));
34
+ } catch { /* privater Modus oder voll - nicht der Rede wert */ }
35
+ }
36
+
37
+ /* ================== Expertenmodus ==================
38
+ Der Admin fuehrt ihn zweistufig: der Schalter oben rechts gilt nur fuer
39
+ diese Browsersitzung und steht im sessionStorage, darunter liegt die
40
+ dauerhafte Vorgabe in system.config. Genau diese Reihenfolge steht auch
41
+ im Admin-Bundle:
42
+
43
+ const f = sessionStorage.getItem("App.expertMode");
44
+ expertMode = f ? f === "true" : !!systemConfig.common.expertMode;
45
+
46
+ Der Reiter laeuft im iframe derselben Herkunft, liest also denselben
47
+ Speicher — und bekommt beim Umschalten ein storage-Ereignis, ohne dass
48
+ ihn jemand neu laden muss. */
49
+
50
+ export function experte() {
51
+ var f = null;
52
+ try { f = window.sessionStorage.getItem('App.expertMode'); } catch { /* gesperrt */ }
53
+ return f ? (f === 'true') : expertenVorgabe;
54
+ }
55
+
56
+ /* Was nur Experten sehen. Woertlich dieselbe Regel wie im Objektbrowser
57
+ des Admin, damit die Werkbank nicht ihre eigene Wahrheit pflegt. Das
58
+ letzte Stueck ist das nuetzlichste: ein Adapter kann einen Punkt selbst
59
+ als Expertensache markieren — Zugangstoken etwa tun das. */
60
+ export function nurFuerExperten(id, o) {
61
+ if (id === 'system' || id.indexOf('system.') === 0) { return true; }
62
+ if (id === 'enum' || id.indexOf('enum.') === 0) { return true; }
63
+ if (id.indexOf('_design/') === 0) { return true; }
64
+ if (id.slice(-6) === '.admin') { return true; }
65
+ return !!(o && o.common && o.common.expert);
66
+ }
67
+
68
+ window.addEventListener('storage', function (e) {
69
+ if (e.key !== 'App.expertMode') { return; }
70
+ /* Steht die Auswahl in einem Zweig, den es gleich nicht mehr gibt, muss
71
+ sie weg — sonst zeigt die rechte Seite ein Geraet, das links fehlt. */
72
+ if (S.current && !experte() && nurFuerExperten(S.current, S.objects[S.current])) {
73
+ S.current = null;
74
+ S.entwurf = null;
75
+ }
76
+ zeichneBaum();
77
+ zeichneErgebnis();
78
+ });
79
+
80
+ /* Der Name eines Objekts — aber nur, wenn er mehr sagt als die ID.
81
+
82
+ Bei Adaptern, die Seriennummern als Kennung nehmen, steht im Baum
83
+ sonst nichts Lesbares: `000A1D89900002` ist HK_Bastelzimmer,
84
+ `AFDCMSTESFOHWVAXIDNBDQQROIYQ` ist „Ricardo (Self)". Gemessen am
85
+ Produktivsystem sagt bei 1575 von 2532 Behaeltern der Name mehr als
86
+ die Kennung; bei 569 ist er dasselbe Wort, bei 388 gibt es keinen.
87
+ Die letzten beiden Faelle bleiben so kurz wie bisher. */
88
+
89
+ export function zusatzName(id) {
90
+ var o = S.objects[id];
91
+ if (!o) { return ''; }
92
+ var n = txt(o.common && o.common.name).trim();
93
+ if (!n) { return ''; }
94
+ return (n === String(id).split('.').pop()) ? '' : n;
95
+ }
96
+
97
+ /* Wonach die Zeile gelesen und sortiert wird. */
98
+ export function anzeigeText(k) {
99
+ return zusatzName(k.id) || k.name;
100
+ }
101
+
102
+ function baueBaum() {
103
+ var wurzel = { kinder: {}, id: '', name: '' };
104
+ var alles = experte();
105
+ S.keysSorted.forEach(function (id) {
106
+ var o = S.objects[id];
107
+ /* Der Index darf einen Takt hinterherhinken - das Abo traegt aus und
108
+ raeumt erst mit seinem Zeitgeber auf. Ein fehlendes Objekt ist
109
+ hier kein Grund, den ganzen Baum abzubrechen. */
110
+ if (!o) { return; }
111
+ /* Gefiltert wird beim Bauen, nicht beim Zeichnen: sonst zaehlte ein
112
+ Ordner Zustaende mit, die niemand sieht. Die Quellenauswahl im
113
+ Detailfenster bleibt bewusst ungefiltert — ein Alias, der aus
114
+ system.adapter.…alive liest, muss bearbeitbar bleiben. */
115
+ if (!alles && nurFuerExperten(id, o)) { return; }
116
+ var teile = id.split('.');
117
+ var k = wurzel;
118
+ var pfad = '';
119
+ for (var i = 0; i < teile.length; i++) {
120
+ pfad = pfad ? (pfad + '.' + teile[i]) : teile[i];
121
+ if (!k.kinder[teile[i]]) {
122
+ k.kinder[teile[i]] = { kinder: {}, id: pfad, name: teile[i] };
123
+ }
124
+ k = k.kinder[teile[i]];
125
+ }
126
+ k.obj = o;
127
+ k.art = o.type;
128
+ });
129
+ return wurzel;
130
+ }
131
+
132
+ function zaehleZustaende(k, eltern) {
133
+ k._eltern = eltern || null;
134
+ var n = (k.art === 'state') ? 1 : 0;
135
+ var tiefe = -1;
136
+ Object.keys(k.kinder).forEach(function (name) {
137
+ var kind = k.kinder[name];
138
+ n += zaehleZustaende(kind, k);
139
+ if (kind._zust) {
140
+ var t = kind._tiefe + 1;
141
+ if (t > tiefe) { tiefe = t; }
142
+ }
143
+ });
144
+ k._zust = n;
145
+ k._tiefe = tiefe < 0 ? 0 : tiefe;
146
+ k._hatTypen = false;
147
+ Object.keys(k.kinder).forEach(function (name) {
148
+ var kind = k.kinder[name];
149
+ if (kind.art === 'channel' || kind.art === 'device' || kind._hatTypen) { k._hatTypen = true; }
150
+ });
151
+ return n;
152
+ }
153
+
154
+
155
+ export function zeichneBaum() {
156
+ if (S.baumModus === 'vorlagen') { return zeichneVorlagenListe(); }
157
+ S.baum = baueBaum();
158
+ zaehleZustaende(S.baum, null);
159
+
160
+ /* alias.0 ist unser Ergebnis, keine Quelle — deshalb getrennte Sicht. */
161
+ var wurzel = { kinder: {}, id: '', name: '', _zust: 1, _tiefe: 9 };
162
+ Object.keys(S.baum.kinder).forEach(function (n) {
163
+ var istAlias = (n === 'alias');
164
+ if ((S.baumModus === 'aliase') === istAlias) { wurzel.kinder[n] = S.baum.kinder[n]; }
165
+ });
166
+ S.baum = wurzel;
167
+ var host = $('#tree');
168
+ host.textContent = '';
169
+ var filter = ($('#q').value || '').trim().toLowerCase();
170
+ var ul = renderKinder(S.baum, filter, 0);
171
+ if (!ul.childNodes.length) {
172
+ host.appendChild(el('div', 'empty', tr('tree.nothingFound')));
173
+ } else {
174
+ host.appendChild(ul);
175
+ }
176
+ }
177
+
178
+ /* Waehlbar ist alles, unter dem irgendwo Zustaende liegen — nicht nur
179
+ direkt darunter. MQTT legt fuer Zwischenebenen gar keine Objekte an:
180
+ unter …SmartHome.Karbonator existieren nur tele.LWT, tele.SENSOR und
181
+ tele.STATE, aber weder „Karbonator" noch „tele" selbst. Wer nur direkte
182
+ Kinder zaehlt, kann das Geraet nie auswaehlen. */
183
+
184
+ /* Was ist ein Geraet? Erst die Typen fragen, die ioBroker selbst pflegt —
185
+ channel und device sind Geraete, folder ist es nie. Das gilt fuer
186
+ hm-rpc, shelly, alexa2, dreame und alle anderen ohne Sonderfall.
187
+ Nur wo gar kein Objekt existiert (MQTT legt fuer Zwischenebenen keins
188
+ an), muss geraten werden: dann der oberste Knoten, unter dem die
189
+ Datenpunkte hoechstens zwei Ebenen tief liegen. */
190
+ /* Liegen unmittelbar unter diesem Knoten Datenpunkte? Dann ist er ein
191
+ Geraet, egal was in seinem Typ steht. */
192
+ export function hatDirekteZustaende(k) {
193
+ return Object.keys(k.kinder).some(function (n) {
194
+ return k.kinder[n].art === 'state';
195
+ });
196
+ }
197
+
198
+ export function istWaehlbar(k) {
199
+ if (!k._zust) { return false; }
200
+ /* Ein `state`, unter dem eigene Datenpunkte liegen, ist ein Behaelter -
201
+ auch wenn er nach Datenmodell keiner sein darf.
202
+
203
+ Der Velux-Adapter legt jedes Modul so an: `velux.0.home.<id>` ist
204
+ `type: state` mit `role: indicator` und traegt siebzehn Datenpunkte
205
+ unter sich, darunter `current_position` und `target_position`. Das
206
+ ist ein Rollladen, kein Messwert. Von 28 Knoten mit Kindern in dem
207
+ Zweig sind 27 auf diese Weise gebaut - Raeume, Module, Zeitplaene.
208
+ Richtig waere `device`, `channel` oder `folder`; das ist ein Fehler
209
+ des Adapters, aber er steht in Ricardos Anlage und die Werkbank kam
210
+ an kein einziges Velux-Geraet heran.
211
+
212
+ Dieselbe Ausnahme gilt seit laengerem fuer `folder` (siehe unten).
213
+ Die Regel ist in beiden Faellen dieselbe: liegen direkte Datenpunkte
214
+ darunter, ist es ein Geraet; liegen keine darunter, ist es ein Blatt.
215
+ Ein gewoehnlicher Messwert bleibt damit unwaehlbar wie bisher. */
216
+ if (k.art === 'state') { return hatDirekteZustaende(k); }
217
+ if (k.art === 'channel' || k.art === 'device') { return true; }
218
+ /* Ein Ordner mit eigenen Datenpunkten ist ein Geraet, das jemand als
219
+ Ordner angelegt hat — der Alias-Manager tut das, Skripte tun es, der
220
+ Admin selbst tut es. Der type-detector sieht das genauso; in seinem
221
+ Quelltext steht woertlich „a state needs to be in a channel, device,
222
+ folder or such", und `folder` steht dort in einem Zweig mit den
223
+ beiden anderen. Die Werkbank war hier strenger als das Werkzeug,
224
+ auf dem sie aufbaut: acht Geraete einer gewachsenen Anlage liessen
225
+ sich nicht anfassen.
226
+
227
+ Ordner, unter denen nur weitere Ordner oder Kanaele liegen, bleiben
228
+ aussen vor — das sind Wegweiser, keine Geraete. */
229
+ if (k.art === 'folder') { return hatDirekteZustaende(k); }
230
+ if (k.art === 'meta') { return false; }
231
+ if (k.art) { return false; }
232
+ /* Notbehelf nur dort, wo im ganzen Zweig keine Typen gepflegt sind.
233
+ Wo es getypte Kanäle gibt, sollen die das Gerät sein. */
234
+ if (k._hatTypen) { return false; }
235
+ /* Sonst: alles waehlbar. Was ein Geraet ist, weiss nur der Mensch —
236
+ bei Duneweaver ist es der Knoten darueber, bei Tasmota der darunter.
237
+ Statt zu sperren wird geraten (siehe wohlGeraet) und gewarnt. */
238
+ return true;
239
+ }
240
+
241
+ /* Sieht dieser Knoten wie ein einzelnes Geraet aus? Nur ein Hinweis. */
242
+ export function wohlGeraet(k) {
243
+ if (k.art === 'channel' || k.art === 'device') { return true; }
244
+ /* `state` nach derselben Regel wie `folder` - sonst waere der Knoten
245
+ waehlbar und bekaeme im selben Atemzug den Hinweis, er sei wohl
246
+ kein Geraet. */
247
+ if (k.art === 'folder' || k.art === 'state') { return hatDirekteZustaende(k); }
248
+ if (k.art) { return false; }
249
+ return k._tiefe <= 2 && (!k._eltern || k._eltern._tiefe > 2);
250
+ }
251
+
252
+ function renderKinder(knoten, filter, tiefe) {
253
+ var ul = el('ul');
254
+ /* Sortiert wird nach dem, was dasteht — sonst steht die Liste in einer
255
+ Ordnung, die niemand sieht: bei hm-rpc nach Seriennummer, wo doch
256
+ FK_* und HK_* beieinander gehoeren. `numeric` dazu, damit POWER2 vor
257
+ POWER10 kommt. Einmal rechnen, nicht bei jedem Vergleich. */
258
+ var reihen = Object.keys(knoten.kinder).map(function (n) {
259
+ var kk = knoten.kinder[n];
260
+ return { k: kk, zus: zusatzName(kk.id) };
261
+ });
262
+ /* Behaelter vor Blaettern, wie im Windows-Explorer: erst, was sich
263
+ aufklappen laesst, darunter die Zeilen ohne eigenes Innenleben.
264
+ Massgeblich ist, was der Baum ZEIGT, nicht die Datenlage - ein
265
+ Ordner, unter dem nur verborgene Datenpunkte liegen (ActivePage
266
+ mit drei states), traegt keinen Pfeil und gehoert nach unten.
267
+ Dieselbe Regel wie beim Zeichnen der Kinder, eine Ebene tiefer. */
268
+ function klappbar(k) {
269
+ return Object.keys(k.kinder).some(function (n) {
270
+ var c = k.kinder[n];
271
+ return c._zust && !(c.art === 'state' && !Object.keys(c.kinder).length);
272
+ });
273
+ }
274
+ reihen.sort(function (a, b) {
275
+ var ba = klappbar(a.k) ? 0 : 1;
276
+ var bb = klappbar(b.k) ? 0 : 1;
277
+ if (ba !== bb) { return ba - bb; }
278
+ return String(a.zus || a.k.name).localeCompare(String(b.zus || b.k.name),
279
+ undefined, { numeric: true, sensitivity: 'base' });
280
+ });
281
+
282
+ reihen.forEach(function (reihe) {
283
+ var k = reihe.k;
284
+ if (!k._zust) { return; }
285
+ /* Zustaende zeigt die rechte Seite, nicht der Baum - es sei denn,
286
+ unter dem Zustand liegt noch etwas. Dann ist er ein Behaelter und
287
+ muss sichtbar sein, sonst hilft es nichts, dass `istWaehlbar` ihn
288
+ zulaesst.
289
+
290
+ Gefragt wird nach Kindern **jeder Art**, nicht nach direkten
291
+ Datenpunkten. Der Unterschied ist gross: `mqtt-client.0.SmartHome`
292
+ ist ein `state` und traegt 804 Objekte unter sich - aber kein
293
+ einziges direkt, denn MQTT legt fuer Zwischenebenen gar keine
294
+ Objekte an. Die erste Fassung fragte nach direkten Datenpunkten
295
+ und liess damit den ganzen Zweig verschwinden. Sichtbarkeit und
296
+ Waehlbarkeit sind zwei Fragen: sichtbar ist, was Kinder hat;
297
+ waehlbar erst, was eigene Datenpunkte hat (siehe `istWaehlbar`). */
298
+ if (k.art === 'state' && tiefe > 0 && !Object.keys(k.kinder).length) { return; }
299
+
300
+ /* Der Filter sucht auch im Namen. Wer „Badezimmer" tippt, meint
301
+ FK_Badezimmer — und nicht die Seriennummer, die er nicht kennt. */
302
+ var passt = !filter || k.id.toLowerCase().indexOf(filter) !== -1 ||
303
+ (reihe.zus && reihe.zus.toLowerCase().indexOf(filter) !== -1);
304
+ var kinderUl = renderKinder(k, filter, tiefe + 1);
305
+ var kinderPassen = kinderUl.childNodes.length > 0;
306
+ if (!passt && !kinderPassen) { return; }
307
+
308
+ var li = el('li');
309
+ var waehlbar = istWaehlbar(k);
310
+ var d = el('div', 'node' + (waehlbar ? ' pick' : ' grp') + (k.id === S.current ? ' sel' : ''));
311
+
312
+ var hatKinder = kinderUl.childNodes.length > 0;
313
+ var vorgabe = (S.alleKlapp === null) ? (tiefe <= 1) : S.alleKlapp;
314
+ /* Im Aliasbaum gibt es unterhalb der Wurzel nur eigene Ordner - ganz
315
+ zugeklappt bliebe eine einzige Zeile `alias` uebrig, und jeder Weg
316
+ begaenne mit zwei Pflichtklicks. Bis `alias.0` bleibt deshalb offen.
317
+ Das steht hier und nicht im Zuklapp-Knopf: sonst gilt es nur, wenn
318
+ man IM Aliasmodus zuklappt - wer in den Quellen zuklappt und dann
319
+ herueberwechselt, fand den Baum zu weit zu (Ricardo, 25.08.2026).
320
+ Einzeln zuklappen darf man sie weiterhin, `S.aufgeklappt` gewinnt. */
321
+ if (S.baumModus === 'aliase' && (k.id === 'alias' || k.id === 'alias.0')) {
322
+ vorgabe = true;
323
+ }
324
+ var auf = filter ? true
325
+ : (S.aufgeklappt[k.id] !== undefined ? S.aufgeklappt[k.id] : vorgabe);
326
+ var tw = el('span', 'tw', hatKinder ? (auf ? '▾' : '▸') : '');
327
+ if (hatKinder) {
328
+ tw.addEventListener('click', function (e) {
329
+ e.stopPropagation();
330
+ S.aufgeklappt[k.id] = !auf;
331
+ merkeKlappstand();
332
+ zeichneBaum();
333
+ });
334
+ }
335
+ d.appendChild(tw);
336
+ var kk = el('span', 'kind', k.art ? k.art.slice(0, 3) : '·');
337
+ if (!k.art) { kk.title = tr('tree.noObjectHint'); }
338
+ d.appendChild(kk);
339
+ d.appendChild(el('span', 'idstueck', k.name));
340
+ if (reihe.zus) {
341
+ var nm2 = el('span', 'nam', reihe.zus);
342
+ d.appendChild(nm2);
343
+ d.title = k.id + ' · ' + reihe.zus;
344
+ }
345
+ if (waehlbar) {
346
+ if (wohlGeraet(k)) {
347
+ var pk = el('span', 'geraetepunkt', '●');
348
+ pk.title = tr('tree.looksLikeDevice');
349
+ d.appendChild(pk);
350
+ }
351
+ d.appendChild(el('span', 'cnt', k._zust + ''));
352
+ }
353
+
354
+ if (waehlbar) {
355
+ d.tabIndex = 0;
356
+ d.setAttribute('role', 'button');
357
+ var go = function () { waehle(k.id); };
358
+ d.addEventListener('click', go);
359
+ d.addEventListener('keydown', function (e) {
360
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); }
361
+ });
362
+ }
363
+ li.appendChild(d);
364
+ if (hatKinder && auf) { li.appendChild(kinderUl); }
365
+ ul.appendChild(li);
366
+ });
367
+ return ul;
368
+ }
369
+
370
+ $('#q').addEventListener('input', zeichneBaum);
371
+
372
+ /* Alles auf, alles zu. Die einzeln gemerkten Knoten fallen dabei weg -
373
+ sonst bliebe ein Zweig zu, den man eben ausdruecklich zugeklappt hat,
374
+ und „alles auf" waere gelogen. */
375
+ function klappeAlle(auf) {
376
+ S.aufgeklappt = {};
377
+ S.alleKlapp = auf;
378
+ merkeKlappstand();
379
+ zeichneBaum();
380
+ /* Verschwindet die Auswahl beim Zuklappen aus dem Baum, muss sie weg -
381
+ sonst steht rechts ein Geraet, das links nicht mehr zu sehen ist
382
+ (Ricardo, 25.08.2026). Dieselbe Regel wie beim Moduswechsel und beim
383
+ Abschalten des Expertenmodus. Geprueft wird an der gezeichneten
384
+ Liste, nicht an der Datenlage: sichtbar ist, was im Baum steht. */
385
+ if (S.current && !auf && !imBaumSichtbar()) {
386
+ S.current = null;
387
+ S.entwurf = null;
388
+ S.openRow = null;
389
+ zeichneErgebnis();
390
+ }
391
+ }
392
+
393
+ /* Steht die Auswahl gerade als Zeile im Baum? Die gewaehlte Zeile traegt
394
+ `sel` - gibt es keine, ist sie zugeklappt und damit unsichtbar. */
395
+ function imBaumSichtbar() {
396
+ var baum = $('#tree');
397
+ return !baum || !!baum.querySelector('.node.sel');
398
+ }
399
+ var bAuf = $('#btn-auf'), bZu = $('#btn-zu');
400
+ if (bAuf) { bAuf.addEventListener('click', function () { klappeAlle(true); }); }
401
+ if (bZu) { bZu.addEventListener('click', function () { klappeAlle(false); }); }
402
+
403
+
404
+ export function setzeModus(m) {
405
+ S.baumModus = m;
406
+
407
+ /* Was im neuen Baum nicht vorkommt, darf rechts nicht stehenbleiben.
408
+
409
+ Vorher blieb `current` beim Wechsel unangetastet: Wer in den Quellen
410
+ einen Ordner gewaehlt hatte und auf „Aliase“ klickte, sah links den
411
+ Aliasbaum und rechts weiter den Ordner - eine Auswahl, die es in
412
+ diesem Baum gar nicht gibt, samt Knoepfen, die auf sie zeigen.
413
+ Umgekehrt genauso.
414
+
415
+ Der Vorlagenbaum ist ausgenommen: er hat eine eigene Ansicht, und
416
+ wer von dort zurueckwechselt, findet seine Auswahl wieder vor. */
417
+ if (m === 'quellen' && S.current && S.current.indexOf('alias.') === 0) {
418
+ S.current = null; S.entwurf = null; S.openRow = null;
419
+ }
420
+ if (m === 'aliase' && S.current && S.current.indexOf('alias.') !== 0) {
421
+ S.current = null; S.entwurf = null; S.openRow = null;
422
+ }
423
+ /* Beim Moduswechsel den Filter fallen lassen und die Geraeteknoepfe
424
+ zurueckholen. Sonst stand man im Vorlagenbaum vor „nichts gefunden"
425
+ und sah nicht, dass noch ein Suchwort von vorhin wirkte. */
426
+ if (m !== S.baumModusVorher) {
427
+ var q0 = $('#q');
428
+ if (q0) { q0.value = ''; }
429
+ }
430
+ S.baumModusVorher = m;
431
+ if (m !== 'vorlagen') {
432
+ ['#btn-dry', '#btn-tpl', '#btn-checks'].forEach(function (w) {
433
+ var b = $(w);
434
+ if (!b) { return; }
435
+ /* „Als Vorlage speichern" gehoert zum Anlegen, nicht zum
436
+ Bearbeiten — im Aliasmodus bleibt er weg (Ricardo, 05.09.2026). */
437
+ b.hidden = (w === '#btn-tpl' && m === 'aliase');
438
+ });
439
+ }
440
+ $('#m-quellen').className = (m === 'quellen') ? 'an' : '';
441
+ $('#m-aliase').className = (m === 'aliase') ? 'an' : '';
442
+ $('#m-vorlagen').className = (m === 'vorlagen') ? 'an' : '';
443
+ var lbl = document.querySelector('.respane .panehead .lbl');
444
+ if (lbl) { lbl.textContent = (m === 'vorlagen') ? tr('tv.paneHead') : tr('result.header'); }
445
+ var ein = document.querySelector('#btn-einlesen');
446
+ if (ein) { ein.hidden = (m !== 'vorlagen'); }
447
+ zeichneBaum();
448
+ zeichneErgebnis();
449
+ }
450
+
451
+ $('#m-quellen').addEventListener('click', function () { setzeModus('quellen'); });
452
+ $('#m-aliase').addEventListener('click', function () { setzeModus('aliase'); });
453
+ $('#m-vorlagen').addEventListener('click', function () { setzeModus('vorlagen'); });