2 * @file modulesAdmin.js
3 * Modul-Verwaltung: DD-Gallery, Modul-Bilder (CMS-Medienbrowser), openWin
5(function (window, document) {
8 if (!window.dbx || !window.dbx.feature) {
12 const dbx = window.dbx;
13 const LIB = "modulesAdmin";
14 const PANEL_UI_ID = "modules-admin";
16 function qs(el, sel) {
17 return el ? el.querySelector(sel) : null;
20 function qsa(el, sel) {
21 return el ? Array.prototype.slice.call(el.querySelectorAll(sel)) : [];
24 function modimgMediaCfg(root) {
25 root = root || document.querySelector(".dbx-modules-admin") || document;
27 media: root.getAttribute("data-modimg-media") || "",
28 upload: root.getAttribute("data-modimg-upload") || "",
29 mediafolders: root.getAttribute("data-modimg-mediafolders") || "",
30 mediafoldercreate: root.getAttribute("data-modimg-mediafoldercreate") || "",
31 mediafolderdelete: root.getAttribute("data-modimg-mediafolderdelete") || "",
32 deletemedia: root.getAttribute("data-modimg-deletemedia") || "",
33 uploadmax: root.getAttribute("data-modimg-uploadmax") || ""
37 function panelMediaCfg(panel) {
38 const cfg = modimgMediaCfg(panel.closest(".dbx-modules-admin") || panel.closest(".dbx-modimg-manager") || document);
39 if (panel.getAttribute("data-modimg-media")) {
40 cfg.media = panel.getAttribute("data-modimg-media");
42 if (panel.getAttribute("data-modimg-upload")) {
43 cfg.upload = panel.getAttribute("data-modimg-upload");
45 if (panel.getAttribute("data-modimg-mediafolders")) {
46 cfg.mediafolders = panel.getAttribute("data-modimg-mediafolders");
48 if (panel.getAttribute("data-modimg-mediafoldercreate")) {
49 cfg.mediafoldercreate = panel.getAttribute("data-modimg-mediafoldercreate");
51 if (panel.getAttribute("data-modimg-mediafolderdelete")) {
52 cfg.mediafolderdelete = panel.getAttribute("data-modimg-mediafolderdelete");
54 if (panel.getAttribute("data-modimg-deletemedia")) {
55 cfg.deletemedia = panel.getAttribute("data-modimg-deletemedia");
57 if (panel.getAttribute("data-modimg-uploadmax")) {
58 cfg.uploadmax = panel.getAttribute("data-modimg-uploadmax");
63 function ensureCmsMediaBrowser() {
64 if (dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function") {
65 return Promise.resolve(true);
67 return new Promise(function (resolve) {
73 ["js", "lib", "ajax.js"],
74 ["js", "lib", "openWin.js"],
75 ["js", "lib", "cms.js"]
77 resolve(!!(dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function"));
82 function getActiveDdItem(galleryId) {
83 const gallery = document.getElementById(galleryId);
87 return gallery.querySelector(".dbx-module-dd-item.is-active") || gallery.querySelector(".dbx-module-dd-item");
90 function openDdItem(item) {
91 if (!item || !dbx.openWin) {
94 const url = item.getAttribute("data-edit-url") || "";
95 const title = item.getAttribute("data-edit-title") || "DD";
104 position: "center-top",
110 function bindModuleDdGallery(root) {
111 const scope = root || document;
113 scope.querySelectorAll(".dbx-module-dd-gallery").forEach(function (gallery) {
114 if (gallery.dataset.ddBound === "1") {
117 gallery.dataset.ddBound = "1";
119 gallery.querySelectorAll(".dbx-module-dd-item").forEach(function (item) {
120 item.addEventListener("click", function () {
121 gallery.querySelectorAll(".dbx-module-dd-item").forEach(function (node) {
122 node.classList.remove("is-active");
123 node.setAttribute("aria-selected", "false");
125 item.classList.add("is-active");
126 item.setAttribute("aria-selected", "true");
127 const panel = gallery.closest(".dbx-module-dd-gallery-panel");
128 const btn = panel ? panel.querySelector(".dbx-module-dd-open-btn") : null;
130 btn.setAttribute("data-dd-active", item.getAttribute("data-edit-url") || "");
133 item.addEventListener("dblclick", function () {
139 scope.querySelectorAll(".dbx-module-dd-open-btn").forEach(function (btn) {
140 if (btn.dataset.ddBound === "1") {
143 btn.dataset.ddBound = "1";
144 const galleryId = btn.getAttribute("data-dd-select") || "";
145 btn.addEventListener("click", function () {
146 const item = getActiveDdItem(galleryId);
152 function fetchJson(url, options) {
153 options = options || {};
154 if (!dbx.ajax || typeof dbx.ajax.request !== "function") {
155 return Promise.reject(new Error("ajax.js nicht geladen."));
158 return dbx.ajax.request({
160 method: options.method || "GET",
162 body: (typeof options.body !== "undefined") ? options.body : null,
163 headers: options.headers || {},
164 timeout: Number(options.timeout || 20000)
168 function updatePanelPreview(panel, items) {
169 const previewCol = qs(panel, "[data-module-images-preview]");
173 const modul = panel.getAttribute("data-modul") || "";
174 const placeholder = panel.getAttribute("data-placeholder-url") || "";
175 const placeholderAlt = panel.getAttribute("data-placeholder-alt") || modul;
178 let isPlaceholder = true;
179 if (items && items.length) {
180 url = items[0].url || "";
181 label = items[0].label || items[0].file || modul;
182 isPlaceholder = false;
188 previewCol.innerHTML = '<img src="' + escapeAttr(url) + '" alt="' + escapeAttr(isPlaceholder ? placeholderAlt : label) + '" class="dbx-module-images-preview-img' + (isPlaceholder ? " is-placeholder" : "") + '" loading="lazy">';
191 previewCol.innerHTML = '<span class="dbx-module-images-placeholder-icon" aria-hidden="true"><i class="bi bi-box-seam"></i></span>';
194 function updateSymbolPreview(panel, symbol) {
195 const previewCol = qs(panel, "[data-module-symbol-preview]");
199 const modul = panel.getAttribute("data-modul") || "";
200 const placeholder = panel.getAttribute("data-placeholder-url") || "";
201 const placeholderAlt = panel.getAttribute("data-placeholder-alt") || modul;
202 symbol = symbol || {};
203 const url = symbol.url || placeholder;
204 const alt = symbol.alt || (symbol.url ? modul : placeholderAlt);
206 previewCol.innerHTML = '<img src="' + escapeAttr(url) + '" alt="' + escapeAttr(alt) + '" class="dbx-module-symbol-img' + (symbol.url ? "" : " is-placeholder") + '" loading="lazy">';
209 previewCol.innerHTML = '<span class="dbx-module-images-placeholder-icon" aria-hidden="true"><i class="bi bi-box-seam"></i></span>';
212 function renderModuleImagesEmptyListHtml() {
213 return '<div class="dbx-module-images-empty-list text-muted small">Keine Modulbilder</div>';
216 const MOD_FILENAME_SEP = "__";
218 function stemForRuns(modul, run1, run2) {
219 let name = String(modul || "").trim();
220 run1 = String(run1 || "").trim();
221 run2 = String(run2 || "").trim();
222 if (!name || !run1) {
225 name += MOD_FILENAME_SEP + run1;
227 name += MOD_FILENAME_SEP + run2;
232 function buildModuleImageCall(modul, params) {
233 const call = "dbx_modul=" + encodeURIComponent(modul || "");
235 return call + "&" + params;
240 function buildModulPreviewUrl(modul, run1, run2, params, panel) {
241 run1 = String(run1 || "").trim();
242 run2 = String(run2 || "").trim();
243 params = String(params || "");
244 if ((!run1 || !run2) && params) {
245 const m1 = params.match(/(?:^|&)dbx_run1=([^&]+)/);
246 const m2 = params.match(/(?:^|&)dbx_run2=([^&]+)/);
248 run1 = decodeURIComponent(m1[1].replace(/\+/g, " "));
251 run2 = decodeURIComponent(m2[1].replace(/\+/g, " "));
254 if (!run1 && panel) {
255 run1 = String(panel.getAttribute("data-default-run1") || "").trim();
257 if (!run2 && panel) {
258 run2 = String(panel.getAttribute("data-default-run2") || "").trim();
260 let url = "?dbx_modul=" + encodeURIComponent(modul || "");
262 url += "&dbx_run1=" + encodeURIComponent(run1);
265 url += "&dbx_run2=" + encodeURIComponent(run2);
267 return { url: url, run1: run1, run2: run2 };
270 function setActiveGalleryItem(panel, item) {
271 const list = qs(panel, "[data-module-images-list]");
275 qsa(list, ".dbx-module-images-item").forEach(function (el) {
276 el.classList.toggle("is-active", item && el === item);
280 function escapeHtml(text) {
281 return String(text || "")
282 .replace(/&/g, "&")
283 .replace(/</g, "<")
284 .replace(/>/g, ">")
285 .replace(/"/g, """);
288 function escapeAttr(text) {
289 return escapeHtml(text);
292 function renderModuleImagesList(container, items) {
296 if (!items || !items.length) {
297 container.innerHTML = renderModuleImagesEmptyListHtml();
300 container.innerHTML = items.map(function (item, index) {
301 const file = item.file || "";
302 const url = item.url || "";
303 const label = item.label || file;
304 const params = item.default_params || "";
305 const panel = container.closest(".dbx-module-images-panel");
306 const modul = panel ? (panel.getAttribute("data-modul") || "") : "";
307 const preview = buildModulPreviewUrl(modul, item.run1 || "", item.run2 || "", params, panel);
308 const run1 = preview.run1;
309 const run2 = preview.run2;
310 const call = buildModuleImageCall(modul, params);
311 const previewTitle = "Vorschau: " + (modul || "");
312 const active = index === 0 ? " is-active" : "";
313 const previewBtn = (run1 || params)
314 ? '<a class="btn btn-outline-secondary btn-sm dbx-win dbx-module-images-preview" href="' + escapeAttr(preview.url) + '" data-url="' + escapeAttr(preview.url) + '" data-title="' + escapeAttr(previewTitle) + '" data-width="88%" data-height="88%" title="Modul aufrufen"><i class="bi bi-box-arrow-up-right"></i></a>'
316 return '<div class="dbx-module-images-item' + active + '" data-file="' + escapeAttr(file) + '" data-params="' + escapeAttr(params) + '" data-url="' + escapeAttr(url) + '" data-run1="' + escapeAttr(run1) + '" data-run2="' + escapeAttr(run2) + '">'
317 + '<span class="dbx-module-images-thumb"><img src="' + escapeAttr(url) + '" alt="' + escapeAttr(label) + '" loading="lazy"></span>'
318 + '<span class="dbx-module-images-meta"><code class="dbx-module-images-call">' + escapeHtml(call) + '</code>'
319 + '<span class="dbx-module-images-file">' + escapeHtml(file) + '</span></span>'
320 + '<span class="dbx-module-images-actions">'
322 + '<button type="button" class="btn btn-outline-danger btn-sm dbx-module-images-remove" title="Bild entfernen"><i class="bi bi-trash"></i></button>'
327 function getPanelRunTarget(panel) {
328 const run1El = qs(panel, ".dbx-module-images-run1");
329 const run2El = qs(panel, ".dbx-module-images-run2");
330 let run1 = run1El ? String(run1El.value || "").trim() : "";
331 let run2 = run2El ? String(run2El.value || "").trim() : "";
333 run1 = String(panel.getAttribute("data-active-run1") || panel.getAttribute("data-default-run1") || "").trim();
336 run2 = String(panel.getAttribute("data-active-run2") || panel.getAttribute("data-default-run2") || "").trim();
338 return { run1: run1, run2: run2 };
341 function setPanelRunTarget(panel, run1, run2) {
342 const run1El = qs(panel, ".dbx-module-images-run1");
343 const run2El = qs(panel, ".dbx-module-images-run2");
345 run1El.value = run1 || "";
348 run2El.value = run2 || "";
350 panel.setAttribute("data-active-run1", run1 || "");
351 panel.setAttribute("data-active-run2", run2 || "");
352 updateFilenamePreview(panel);
355 function updateFilenamePreview(panel) {
356 const preview = qs(panel, ".dbx-module-images-filename-preview");
360 const modul = preview.getAttribute("data-modul") || panel.getAttribute("data-modul") || "";
361 const target = getPanelRunTarget(panel);
362 if (!modul || !target.run1) {
363 preview.textContent = modul ? modul + "__run1.*" : "modul__run1.*";
366 preview.textContent = stemForRuns(modul, target.run1, target.run2) + ".*";
369 function applyRunTargetToPanel(panel, run1, run2) {
370 setPanelRunTarget(panel, run1, run2);
373 function selectGalleryItem(panel, item) {
374 if (!panel || !item) {
377 setActiveGalleryItem(panel, item);
378 setPanelRunTarget(panel, item.getAttribute("data-run1") || "", item.getAttribute("data-run2") || "");
379 const url = item.getAttribute("data-url") || "";
380 const label = qs(item, ".dbx-module-images-file") ? qs(item, ".dbx-module-images-file").textContent : "";
382 updatePanelPreview(panel, [{ url: url, label: label }]);
386 function bindModuleImagesPanel(panel) {
387 if (!panel || panel.dataset.modimgTargetBound === "1") {
390 panel.dataset.modimgTargetBound = "1";
391 panel.addEventListener("input", function (e) {
392 if (e.target.closest(".dbx-module-images-run1, .dbx-module-images-run2")) {
393 const run1 = String((qs(panel, ".dbx-module-images-run1") || {}).value || "").trim();
394 const run2 = String((qs(panel, ".dbx-module-images-run2") || {}).value || "").trim();
395 panel.setAttribute("data-active-run1", run1);
396 panel.setAttribute("data-active-run2", run2);
397 updateFilenamePreview(panel);
400 updateFilenamePreview(panel);
401 const list = qs(panel, "[data-module-images-list]");
404 qsa(list, ".dbx-module-images-item").forEach(function (item) {
406 file: item.getAttribute("data-file") || "",
407 url: item.getAttribute("data-url") || "",
408 label: item.getAttribute("data-file") || "",
409 default_params: item.getAttribute("data-params") || "",
410 run1: item.getAttribute("data-run1") || "",
411 run2: item.getAttribute("data-run2") || ""
415 updatePanelPreview(panel, items);
416 const first = qs(list, ".dbx-module-images-item");
418 selectGalleryItem(panel, first);
424 function bindModuleImagesPanels(scope) {
425 qsa(scope || document, ".dbx-module-images-panel").forEach(bindModuleImagesPanel);
428 function moduleImagesPanelFromTarget(target) {
429 if (!target || !target.closest) {
432 const panel = target.closest(".dbx-module-images-panel");
436 const modul = target.getAttribute("data-modul") || "";
440 return document.querySelector('.dbx-module-images-panel[data-modul="' + modul.replace(/"/g, '\\"') + '"]');
443 function applyModuleImageItems(panel, items) {
444 const list = qs(panel, "[data-module-images-list]");
445 const galleryHead = qs(panel, ".dbx-module-images-gallery-head .dbx-module-images-count");
446 if (list && Array.isArray(items)) {
447 renderModuleImagesList(list, items);
450 galleryHead.textContent = String((items && items.length) || 0);
452 if (items && items.length) {
453 updatePanelPreview(panel, items);
454 const first = list ? qs(list, ".dbx-module-images-item") : null;
456 selectGalleryItem(panel, first);
459 updatePanelPreview(panel, []);
460 setActiveGalleryItem(panel, null);
464 function addModuleImage(panel, row) {
465 if (row && Array.isArray(row.items)) {
466 applyModuleImageItems(panel, row.items);
467 return Promise.resolve({ ok: 1, items: row.items });
470 const modul = panel.getAttribute("data-modul") || "";
471 const addUrl = panel.getAttribute("data-add-url") || "";
472 const list = qs(panel, "[data-module-images-list]");
473 const target = getPanelRunTarget(panel);
474 if (!modul || !addUrl) {
475 return Promise.resolve(null);
478 window.alert("Bitte dbx_run1 angeben.");
479 return Promise.resolve(null);
481 const mediaId = parseInt(row && row.id ? row.id : 0, 10);
482 const filePath = row && (row.file_path || row.filePath) ? (row.file_path || row.filePath) : "";
483 const payload = mediaId > 0 ? { media_id: mediaId } : { file_path: filePath };
484 if (!payload.media_id && !payload.file_path) {
485 return Promise.resolve(null);
487 payload.xmodul = modul;
488 payload.run1 = target.run1;
489 payload.run2 = target.run2;
490 return fetchJson(addUrl, {
492 headers: { "Content-Type": "application/json" },
493 body: JSON.stringify(payload)
494 }).then(function (data) {
495 if (data && data.ok && data.items) {
496 applyModuleImageItems(panel, data.items);
497 } else if (data && data.msg) {
498 window.alert(data.msg);
501 }).catch(function (err) {
502 window.alert(err && err.message ? err.message : "Bild konnte nicht uebernommen werden.");
507 function addModuleSymbol(panel, row) {
508 const modul = panel.getAttribute("data-modul") || "";
509 const addUrl = panel.getAttribute("data-symbol-add-url") || "";
510 if (!modul || !addUrl) {
511 return Promise.resolve(null);
513 const mediaId = parseInt(row && row.id ? row.id : 0, 10);
514 const filePath = row && (row.file_path || row.filePath) ? (row.file_path || row.filePath) : "";
515 const payload = mediaId > 0 ? { media_id: mediaId } : { file_path: filePath };
516 if (!payload.media_id && !payload.file_path) {
517 return Promise.resolve(null);
519 payload.xmodul = modul;
521 return fetchJson(addUrl, {
523 headers: { "Content-Type": "application/json" },
524 body: JSON.stringify(payload)
525 }).then(function (data) {
526 if (data && data.ok && data.symbol) {
527 updateSymbolPreview(panel, data.symbol);
528 } else if (data && data.msg) {
529 window.alert(data.msg);
532 }).catch(function (err) {
533 window.alert(err && err.message ? err.message : "Symbolbild konnte nicht uebernommen werden.");
538 function openModuleSymbolBrowser(panel) {
542 const root = panel.closest(".dbx-modules-admin") || document.body;
543 const cfg = Object.assign({}, panelMediaCfg(panel));
544 cfg.media = panel.getAttribute("data-symbol-media-url") || cfg.media;
545 cfg.upload = panel.getAttribute("data-symbol-upload-url") || cfg.upload;
546 cfg.mediafolders = panel.getAttribute("data-symbol-mediafolders-url") || cfg.mediafolders;
547 cfg.mediafoldercreate = panel.getAttribute("data-symbol-mediafoldercreate-url") || cfg.mediafoldercreate;
548 cfg.mediafolderdelete = panel.getAttribute("data-symbol-mediafolderdelete-url") || cfg.mediafolderdelete;
549 cfg.deletemedia = panel.getAttribute("data-symbol-deletemedia-url") || cfg.deletemedia;
551 ensureCmsMediaBrowser().then(function (ok) {
552 if (!ok || !dbx.cmsMediaBrowser) {
555 dbx.cmsMediaBrowser.open(root, cfg, {
558 afterAssign: function (row) {
559 return addModuleSymbol(panel, row);
565 function openModuleImagesBrowser(panel) {
569 const target = getPanelRunTarget(panel);
571 window.alert("Bitte dbx_run1 fuer den Bildnamen angeben.");
574 const modul = panel.getAttribute("data-modul") || "";
575 const root = panel.closest(".dbx-modules-admin") || panel.closest(".dbx-modimg-manager") || document.body;
576 const cfg = Object.assign({}, panelMediaCfg(panel));
577 const moduleUploadUrl = panel.getAttribute("data-module-upload-url") || "";
578 if (moduleUploadUrl) {
579 cfg.upload = moduleUploadUrl;
582 ensureCmsMediaBrowser().then(function (ok) {
583 if (!ok || !dbx.cmsMediaBrowser) {
586 dbx.cmsMediaBrowser.open(root, cfg, {
594 afterAssign: function (row) {
595 return addModuleImage(panel, row);
601 function bindModuleImages(scope) {
602 scope = scope || document;
604 scope.addEventListener("click", function (e) {
605 const symbolBtn = e.target.closest(".dbx-module-symbol-pick");
608 const panel = moduleImagesPanelFromTarget(symbolBtn);
610 openModuleSymbolBrowser(panel);
615 const pickBtn = e.target.closest(".dbx-module-images-pick, .dbx-module-images-pick-btn");
618 const panel = moduleImagesPanelFromTarget(pickBtn);
620 openModuleImagesBrowser(panel);
625 const emptyPick = e.target.closest(".dbx-module-images-empty");
626 if (emptyPick && emptyPick.classList.contains("dbx-module-images-pick")) {
628 const panel = emptyPick.closest(".dbx-module-images-panel");
630 openModuleImagesBrowser(panel);
635 const removeBtn = e.target.closest(".dbx-module-images-remove");
639 const item = removeBtn.closest(".dbx-module-images-item");
640 const panel = removeBtn.closest(".dbx-module-images-panel");
641 if (!item || !panel) {
644 const modul = panel.getAttribute("data-modul") || "";
645 const removeUrl = panel.getAttribute("data-remove-url") || "";
646 const file = item.getAttribute("data-file") || "";
647 if (!modul || !removeUrl || !file) {
650 const panelRoot = panel.closest(".dbx-modules-admin") || panel.closest(".dbx-modimg-manager") || document.body;
651 removeBtn.disabled = true;
652 ensureConfirm().then(function (ok) {
654 window.alert("confirm.js konnte nicht geladen werden.");
657 return dbx.confirm.open({
658 id: "module-image-remove-" + modul + "-" + Date.now(),
661 title: "Modul-Aufrufbild loeschen",
662 question: "Modul-Aufrufbild <strong>" + escapeHtml(file) + "</strong> wirklich loeschen?",
663 hint: "Das Bild wird aus der Zuordnung entfernt und aus dem Verzeichnis <code>mod</code> geloescht.",
665 labelyes: "<i class=\"bi bi-trash\"></i> Loeschen",
666 labelno: "<i class=\"bi bi-x-lg\"></i> Abbrechen",
668 backdropclose: false,
671 }).then(function (result) {
672 if (!result || result.action !== "yes") {
675 return fetchJson(removeUrl, {
677 headers: { "Content-Type": "application/json" },
678 body: JSON.stringify({ xmodul: modul, file: file, delete_file: 1 })
680 }).then(function (data) {
681 if (data && data.ok && data.items) {
682 applyModuleImageItems(panel, data.items);
683 } else if (data && data.msg) {
684 window.alert(data.msg);
686 }).catch(function (err) {
687 if (err && err.message) {
688 window.alert(err.message);
690 }).finally(function () {
691 removeBtn.disabled = false;
696 const galleryItem = e.target.closest(".dbx-module-images-item");
697 if (galleryItem && !e.target.closest(".dbx-module-images-remove, .dbx-module-images-preview")) {
698 const panel = galleryItem.closest(".dbx-module-images-panel");
700 selectGalleryItem(panel, galleryItem);
705 scope.addEventListener("focusin", function (e) {
706 const panel = e.target.closest && e.target.closest(".dbx-module-images-panel");
707 if (panel && !panel.dataset.modimgTargetBound) {
708 bindModuleImagesPanel(panel);
713 function syncAccessGroupScroll(target) {
715 if (target && target.classList && target.classList.contains("dbx-module-access-groups-scroll")) {
717 } else if (target && target.querySelectorAll) {
718 qsa(target, ".dbx-module-access-groups-scroll").forEach(function (wrap) {
722 qsa(document, ".dbx-module-access-groups-scroll").forEach(function (wrap) {
727 wraps.forEach(function (wrap) {
728 const select = qs(wrap, ".dbx-module-access-groups");
732 const h = wrap.clientHeight;
736 const style = window.getComputedStyle(select);
737 const rowH = parseFloat(style.lineHeight) || (parseFloat(style.fontSize) * 1.45) || 20;
738 const padding = (parseFloat(style.paddingTop) || 0) + (parseFloat(style.paddingBottom) || 0);
739 const visible = Math.max(2, Math.floor((h - padding) / rowH));
740 select.size = visible;
744 function bindAccessGroupScroll(scope) {
745 scope = scope || document;
746 qsa(scope, ".dbx-module-access-groups-scroll").forEach(function (wrap) {
747 if (wrap.__dbxAccessScrollBound === "1") {
750 wrap.__dbxAccessScrollBound = "1";
751 const sync = function () {
752 syncAccessGroupScroll(wrap);
755 if (typeof ResizeObserver !== "undefined") {
756 const ro = new ResizeObserver(sync);
758 wrap.__dbxAccessScrollRo = ro;
763 function bindModuleAccess(scope) {
764 scope = scope || document;
765 qsa(scope, ".dbx-module-access-save").forEach(function (btn) {
766 if (btn.dataset.accessBound === "1") {
769 btn.dataset.accessBound = "1";
770 btn.addEventListener("click", function () {
771 const panel = btn.closest(".dbx-module-access-panel");
775 const modul = panel.getAttribute("data-modul") || "";
776 const saveUrl = btn.getAttribute("data-save-url") || "";
777 const select = qs(panel, ".dbx-module-access-groups");
778 const status = qs(panel, ".dbx-module-access-status");
779 if (!modul || !saveUrl || !select) {
782 const groups = Array.prototype.slice.call(select.selectedOptions).map(function (opt) {
787 status.textContent = "Speichern…";
788 status.className = "dbx-module-access-status small text-muted";
792 headers: { "Content-Type": "application/json" },
793 body: JSON.stringify({ xmodul: modul, groups: groups })
794 }).then(function (data) {
796 if (data && data.ok) {
797 status.textContent = data.msg || "Gespeichert";
798 status.className = "dbx-module-access-status small is-ok";
800 status.textContent = (data && data.msg) ? data.msg : "Speichern fehlgeschlagen";
801 status.className = "dbx-module-access-status small is-error";
803 } else if (data && data.msg && !data.ok) {
804 window.alert(data.msg);
806 }).catch(function (err) {
808 status.textContent = err && err.message ? err.message : "Speichern fehlgeschlagen";
809 status.className = "dbx-module-access-status small is-error";
811 }).finally(function () {
812 btn.disabled = false;
818 function syncModulePanelToggleIcon(panel) {
819 const icon = qs(panel, ".dbx-module-toggle-icon");
820 const btn = qs(panel, ".dbx-module-panel-toggle-btn");
821 const isOpen = panel.classList.contains("is-open");
823 icon.classList.toggle("bi-chevron-down", isOpen);
824 icon.classList.toggle("bi-chevron-right", !isOpen);
827 btn.setAttribute("aria-expanded", isOpen ? "true" : "false");
831 function setModulePanelOpen(panel, open) {
832 panel.classList.toggle("is-open", !!open);
833 syncModulePanelToggleIcon(panel);
835 window.requestAnimationFrame(function () {
836 syncAccessGroupScroll(panel);
841 function bindModulePanels(root) {
842 root = root || document;
843 const saved = dbx.uiGet ? dbx.uiGet(LIB, PANEL_UI_ID, "openPanels", {}) : {};
844 const openPanels = (saved && typeof saved === "object" && !Array.isArray(saved)) ? saved : {};
846 qsa(root, ".dbx-module-panel-wrap").forEach(function (panel, index) {
847 const key = panel.getAttribute("data-module-ui-state") || ("module-" + index);
848 panel.setAttribute("data-module-ui-state", key);
850 if (Object.prototype.hasOwnProperty.call(openPanels, key)) {
851 setModulePanelOpen(panel, !!openPanels[key]);
853 syncModulePanelToggleIcon(panel);
856 if (panel.__dbxModuleToggleReady) {
859 panel.__dbxModuleToggleReady = true;
861 function togglePanel() {
862 const nextOpen = !panel.classList.contains("is-open");
863 setModulePanelOpen(panel, nextOpen);
864 const state = dbx.uiGet ? dbx.uiGet(LIB, PANEL_UI_ID, "openPanels", {}) : {};
865 const next = (state && typeof state === "object" && !Array.isArray(state)) ? Object.assign({}, state) : {};
866 next[key] = nextOpen;
868 dbx.uiSet(LIB, PANEL_UI_ID, "openPanels", next);
872 const toggleBtn = qs(panel, ".dbx-module-panel-toggle-btn");
874 toggleBtn.addEventListener("click", function (e) {
881 const summaryMain = qs(panel, ".dbx-module-summary-main");
883 summaryMain.addEventListener("click", function (e) {
884 if (e.target.closest("input, label, .dbx-module-row-select")) {
894 if (!document.__dbxModulePanelsBound) {
895 document.__dbxModulePanelsBound = true;
896 bindModulePanels(document);
899 function ensureConfirm() {
900 if (dbx.confirm && typeof dbx.confirm.open === "function") {
901 return Promise.resolve(true);
903 return new Promise(function (resolve) {
904 if (!dbx.loadFeature) {
908 dbx.loadFeature("confirm", function () {
909 resolve(!!(dbx.confirm && typeof dbx.confirm.open === "function"));
914 function updateActiveToggleButton(btn, active) {
915 const isActive = String(active) === "1";
916 btn.setAttribute("data-active", isActive ? "1" : "0");
917 btn.classList.toggle("btn-success", isActive);
918 btn.classList.toggle("btn-outline-secondary", !isActive);
919 btn.innerHTML = '<i class="bi ' + (isActive ? "bi-toggle-on" : "bi-toggle-off") + '"></i> '
920 + (isActive ? "Aktiv" : "Inaktiv");
923 function bindModuleBarActions(root) {
924 root = root || document;
926 qsa(root, ".dbx-module-active-toggle").forEach(function (btn) {
927 if (btn.dataset.modActiveBound === "1") {
930 btn.dataset.modActiveBound = "1";
931 btn.addEventListener("click", function (e) {
934 const url = btn.getAttribute("data-toggle-url") || "";
935 const modul = btn.getAttribute("data-modul") || "";
936 if (!url || !modul) {
942 headers: { "Content-Type": "application/json" },
943 body: JSON.stringify({ xmodul: modul })
944 }).then(function (data) {
945 if (data && data.ok) {
946 updateActiveToggleButton(btn, data.active);
947 const panel = btn.closest(".dbx-module-panel-wrap");
948 const row = btn.closest(".dbx-module-row");
950 qsa(panel, ".dbx-module-row-badges .badge").forEach(function (badge) {
951 const text = String(badge.textContent || "").trim();
952 if (text !== "Aktiv" && text !== "Inaktiv") {
955 const isActive = String(data.active) === "1";
956 badge.textContent = data.active_label || (isActive ? "Aktiv" : "Inaktiv");
957 badge.classList.toggle("text-bg-success", isActive);
958 badge.classList.toggle("text-bg-secondary", !isActive);
962 row.classList.toggle("dbx-module-row-inactive", String(data.active) !== "1");
964 } else if (data && data.msg) {
965 window.alert(data.msg);
967 }).catch(function (err) {
968 window.alert(err && err.message ? err.message : "Status konnte nicht gespeichert werden.");
969 }).finally(function () {
970 btn.disabled = false;
975 qsa(root, ".dbx-module-delete-btn").forEach(function (btn) {
976 if (btn.dataset.modDeleteBound === "1") {
979 btn.dataset.modDeleteBound = "1";
980 btn.addEventListener("click", function (e) {
983 const url = btn.getAttribute("data-delete-url") || "";
984 const modul = btn.getAttribute("data-modul") || "";
985 const title = btn.getAttribute("data-title") || modul;
986 const panelRoot = btn.closest(".dbx-modules-admin") || document.body;
987 if (!url || !modul) {
991 ensureConfirm().then(function (ok) {
993 if (!window.confirm('Modul "' + title + '" wirklich loeschen?')) {
996 return { action: "yes" };
998 return dbx.confirm.open({
999 id: "module-delete-" + modul + "-" + Date.now(),
1002 title: "Modul loeschen",
1003 question: "Modul <strong>" + escapeHtml(title) + "</strong> komplett loeschen?",
1004 hint: "Das Modulverzeichnis inklusive aller Dateien wird unwiderruflich entfernt.",
1006 labelyes: "<i class=\"bi bi-trash\"></i> Loeschen",
1007 labelno: "<i class=\"bi bi-x-lg\"></i> Abbrechen",
1009 backdropclose: false,
1012 }).then(function (result) {
1013 if (!result || result.action !== "yes") {
1016 return fetchJson(url, {
1018 headers: { "Content-Type": "application/json" },
1019 body: JSON.stringify({ xmodul: modul })
1021 }).then(function (data) {
1026 const row = btn.closest(".dbx-module-row");
1030 } else if (data.msg) {
1031 window.alert(data.msg);
1033 }).catch(function (err) {
1034 if (err && err.message) {
1035 window.alert(err.message);
1042 function bindModimgManager(root) {
1043 const manager = qs(root, ".dbx-modimg-manager");
1044 if (!manager || manager.dataset.modimgBound === "1") {
1047 manager.dataset.modimgBound = "1";
1048 manager.classList.add("dbx-module-images-panel");
1049 if (!manager.getAttribute("data-module-images-list")) {
1050 const gallery = qs(manager, "[data-module-images-list]");
1052 manager.setAttribute("data-module-images-list", "1");
1057 function initModulesAdmin(root) {
1058 bindModuleDdGallery(root || document);
1059 bindModimgManager(root || document);
1060 bindModuleImagesPanels(root || document);
1061 bindModuleAccess(root || document);
1062 bindAccessGroupScroll(root || document);
1063 bindModulePanels(root || document);
1064 bindModuleBarActions(root || document);
1065 syncAccessGroupScroll(root || document);
1068 if (!document.__dbxModuleImagesBound) {
1069 document.__dbxModuleImagesBound = true;
1070 bindModuleImages(document);
1073 dbx.modulesAdmin = {
1074 init: initModulesAdmin,
1075 openImagesBrowser: openModuleImagesBrowser,
1076 rescan: function (ctx) {
1077 const root = ctx && ctx.root ? ctx.root : (ctx || document);
1078 initModulesAdmin(root);
1082 dbx.feature.register("modulesAdmin", {
1085 ["js", "lib", "openWin.js"],
1086 ["js", "lib", "confirm.js"]
1089 ["css", "root", "modules/dbxAdmin/tpl/css/modules-admin.css"],
1090 ["css", "design", "c-cms.css"]
1092 init: function (el) {
1093 dbx.modulesAdmin.init(el);
1095 rescan: function (ctx) {
1096 dbx.modulesAdmin.rescan(ctx);
1099})(window, document);