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) {
20 function optionText(option) {
21 return (option.textContent || option.innerText || option.value || "").trim();
24 function setSelected(select, value, selected) {
25 Array.from(select.options).forEach(option => {
26 if (option.value === value) {
27 option.selected = selected;
31 select.dispatchEvent(new Event("change", { bubbles: true }));
32 select.dispatchEvent(new Event("input", { bubbles: true }));
35 function createItem(option, side, move) {
36 const btn = document.createElement("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));
47 function buildMultiSelect(select) {
48 if (!select || select.dataset.dbxMultiselectReady === "1") return;
50 select.dataset.dbxMultiselectReady = "1";
51 select.classList.add("dbx-ms2-source");
53 const wrapper = document.createElement("div");
54 wrapper.className = "dbx-ms2";
55 wrapper.dataset.source = select.id || select.name || "";
57 const selectedCol = document.createElement("div");
58 selectedCol.className = "dbx-ms2-col";
60 const availableCol = document.createElement("div");
61 availableCol.className = "dbx-ms2-col";
63 const selectedTitle = document.createElement("div");
64 selectedTitle.className = "dbx-ms2-title";
65 selectedTitle.textContent = "Ausgewählt";
67 const availableTitle = document.createElement("div");
68 availableTitle.className = "dbx-ms2-title";
69 availableTitle.textContent = "Verfügbar";
71 const selectedList = document.createElement("div");
72 selectedList.className = "dbx-ms2-list";
74 const availableList = document.createElement("div");
75 availableList.className = "dbx-ms2-list";
77 selectedCol.appendChild(selectedTitle);
78 selectedCol.appendChild(selectedList);
79 availableCol.appendChild(availableTitle);
80 availableCol.appendChild(availableList);
82 wrapper.appendChild(selectedCol);
83 wrapper.appendChild(availableCol);
85 const render = () => {
86 selectedList.innerHTML = "";
87 availableList.innerHTML = "";
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);
98 available.push(option);
102 selected.forEach(option => selectedList.appendChild(createItem(option, "selected", move)));
103 available.forEach(option => availableList.appendChild(createItem(option, "available", move)));
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);
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);
120 function move(value, side) {
121 setSelected(select, value, side === "available");
125 select.addEventListener("change", render);
126 select.insertAdjacentElement("afterend", wrapper);
130 function findMultiSelectTargets(target, root) {
131 const key = String(target || "").trim();
132 const ctx = root || document;
133 const nodes = Array.from(ctx.querySelectorAll([
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");
145 if (!key) return nodes;
147 return nodes.filter(select => {
148 return select.id === key ||
149 select.id.indexOf(key + "_") === 0 ||
150 select.name === key ||
151 select.name === key + "[]";
155 function initMultiSelects(root, target) {
156 findMultiSelectTargets(target, root).forEach(buildMultiSelect);
159 window.dbxFormMultiselect = function(target) {
160 initMultiSelects(document, target);
163 window.multiselect2 = window.dbxFormMultiselect;
164 window.multiselect = window.dbxFormMultiselect;
166 function selectedValues(select) {
167 return Array.from(select.options)
168 .filter(option => option.selected && !option.disabled)
169 .map(option => option.value);
172 function syncSelect1Hidden(select, input) {
176 function emitSelect1Change(select) {
177 select.dispatchEvent(new Event("change", { bubbles: true }));
178 select.dispatchEvent(new Event("input", { bubbles: true }));
181 function toggleSelect1Value(select, value, selected) {
182 Array.from(select.options).forEach(option => {
183 if (option.value === value) option.selected = selected;
187 function buildSelect1(select) {
188 if (!select || select.dataset.dbxSelect1Ready === "1") return;
190 select.dataset.dbxSelect1Ready = "1";
191 select.classList.add("dbx-select1-source");
193 const wrapper = document.createElement("div");
194 wrapper.className = "dbx-select1";
195 wrapper.dataset.source = select.id || select.name || "";
197 const control = document.createElement("div");
198 control.className = "dbx-select1-control";
200 const chips = document.createElement("div");
201 chips.className = "dbx-select1-chips";
203 const input = document.createElement("input");
205 input.className = "dbx-select1-input";
206 input.autocomplete = "off";
207 input.placeholder = select.getAttribute("placeholder") || select.dataset.prompt || "Auswahl...";
209 const prompt = document.createElement("div");
210 prompt.className = "dbx-select1-prompt";
211 prompt.hidden = true;
213 control.appendChild(chips);
214 control.appendChild(input);
215 wrapper.appendChild(control);
216 wrapper.appendChild(prompt);
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;
230 const filter = input.value.trim().toLowerCase();
231 chips.innerHTML = "";
232 prompt.innerHTML = "";
234 const selected = selectedValues(select);
235 selected.forEach(value => {
236 const option = options().find(opt => opt.value === value);
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);
252 chips.appendChild(chip);
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);
261 matches.forEach(option => {
262 const row = document.createElement("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);
271 syncSelect1Hidden(select, input);
272 emitSelect1Change(select);
275 prompt.hidden = false;
277 prompt.appendChild(row);
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);
287 syncSelect1Hidden(select, input);
290 let closeOnControlClick = false;
292 input.addEventListener("focus", () => {
293 prompt.hidden = false;
296 input.addEventListener("input", () => {
297 prompt.hidden = false;
300 input.addEventListener("keydown", event => {
301 if (event.key === "Escape") {
302 prompt.hidden = true;
306 control.addEventListener("pointerdown", event => {
307 closeOnControlClick = !prompt.hidden && !event.target.closest(".dbx-select1-chip");
309 control.addEventListener("click", event => {
310 if (event.target.closest(".dbx-select1-chip")) return;
312 if (closeOnControlClick) {
313 prompt.hidden = true;
315 closeOnControlClick = false;
320 prompt.hidden = false;
322 select.addEventListener("change", render);
323 document.addEventListener("click", event => {
324 if (!wrapper.contains(event.target) && event.target !== select) {
325 prompt.hidden = true;
329 select.insertAdjacentElement("afterend", wrapper);
333 function findSelect1Targets(root) {
334 const ctx = root || document;
336 if (ctx.matches && ctx.matches("select.dbxSelect1,select[data-dbx-select1]")) {
339 ctx.querySelectorAll("select.dbxSelect1,select[data-dbx-select1]").forEach(select => nodes.push(select));
343 function initSelect1(root) {
344 findSelect1Targets(root || document).forEach(buildSelect1);
347 function initPasswordToggles(root) {
348 const ctx = root || document;
351 if (ctx.matches && ctx.matches("[data-dbx-password-toggle]")) {
355 ctx.querySelectorAll("[data-dbx-password-toggle]").forEach(button => buttons.push(button));
356 buttons.forEach(button => {
357 if (button.dataset.dbxPasswordToggleReady === "1") return;
359 const input = document.getElementById(button.dataset.dbxPasswordToggle || "");
362 button.dataset.dbxPasswordToggleReady = "1";
363 button.addEventListener("click", () => {
364 const show = input.type === "password";
365 const icon = button.querySelector("i");
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");
374 icon.classList.toggle("bi-eye", !show);
375 icon.classList.toggle("bi-eye-slash", show);
383 function initRoot(root) {
384 initPasswordToggles(root || document);
385 initSelect1(root || document);
386 initMultiSelects(root || document);
389 function registerFormFeature() {
390 if (!window.dbx || !window.dbx.feature || !window.dbx.feature.register) {
394 if (window.dbx.feature.has && window.dbx.feature.has("form")) {
398 window.dbx.feature.register("form", {
400 scope: "element", // 🔥 FIX
402 // 🔥 CSS über Core PREPARE
404 ['css', 'design', 'c-form.css']
414 // --------------------------------------------------
415 // INIT GUARD (pro Element)
416 // --------------------------------------------------
417 el.__dbxInitialized = el.__dbxInitialized || {};
418 if (el.__dbxInitialized["form"]) return;
419 el.__dbxInitialized["form"] = true;
423 // --------------------------------------------------
424 // GLOBAL EVENT (einmalig!)
425 // --------------------------------------------------
426 if (document.__dbxFormInit) return;
427 document.__dbxFormInit = true;
429 window.dbx.log("[form] init global handlers");
431 window.dbx.on("click", ".dbx-input-btn", function (e, btn) {
433 const wrap = btn.closest(".dbx-input");
436 const input = wrap.querySelector(".dbx-input-field");
439 const action = btn.dataset.action;
445 input.dispatchEvent(new Event("input", { bubbles: true }));
460 window.dbx.on("change", "select.dbxMultiSelect2", function () {
461 initMultiSelects(document);
464 window.dbx.on("change", ".dbxForm_wrapper select[multiple]", function () {
465 initMultiSelects(document);
475 if (!registerFormFeature()) {
477 const timer = window.setInterval(function () {
479 if (registerFormFeature() || tries > 100) {
480 window.clearInterval(timer);