3 * Clientseitige Formular-Erweiterungen fuer dbXapp.
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
12 * <form data-dbx="lib=form">
13 * <select multiple class="dbxMultiSelect2" name="groups[]">...</select>
17(function (window, document) {
21 const language = String(document.documentElement.lang || "de")
24 const translations = {
26 selected: "Ausgewählt",
27 available: "Verfügbar",
28 emptySelected: "Keine Auswahl",
29 emptyAvailable: "Keine weiteren Werte",
30 emptyValues: "Keine Werte"
34 available: "Available",
35 emptySelected: "Nothing selected",
36 emptyAvailable: "No more values",
37 emptyValues: "No values"
40 selected: "Seleccionados",
41 available: "Disponibles",
42 emptySelected: "Ninguna selección",
43 emptyAvailable: "No hay más valores",
44 emptyValues: "No hay valores"
47 return translations[language] || translations.de;
50 function optionText(option) {
51 return (option.textContent || option.innerText || option.value || "").trim();
54 function setSelected(select, value, selected) {
55 Array.from(select.options).forEach(option => {
56 if (option.value === value) {
57 option.selected = selected;
61 select.dispatchEvent(new Event("change", { bubbles: true }));
62 select.dispatchEvent(new Event("input", { bubbles: true }));
65 function createItem(option, side, move) {
66 const btn = document.createElement("button");
68 btn.className = "dbx-ms2-item";
69 btn.dataset.value = option.value;
70 btn.dataset.side = side;
71 btn.textContent = optionText(option);
72 btn.title = optionText(option);
73 btn.addEventListener("click", () => move(option.value, side));
77 function buildMultiSelect(select) {
78 if (!select || select.dataset.dbxMultiselectReady === "1") return;
79 const text = uiText();
81 select.dataset.dbxMultiselectReady = "1";
82 select.classList.add("dbx-ms2-source");
84 const wrapper = document.createElement("div");
85 wrapper.className = "dbx-ms2";
86 wrapper.dataset.source = select.id || select.name || "";
88 const selectedCol = document.createElement("div");
89 selectedCol.className = "dbx-ms2-col";
91 const availableCol = document.createElement("div");
92 availableCol.className = "dbx-ms2-col";
94 const selectedTitle = document.createElement("div");
95 selectedTitle.className = "dbx-ms2-title";
96 selectedTitle.textContent = text.selected;
98 const availableTitle = document.createElement("div");
99 availableTitle.className = "dbx-ms2-title";
100 availableTitle.textContent = text.available;
102 const selectedList = document.createElement("div");
103 selectedList.className = "dbx-ms2-list";
105 const availableList = document.createElement("div");
106 availableList.className = "dbx-ms2-list";
108 selectedCol.appendChild(selectedTitle);
109 selectedCol.appendChild(selectedList);
110 availableCol.appendChild(availableTitle);
111 availableCol.appendChild(availableList);
113 wrapper.appendChild(selectedCol);
114 wrapper.appendChild(availableCol);
116 const render = () => {
117 selectedList.innerHTML = "";
118 availableList.innerHTML = "";
121 const available = [];
123 Array.from(select.options).forEach(option => {
124 if (option.disabled) return;
125 if (select.multiple && (option.value === "" || (option.value === "0" && optionText(option).toLowerCase().indexOf("bitte") === 0))) return;
126 if (option.selected) {
127 selected.push(option);
129 available.push(option);
133 selected.forEach(option => selectedList.appendChild(createItem(option, "selected", move)));
134 available.forEach(option => availableList.appendChild(createItem(option, "available", move)));
136 if (!selected.length) {
137 const empty = document.createElement("div");
138 empty.className = "dbx-ms2-empty";
139 empty.textContent = text.emptySelected;
140 selectedList.appendChild(empty);
143 if (!available.length) {
144 const empty = document.createElement("div");
145 empty.className = "dbx-ms2-empty";
146 empty.textContent = text.emptyAvailable;
147 availableList.appendChild(empty);
151 function move(value, side) {
152 setSelected(select, value, side === "available");
156 select.addEventListener("change", render);
157 select.insertAdjacentElement("afterend", wrapper);
161 function findMultiSelectTargets(target, root) {
162 const key = String(target || "").trim();
163 const ctx = root || document;
164 const nodes = Array.from(ctx.querySelectorAll([
166 "select.dbxMultiSelect2",
167 "select[data-dbx-multiselect2]",
168 "select[data-dbx-multiselect]"
169 ].join(","))).filter(select => {
170 return !select.classList.contains("bsMultiSelect") &&
171 !select.classList.contains("sel-multible-line") &&
172 !select.classList.contains("dbxSelect1") &&
173 !select.hasAttribute("data-dbx-select1");
176 if (!key) return nodes;
178 return nodes.filter(select => {
179 return select.id === key ||
180 select.id.indexOf(key + "_") === 0 ||
181 select.name === key ||
182 select.name === key + "[]";
186 function initMultiSelects(root, target) {
187 findMultiSelectTargets(target, root).forEach(buildMultiSelect);
190 window.dbxFormMultiselect = function(target) {
191 initMultiSelects(document, target);
194 window.multiselect2 = window.dbxFormMultiselect;
195 window.multiselect = window.dbxFormMultiselect;
197 function selectedValues(select) {
198 return Array.from(select.options)
199 .filter(option => option.selected && !option.disabled)
200 .map(option => option.value);
203 function syncSelect1Hidden(select, input) {
207 function emitSelect1Change(select) {
208 select.dispatchEvent(new Event("change", { bubbles: true }));
209 select.dispatchEvent(new Event("input", { bubbles: true }));
212 function toggleSelect1Value(select, value, selected) {
213 Array.from(select.options).forEach(option => {
214 if (option.value === value) option.selected = selected;
218 function buildSelect1(select) {
219 if (!select || select.dataset.dbxSelect1Ready === "1") return;
221 select.dataset.dbxSelect1Ready = "1";
222 select.classList.add("dbx-select1-source");
224 const wrapper = document.createElement("div");
225 wrapper.className = "dbx-select1";
226 wrapper.dataset.source = select.id || select.name || "";
228 const control = document.createElement("div");
229 control.className = "dbx-select1-control";
231 const chips = document.createElement("div");
232 chips.className = "dbx-select1-chips";
234 const input = document.createElement("input");
236 input.className = "dbx-select1-input";
237 input.autocomplete = "off";
238 input.placeholder = select.getAttribute("placeholder") || select.dataset.prompt || "Auswahl...";
240 const prompt = document.createElement("div");
241 prompt.className = "dbx-select1-prompt";
242 prompt.hidden = true;
244 control.appendChild(chips);
245 control.appendChild(input);
246 wrapper.appendChild(control);
247 wrapper.appendChild(prompt);
250 return Array.from(select.options).filter(option => {
251 if (option.disabled) return false;
252 if (option.value === "" || option.value === "0") {
253 const text = optionText(option).toLowerCase();
254 return text && text.indexOf("bitte") !== 0 && text.indexOf("auswahl") !== 0;
261 const filter = input.value.trim().toLowerCase();
262 chips.innerHTML = "";
263 prompt.innerHTML = "";
265 const selected = selectedValues(select);
266 selected.forEach(value => {
267 const option = options().find(opt => opt.value === value);
270 const chip = document.createElement("button");
271 chip.type = "button";
272 chip.className = "dbx-select1-chip";
273 chip.dataset.value = value;
274 chip.title = "Abwählen";
275 chip.innerHTML = `<span>${optionText(option)}</span><i class="bi bi-x-lg" aria-hidden="true"></i>`;
276 chip.addEventListener("click", () => {
277 toggleSelect1Value(select, value, false);
278 syncSelect1Hidden(select, input);
279 emitSelect1Change(select);
283 chips.appendChild(chip);
286 const matches = options().filter(option => {
287 const text = optionText(option).toLowerCase();
288 const value = String(option.value || "").toLowerCase();
289 return !filter || text.includes(filter) || value.includes(filter);
292 matches.forEach(option => {
293 const row = document.createElement("button");
295 row.className = "dbx-select1-option";
296 if (option.selected) row.classList.add("is-selected");
297 row.dataset.value = option.value;
298 row.innerHTML = `<i class="bi ${option.selected ? "bi-check2-square" : "bi-square"}" aria-hidden="true"></i><span>${optionText(option)}</span>`;
299 row.addEventListener("click", () => {
300 toggleSelect1Value(select, option.value, !option.selected);
302 syncSelect1Hidden(select, input);
303 emitSelect1Change(select);
306 prompt.hidden = false;
308 prompt.appendChild(row);
311 if (!matches.length) {
312 const empty = document.createElement("div");
313 empty.className = "dbx-select1-empty";
314 empty.textContent = uiText().emptyValues;
315 prompt.appendChild(empty);
318 syncSelect1Hidden(select, input);
321 let closeOnControlClick = false;
323 input.addEventListener("focus", () => {
324 prompt.hidden = false;
327 input.addEventListener("input", () => {
328 prompt.hidden = false;
331 input.addEventListener("keydown", event => {
332 if (event.key === "Escape") {
333 prompt.hidden = true;
337 control.addEventListener("pointerdown", event => {
338 closeOnControlClick = !prompt.hidden && !event.target.closest(".dbx-select1-chip");
340 control.addEventListener("click", event => {
341 if (event.target.closest(".dbx-select1-chip")) return;
343 if (closeOnControlClick) {
344 prompt.hidden = true;
346 closeOnControlClick = false;
351 prompt.hidden = false;
353 select.addEventListener("change", render);
354 document.addEventListener("click", event => {
355 if (!wrapper.contains(event.target) && event.target !== select) {
356 prompt.hidden = true;
360 select.insertAdjacentElement("afterend", wrapper);
364 function findSelect1Targets(root) {
365 const ctx = root || document;
367 if (ctx.matches && ctx.matches("select.dbxSelect1,select[data-dbx-select1]")) {
370 ctx.querySelectorAll("select.dbxSelect1,select[data-dbx-select1]").forEach(select => nodes.push(select));
374 function initSelect1(root) {
375 findSelect1Targets(root || document).forEach(buildSelect1);
378 function initPasswordToggles(root) {
379 const ctx = root || document;
382 if (ctx.matches && ctx.matches("[data-dbx-password-toggle]")) {
386 ctx.querySelectorAll("[data-dbx-password-toggle]").forEach(button => buttons.push(button));
387 buttons.forEach(button => {
388 if (button.dataset.dbxPasswordToggleReady === "1") return;
390 const input = document.getElementById(button.dataset.dbxPasswordToggle || "");
393 button.dataset.dbxPasswordToggleReady = "1";
394 button.addEventListener("click", () => {
395 const show = input.type === "password";
396 const icon = button.querySelector("i");
398 input.type = show ? "text" : "password";
399 input.dataset.dbxPasswordVisible = show ? "1" : "0";
400 button.setAttribute("aria-pressed", show ? "true" : "false");
401 button.setAttribute("aria-label", show ? "Passwort verbergen" : "Passwort anzeigen");
402 button.setAttribute("title", show ? "Passwort verbergen" : "Passwort anzeigen");
405 icon.classList.toggle("bi-eye", !show);
406 icon.classList.toggle("bi-eye-slash", show);
414 function initRoot(root) {
415 initPasswordToggles(root || document);
416 initSelect1(root || document);
417 initMultiSelects(root || document);
420 function registerFormFeature() {
421 if (!window.dbx || !window.dbx.feature || !window.dbx.feature.register) {
425 if (window.dbx.feature.has && window.dbx.feature.has("form")) {
429 window.dbx.feature.register("form", {
431 scope: "element", // 🔥 FIX
433 // 🔥 CSS über Core PREPARE
435 ['css', 'design', 'c-form.css']
445 // --------------------------------------------------
446 // INIT GUARD (pro Element)
447 // --------------------------------------------------
448 el.__dbxInitialized = el.__dbxInitialized || {};
449 if (el.__dbxInitialized["form"]) return;
450 el.__dbxInitialized["form"] = true;
454 // --------------------------------------------------
455 // GLOBAL EVENT (einmalig!)
456 // --------------------------------------------------
457 if (document.__dbxFormInit) return;
458 document.__dbxFormInit = true;
460 window.dbx.log("[form] init global handlers");
462 window.dbx.on("click", ".dbx-input-btn", function (e, btn) {
464 const wrap = btn.closest(".dbx-input");
467 const input = wrap.querySelector(".dbx-input-field");
470 const action = btn.dataset.action;
476 input.dispatchEvent(new Event("input", { bubbles: true }));
491 window.dbx.on("change", "select.dbxMultiSelect2", function () {
492 initMultiSelects(document);
495 window.dbx.on("change", ".dbxForm_wrapper select[multiple]", function () {
496 initMultiSelects(document);
506 if (!registerFormFeature()) {
508 const timer = window.setInterval(function () {
510 if (registerFormFeature() || tries > 100) {
511 window.clearInterval(timer);