dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
form.js
Go to the documentation of this file.
1/**
2 * @file form.js
3 * Clientseitige Formular-Erweiterungen fuer dbXapp.
4 *
5 * Diese Lib ist bewusst UI-nah und kapselt wiederverwendbare Form-Features:
6 * - Multi-Select-Darstellung fuer `select[multiple]`
7 * - Select1-UI fuer kompakte Auswahlfelder
8 * - Passwortanzeigen
9 *
10 * Beispiel:
11 * ```html
12 * <form data-dbx="lib=form">
13 * <select multiple class="dbxMultiSelect2" name="groups[]">...</select>
14 * </form>
15 * ```
16 */
17(function (window, document) {
18 "use strict";
19
20 function optionText(option) {
21 return (option.textContent || option.innerText || option.value || "").trim();
22 }
23
24 function setSelected(select, value, selected) {
25 Array.from(select.options).forEach(option => {
26 if (option.value === value) {
27 option.selected = selected;
28 }
29 });
30
31 select.dispatchEvent(new Event("change", { bubbles: true }));
32 select.dispatchEvent(new Event("input", { bubbles: true }));
33 }
34
35 function createItem(option, side, move) {
36 const btn = document.createElement("button");
37 btn.type = "button";
38 btn.className = "dbx-ms2-item";
39 btn.dataset.value = option.value;
40 btn.dataset.side = side;
41 btn.textContent = optionText(option);
42 btn.title = optionText(option);
43 btn.addEventListener("click", () => move(option.value, side));
44 return btn;
45 }
46
47 function buildMultiSelect(select) {
48 if (!select || select.dataset.dbxMultiselectReady === "1") return;
49
50 select.dataset.dbxMultiselectReady = "1";
51 select.classList.add("dbx-ms2-source");
52
53 const wrapper = document.createElement("div");
54 wrapper.className = "dbx-ms2";
55 wrapper.dataset.source = select.id || select.name || "";
56
57 const selectedCol = document.createElement("div");
58 selectedCol.className = "dbx-ms2-col";
59
60 const availableCol = document.createElement("div");
61 availableCol.className = "dbx-ms2-col";
62
63 const selectedTitle = document.createElement("div");
64 selectedTitle.className = "dbx-ms2-title";
65 selectedTitle.textContent = "Ausgewählt";
66
67 const availableTitle = document.createElement("div");
68 availableTitle.className = "dbx-ms2-title";
69 availableTitle.textContent = "Verfügbar";
70
71 const selectedList = document.createElement("div");
72 selectedList.className = "dbx-ms2-list";
73
74 const availableList = document.createElement("div");
75 availableList.className = "dbx-ms2-list";
76
77 selectedCol.appendChild(selectedTitle);
78 selectedCol.appendChild(selectedList);
79 availableCol.appendChild(availableTitle);
80 availableCol.appendChild(availableList);
81
82 wrapper.appendChild(selectedCol);
83 wrapper.appendChild(availableCol);
84
85 const render = () => {
86 selectedList.innerHTML = "";
87 availableList.innerHTML = "";
88
89 const selected = [];
90 const available = [];
91
92 Array.from(select.options).forEach(option => {
93 if (option.disabled) return;
94 if (select.multiple && (option.value === "" || (option.value === "0" && optionText(option).toLowerCase().indexOf("bitte") === 0))) return;
95 if (option.selected) {
96 selected.push(option);
97 } else {
98 available.push(option);
99 }
100 });
101
102 selected.forEach(option => selectedList.appendChild(createItem(option, "selected", move)));
103 available.forEach(option => availableList.appendChild(createItem(option, "available", move)));
104
105 if (!selected.length) {
106 const empty = document.createElement("div");
107 empty.className = "dbx-ms2-empty";
108 empty.textContent = "Keine Auswahl";
109 selectedList.appendChild(empty);
110 }
111
112 if (!available.length) {
113 const empty = document.createElement("div");
114 empty.className = "dbx-ms2-empty";
115 empty.textContent = "Keine weiteren Werte";
116 availableList.appendChild(empty);
117 }
118 };
119
120 function move(value, side) {
121 setSelected(select, value, side === "available");
122 render();
123 }
124
125 select.addEventListener("change", render);
126 select.insertAdjacentElement("afterend", wrapper);
127 render();
128 }
129
130 function findMultiSelectTargets(target, root) {
131 const key = String(target || "").trim();
132 const ctx = root || document;
133 const nodes = Array.from(ctx.querySelectorAll([
134 "select[multiple]",
135 "select.dbxMultiSelect2",
136 "select[data-dbx-multiselect2]",
137 "select[data-dbx-multiselect]"
138 ].join(","))).filter(select => {
139 return !select.classList.contains("bsMultiSelect") &&
140 !select.classList.contains("sel-multible-line") &&
141 !select.classList.contains("dbxSelect1") &&
142 !select.hasAttribute("data-dbx-select1");
143 });
144
145 if (!key) return nodes;
146
147 return nodes.filter(select => {
148 return select.id === key ||
149 select.id.indexOf(key + "_") === 0 ||
150 select.name === key ||
151 select.name === key + "[]";
152 });
153 }
154
155 function initMultiSelects(root, target) {
156 findMultiSelectTargets(target, root).forEach(buildMultiSelect);
157 }
158
159 window.dbxFormMultiselect = function(target) {
160 initMultiSelects(document, target);
161 };
162
163 window.multiselect2 = window.dbxFormMultiselect;
164 window.multiselect = window.dbxFormMultiselect;
165
166 function selectedValues(select) {
167 return Array.from(select.options)
168 .filter(option => option.selected && !option.disabled)
169 .map(option => option.value);
170 }
171
172 function syncSelect1Hidden(select, input) {
173 input.value = "";
174 }
175
176 function emitSelect1Change(select) {
177 select.dispatchEvent(new Event("change", { bubbles: true }));
178 select.dispatchEvent(new Event("input", { bubbles: true }));
179 }
180
181 function toggleSelect1Value(select, value, selected) {
182 Array.from(select.options).forEach(option => {
183 if (option.value === value) option.selected = selected;
184 });
185 }
186
187 function buildSelect1(select) {
188 if (!select || select.dataset.dbxSelect1Ready === "1") return;
189
190 select.dataset.dbxSelect1Ready = "1";
191 select.classList.add("dbx-select1-source");
192
193 const wrapper = document.createElement("div");
194 wrapper.className = "dbx-select1";
195 wrapper.dataset.source = select.id || select.name || "";
196
197 const control = document.createElement("div");
198 control.className = "dbx-select1-control";
199
200 const chips = document.createElement("div");
201 chips.className = "dbx-select1-chips";
202
203 const input = document.createElement("input");
204 input.type = "text";
205 input.className = "dbx-select1-input";
206 input.autocomplete = "off";
207 input.placeholder = select.getAttribute("placeholder") || select.dataset.prompt || "Auswahl...";
208
209 const prompt = document.createElement("div");
210 prompt.className = "dbx-select1-prompt";
211 prompt.hidden = true;
212
213 control.appendChild(chips);
214 control.appendChild(input);
215 wrapper.appendChild(control);
216 wrapper.appendChild(prompt);
217
218 function options() {
219 return Array.from(select.options).filter(option => {
220 if (option.disabled) return false;
221 if (option.value === "" || option.value === "0") {
222 const text = optionText(option).toLowerCase();
223 return text && text.indexOf("bitte") !== 0 && text.indexOf("auswahl") !== 0;
224 }
225 return true;
226 });
227 }
228
229 function render() {
230 const filter = input.value.trim().toLowerCase();
231 chips.innerHTML = "";
232 prompt.innerHTML = "";
233
234 const selected = selectedValues(select);
235 selected.forEach(value => {
236 const option = options().find(opt => opt.value === value);
237 if (!option) return;
238
239 const chip = document.createElement("button");
240 chip.type = "button";
241 chip.className = "dbx-select1-chip";
242 chip.dataset.value = value;
243 chip.title = "Abwählen";
244 chip.innerHTML = `<span>${optionText(option)}</span><i class="bi bi-x-lg" aria-hidden="true"></i>`;
245 chip.addEventListener("click", () => {
246 toggleSelect1Value(select, value, false);
247 syncSelect1Hidden(select, input);
248 emitSelect1Change(select);
249 render();
250 input.focus();
251 });
252 chips.appendChild(chip);
253 });
254
255 const matches = options().filter(option => {
256 const text = optionText(option).toLowerCase();
257 const value = String(option.value || "").toLowerCase();
258 return !filter || text.includes(filter) || value.includes(filter);
259 });
260
261 matches.forEach(option => {
262 const row = document.createElement("button");
263 row.type = "button";
264 row.className = "dbx-select1-option";
265 if (option.selected) row.classList.add("is-selected");
266 row.dataset.value = option.value;
267 row.innerHTML = `<i class="bi ${option.selected ? "bi-check2-square" : "bi-square"}" aria-hidden="true"></i><span>${optionText(option)}</span>`;
268 row.addEventListener("click", () => {
269 toggleSelect1Value(select, option.value, !option.selected);
270 input.value = "";
271 syncSelect1Hidden(select, input);
272 emitSelect1Change(select);
273 render();
274 input.focus();
275 prompt.hidden = false;
276 });
277 prompt.appendChild(row);
278 });
279
280 if (!matches.length) {
281 const empty = document.createElement("div");
282 empty.className = "dbx-select1-empty";
283 empty.textContent = "Keine Werte";
284 prompt.appendChild(empty);
285 }
286
287 syncSelect1Hidden(select, input);
288 }
289
290 let closeOnControlClick = false;
291
292 input.addEventListener("focus", () => {
293 prompt.hidden = false;
294 render();
295 });
296 input.addEventListener("input", () => {
297 prompt.hidden = false;
298 render();
299 });
300 input.addEventListener("keydown", event => {
301 if (event.key === "Escape") {
302 prompt.hidden = true;
303 input.blur();
304 }
305 });
306 control.addEventListener("pointerdown", event => {
307 closeOnControlClick = !prompt.hidden && !event.target.closest(".dbx-select1-chip");
308 });
309 control.addEventListener("click", event => {
310 if (event.target.closest(".dbx-select1-chip")) return;
311
312 if (closeOnControlClick) {
313 prompt.hidden = true;
314 input.blur();
315 closeOnControlClick = false;
316 return;
317 }
318
319 input.focus();
320 prompt.hidden = false;
321 });
322 select.addEventListener("change", render);
323 document.addEventListener("click", event => {
324 if (!wrapper.contains(event.target) && event.target !== select) {
325 prompt.hidden = true;
326 }
327 });
328
329 select.insertAdjacentElement("afterend", wrapper);
330 render();
331 }
332
333 function findSelect1Targets(root) {
334 const ctx = root || document;
335 const nodes = [];
336 if (ctx.matches && ctx.matches("select.dbxSelect1,select[data-dbx-select1]")) {
337 nodes.push(ctx);
338 }
339 ctx.querySelectorAll("select.dbxSelect1,select[data-dbx-select1]").forEach(select => nodes.push(select));
340 return nodes;
341 }
342
343 function initSelect1(root) {
344 findSelect1Targets(root || document).forEach(buildSelect1);
345 }
346
347 function initPasswordToggles(root) {
348 const ctx = root || document;
349 const buttons = [];
350
351 if (ctx.matches && ctx.matches("[data-dbx-password-toggle]")) {
352 buttons.push(ctx);
353 }
354
355 ctx.querySelectorAll("[data-dbx-password-toggle]").forEach(button => buttons.push(button));
356 buttons.forEach(button => {
357 if (button.dataset.dbxPasswordToggleReady === "1") return;
358
359 const input = document.getElementById(button.dataset.dbxPasswordToggle || "");
360 if (!input) return;
361
362 button.dataset.dbxPasswordToggleReady = "1";
363 button.addEventListener("click", () => {
364 const show = input.type === "password";
365 const icon = button.querySelector("i");
366
367 input.type = show ? "text" : "password";
368 input.dataset.dbxPasswordVisible = show ? "1" : "0";
369 button.setAttribute("aria-pressed", show ? "true" : "false");
370 button.setAttribute("aria-label", show ? "Passwort verbergen" : "Passwort anzeigen");
371 button.setAttribute("title", show ? "Passwort verbergen" : "Passwort anzeigen");
372
373 if (icon) {
374 icon.classList.toggle("bi-eye", !show);
375 icon.classList.toggle("bi-eye-slash", show);
376 }
377
378 input.focus();
379 });
380 });
381 }
382
383 function initRoot(root) {
384 initPasswordToggles(root || document);
385 initSelect1(root || document);
386 initMultiSelects(root || document);
387 }
388
389 function registerFormFeature() {
390 if (!window.dbx || !window.dbx.feature || !window.dbx.feature.register) {
391 return false;
392 }
393
394 if (window.dbx.feature.has && window.dbx.feature.has("form")) {
395 return true;
396 }
397
398 window.dbx.feature.register("form", {
399
400 scope: "element", // 🔥 FIX
401
402 // 🔥 CSS über Core PREPARE
403 css: [
404 ['css', 'design', 'c-form.css']
405 ],
406
407 // 🔥 Prio
408 priority: "mid",
409
410 init(el, config) {
411
412 if (!el) return;
413
414 // --------------------------------------------------
415 // INIT GUARD (pro Element)
416 // --------------------------------------------------
417 el.__dbxInitialized = el.__dbxInitialized || {};
418 if (el.__dbxInitialized["form"]) return;
419 el.__dbxInitialized["form"] = true;
420
421 initRoot(el);
422
423 // --------------------------------------------------
424 // GLOBAL EVENT (einmalig!)
425 // --------------------------------------------------
426 if (document.__dbxFormInit) return;
427 document.__dbxFormInit = true;
428
429 window.dbx.log("[form] init global handlers");
430
431 window.dbx.on("click", ".dbx-input-btn", function (e, btn) {
432
433 const wrap = btn.closest(".dbx-input");
434 if (!wrap) return;
435
436 const input = wrap.querySelector(".dbx-input-field");
437 if (!input) return;
438
439 const action = btn.dataset.action;
440
441 switch (action) {
442
443 case "clear":
444 input.value = "";
445 input.dispatchEvent(new Event("input", { bubbles: true }));
446 input.focus();
447 break;
448
449 case "calendar":
450 // später
451 break;
452
453 case "lookup":
454 // später
455 break;
456 }
457
458 });
459
460 window.dbx.on("change", "select.dbxMultiSelect2", function () {
461 initMultiSelects(document);
462 });
463
464 window.dbx.on("change", ".dbxForm_wrapper select[multiple]", function () {
465 initMultiSelects(document);
466 });
467
468 }
469
470 });
471
472 return true;
473 }
474
475 if (!registerFormFeature()) {
476 let tries = 0;
477 const timer = window.setInterval(function () {
478 tries++;
479 if (registerFormFeature() || tries > 100) {
480 window.clearInterval(timer);
481 }
482 }, 25);
483 }
484
485})(window, document);