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.
- package/LICENSE +21 -0
- package/README.md +516 -0
- package/admin/alias-workbench.png +0 -0
- package/admin/css/werkbank.css +916 -0
- package/admin/detector.js +1 -0
- package/admin/geraetewissen/tasmota-befehle.json +116 -0
- package/admin/i18n/de.json +552 -0
- package/admin/i18n/en.json +552 -0
- package/admin/js/aufzaehlungen.js +332 -0
- package/admin/js/basis.js +36 -0
- package/admin/js/baum.js +453 -0
- package/admin/js/detail.js +335 -0
- package/admin/js/einstellungen.js +40 -0
- package/admin/js/entwurf.js +908 -0
- package/admin/js/enums.js +23 -0
- package/admin/js/ergebnis.js +934 -0
- package/admin/js/erkennung.js +126 -0
- package/admin/js/katalog.js +313 -0
- package/admin/js/mqtt.js +885 -0
- package/admin/js/musternamen.js +94 -0
- package/admin/js/musterwahl.js +334 -0
- package/admin/js/objekte.js +243 -0
- package/admin/js/pruefungen.js +130 -0
- package/admin/js/quellentausch.js +570 -0
- package/admin/js/rollenwahl.js +199 -0
- package/admin/js/schreiben.js +1537 -0
- package/admin/js/sprache.js +137 -0
- package/admin/js/start.js +39 -0
- package/admin/js/verbindung.js +10 -0
- package/admin/js/vorlagen.js +572 -0
- package/admin/js/vorlagenblatt.js +1907 -0
- package/admin/js/vorschlagen.js +391 -0
- package/admin/js/werte.js +193 -0
- package/admin/js/zielleiste.js +339 -0
- package/admin/js/zuordnung.js +695 -0
- package/admin/js/zustand.js +47 -0
- package/admin/js/zustandsliste.js +515 -0
- package/admin/jsonConfig.json +249 -0
- package/admin/tab.html +148 -0
- package/admin/vorlagen/hm-dimmer.json +204 -0
- package/admin/vorlagen/hm-fensterkontakt-hm.json +176 -0
- package/admin/vorlagen/hm-fensterkontakt-hmip.json +191 -0
- package/admin/vorlagen/hm-heizgruppe.json +214 -0
- package/admin/vorlagen/hm-heizkoerper.json +236 -0
- package/admin/vorlagen/hm-rollladen.json +190 -0
- package/admin/vorlagen/hm-schaltaktor-lampe.json +197 -0
- package/admin/vorlagen/hm-schaltaktor-mehrfach.json +203 -0
- package/admin/vorlagen/hm-schaltaktor-steckdose.json +197 -0
- package/admin/vorlagen/hm-temperatur-feuchte.json +166 -0
- package/admin/vorlagen/hm-wandthermostat.json +228 -0
- package/admin/vorlagen/index.json +21 -0
- package/admin/vorlagen/messpunkt.json +225 -0
- package/admin/vorlagen/tasmota-lampe.json +263 -0
- package/admin/vorlagen/tasmota-mehrfach.json +203 -0
- package/admin/vorlagen/tasmota-rgb.json +239 -0
- package/admin/vorlagen/tasmota-steckdose.json +265 -0
- package/io-package.json +122 -0
- package/package.json +48 -0
package/admin/js/baum.js
ADDED
|
@@ -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'); });
|