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 moduleMessageRoot(node) {
25 if (node && node.nodeType === 1 && node.hasAttribute("data-module-messages")) {
28 return node && node.closest
29 ? (node.closest(".dbx-modules-admin") || node.closest(".dbx-modimg-manager"))
30 : document.querySelector(".dbx-modules-admin, .dbx-modimg-manager");
33 function moduleMessages(node) {
34 const root = moduleMessageRoot(node);
38 if (root.__dbxModuleMessages) {
39 return root.__dbxModuleMessages;
42 root.__dbxModuleMessages = JSON.parse(root.getAttribute("data-module-messages") || "{}");
44 root.__dbxModuleMessages = {};
46 return root.__dbxModuleMessages;
49 function moduleMsg(node, key, fallback, replacements) {
50 let value = String(moduleMessages(node)[key] || fallback || "");
51 Object.keys(replacements || {}).forEach(function (name) {
52 value = value.split("{" + name + "}").join(String(replacements[name]));
57 function modimgMediaCfg(root) {
58 root = root || document.querySelector(".dbx-modules-admin") || document;
60 media: root.getAttribute("data-modimg-media") || "",
61 upload: root.getAttribute("data-modimg-upload") || "",
62 mediafolders: root.getAttribute("data-modimg-mediafolders") || "",
63 mediafoldercreate: root.getAttribute("data-modimg-mediafoldercreate") || "",
64 mediafolderdelete: root.getAttribute("data-modimg-mediafolderdelete") || "",
65 deletemedia: root.getAttribute("data-modimg-deletemedia") || "",
66 uploadmax: root.getAttribute("data-modimg-uploadmax") || ""
70 function panelMediaCfg(panel) {
71 const cfg = modimgMediaCfg(panel.closest(".dbx-modules-admin") || panel.closest(".dbx-modimg-manager") || document);
72 if (panel.getAttribute("data-modimg-media")) {
73 cfg.media = panel.getAttribute("data-modimg-media");
75 if (panel.getAttribute("data-modimg-upload")) {
76 cfg.upload = panel.getAttribute("data-modimg-upload");
78 if (panel.getAttribute("data-modimg-mediafolders")) {
79 cfg.mediafolders = panel.getAttribute("data-modimg-mediafolders");
81 if (panel.getAttribute("data-modimg-mediafoldercreate")) {
82 cfg.mediafoldercreate = panel.getAttribute("data-modimg-mediafoldercreate");
84 if (panel.getAttribute("data-modimg-mediafolderdelete")) {
85 cfg.mediafolderdelete = panel.getAttribute("data-modimg-mediafolderdelete");
87 if (panel.getAttribute("data-modimg-deletemedia")) {
88 cfg.deletemedia = panel.getAttribute("data-modimg-deletemedia");
90 if (panel.getAttribute("data-modimg-uploadmax")) {
91 cfg.uploadmax = panel.getAttribute("data-modimg-uploadmax");
96 function ensureCmsMediaBrowser() {
97 if (dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function") {
98 return Promise.resolve(true);
100 return new Promise(function (resolve) {
106 ["js", "lib", "ajax.js"],
107 ["js", "lib", "openWin.js"],
108 ["js", "lib", "cms.js"]
110 resolve(!!(dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function"));
115 function getActiveDdItem(galleryId) {
116 const gallery = document.getElementById(galleryId);
120 return gallery.querySelector(".dbx-module-dd-item.is-active") || gallery.querySelector(".dbx-module-dd-item");
123 function openDdItem(item) {
124 if (!item || !dbx.openWin) {
127 const url = item.getAttribute("data-edit-url") || "";
128 const title = item.getAttribute("data-edit-title") || "DD";
137 position: "center-top",
143 function bindModuleDdGallery(root) {
144 const scope = root || document;
146 scope.querySelectorAll(".dbx-module-dd-gallery").forEach(function (gallery) {
147 if (gallery.dataset.ddBound === "1") {
150 gallery.dataset.ddBound = "1";
152 gallery.querySelectorAll(".dbx-module-dd-item").forEach(function (item) {
153 item.addEventListener("click", function () {
154 gallery.querySelectorAll(".dbx-module-dd-item").forEach(function (node) {
155 node.classList.remove("is-active");
156 node.setAttribute("aria-selected", "false");
158 item.classList.add("is-active");
159 item.setAttribute("aria-selected", "true");
160 const panel = gallery.closest(".dbx-module-dd-gallery-panel");
161 const btn = panel ? panel.querySelector(".dbx-module-dd-open-btn") : null;
163 btn.setAttribute("data-dd-active", item.getAttribute("data-edit-url") || "");
166 item.addEventListener("dblclick", function () {
172 scope.querySelectorAll(".dbx-module-dd-open-btn").forEach(function (btn) {
173 if (btn.dataset.ddBound === "1") {
176 btn.dataset.ddBound = "1";
177 const galleryId = btn.getAttribute("data-dd-select") || "";
178 btn.addEventListener("click", function () {
179 const item = getActiveDdItem(galleryId);
185 function fetchJson(url, options) {
186 options = options || {};
187 if (!dbx.ajax || typeof dbx.ajax.request !== "function") {
188 return Promise.reject(new Error("ajax.js nicht geladen."));
191 return dbx.ajax.request({
193 method: options.method || "GET",
195 body: (typeof options.body !== "undefined") ? options.body : null,
196 headers: options.headers || {},
197 timeout: Number(options.timeout || 20000)
201 function updatePanelPreview(panel, items) {
202 const previewCol = qs(panel, "[data-module-images-preview]");
206 const modul = panel.getAttribute("data-modul") || "";
207 const placeholder = panel.getAttribute("data-placeholder-url") || "";
208 const placeholderAlt = panel.getAttribute("data-placeholder-alt") || modul;
211 let isPlaceholder = true;
212 if (items && items.length) {
213 url = items[0].url || "";
214 label = items[0].label || items[0].file || modul;
215 isPlaceholder = false;
221 previewCol.innerHTML = '<img src="' + escapeAttr(url) + '" alt="' + escapeAttr(isPlaceholder ? placeholderAlt : label) + '" class="dbx-module-images-preview-img' + (isPlaceholder ? " is-placeholder" : "") + '" loading="lazy">';
224 previewCol.innerHTML = '<span class="dbx-module-images-placeholder-icon" aria-hidden="true"><i class="bi bi-box-seam"></i></span>';
227 function updateSymbolPreview(panel, symbol) {
228 const previewCol = qs(panel, "[data-module-symbol-preview]");
232 const modul = panel.getAttribute("data-modul") || "";
233 const placeholder = panel.getAttribute("data-placeholder-url") || "";
234 const placeholderAlt = panel.getAttribute("data-placeholder-alt") || modul;
235 symbol = symbol || {};
236 const url = symbol.url || placeholder;
237 const alt = symbol.alt || (symbol.url ? modul : placeholderAlt);
239 previewCol.innerHTML = '<img src="' + escapeAttr(url) + '" alt="' + escapeAttr(alt) + '" class="dbx-module-symbol-img' + (symbol.url ? "" : " is-placeholder") + '" loading="lazy">';
242 previewCol.innerHTML = '<span class="dbx-module-images-placeholder-icon" aria-hidden="true"><i class="bi bi-box-seam"></i></span>';
245 function renderModuleImagesEmptyListHtml(node) {
246 return '<div class="dbx-module-images-empty-list text-muted small">'
247 + escapeHtml(moduleMsg(node, "no_module_images", "No module images"))
251 const MOD_FILENAME_SEP = "__";
253 function stemForRuns(modul, run1, run2) {
254 let name = String(modul || "").trim();
255 run1 = String(run1 || "").trim();
256 run2 = String(run2 || "").trim();
257 if (!name || !run1) {
260 name += MOD_FILENAME_SEP + run1;
262 name += MOD_FILENAME_SEP + run2;
267 function buildModuleImageCall(modul, params) {
268 const call = "dbx_modul=" + encodeURIComponent(modul || "");
270 return call + "&" + params;
275 function buildModulPreviewUrl(modul, run1, run2, params, panel) {
276 run1 = String(run1 || "").trim();
277 run2 = String(run2 || "").trim();
278 params = String(params || "");
279 if ((!run1 || !run2) && params) {
280 const m1 = params.match(/(?:^|&)dbx_run1=([^&]+)/);
281 const m2 = params.match(/(?:^|&)dbx_run2=([^&]+)/);
283 run1 = decodeURIComponent(m1[1].replace(/\+/g, " "));
286 run2 = decodeURIComponent(m2[1].replace(/\+/g, " "));
289 if (!run1 && panel) {
290 run1 = String(panel.getAttribute("data-default-run1") || "").trim();
292 if (!run2 && panel) {
293 run2 = String(panel.getAttribute("data-default-run2") || "").trim();
295 let url = "?dbx_modul=" + encodeURIComponent(modul || "");
297 url += "&dbx_run1=" + encodeURIComponent(run1);
300 url += "&dbx_run2=" + encodeURIComponent(run2);
302 return { url: url, run1: run1, run2: run2 };
305 function setActiveGalleryItem(panel, item) {
306 const list = qs(panel, "[data-module-images-list]");
310 qsa(list, ".dbx-module-images-item").forEach(function (el) {
311 el.classList.toggle("is-active", item && el === item);
315 function escapeHtml(text) {
316 return String(text || "")
317 .replace(/&/g, "&")
318 .replace(/</g, "<")
319 .replace(/>/g, ">")
320 .replace(/"/g, """);
323 function escapeAttr(text) {
324 return escapeHtml(text);
327 function renderModuleImagesList(container, items) {
331 if (!items || !items.length) {
332 container.innerHTML = renderModuleImagesEmptyListHtml(container);
335 container.innerHTML = items.map(function (item, index) {
336 const file = item.file || "";
337 const url = item.url || "";
338 const label = item.label || file;
339 const params = item.default_params || "";
340 const panel = container.closest(".dbx-module-images-panel");
341 const modul = panel ? (panel.getAttribute("data-modul") || "") : "";
342 const preview = buildModulPreviewUrl(modul, item.run1 || "", item.run2 || "", params, panel);
343 const run1 = preview.run1;
344 const run2 = preview.run2;
345 const call = buildModuleImageCall(modul, params);
346 const previewTitle = moduleMsg(panel, "module_preview", "Preview: {module}", { module: modul || "" });
347 const active = index === 0 ? " is-active" : "";
348 const previewBtn = (run1 || params)
349 ? '<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="' + escapeAttr(moduleMsg(panel, "open_module", "Open module")) + '"><i class="bi bi-box-arrow-up-right"></i></a>'
351 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) + '">'
352 + '<span class="dbx-module-images-thumb"><img src="' + escapeAttr(url) + '" alt="' + escapeAttr(label) + '" loading="lazy"></span>'
353 + '<span class="dbx-module-images-meta"><code class="dbx-module-images-call">' + escapeHtml(call) + '</code>'
354 + '<span class="dbx-module-images-file">' + escapeHtml(file) + '</span></span>'
355 + '<span class="dbx-module-images-actions">'
357 + '<button type="button" class="btn btn-outline-danger btn-sm dbx-module-images-remove" title="' + escapeAttr(moduleMsg(panel, "remove_image", "Remove image")) + '"><i class="bi bi-trash"></i></button>'
362 function getPanelRunTarget(panel) {
363 const run1El = qs(panel, ".dbx-module-images-run1");
364 const run2El = qs(panel, ".dbx-module-images-run2");
365 let run1 = run1El ? String(run1El.value || "").trim() : "";
366 let run2 = run2El ? String(run2El.value || "").trim() : "";
368 run1 = String(panel.getAttribute("data-active-run1") || panel.getAttribute("data-default-run1") || "").trim();
371 run2 = String(panel.getAttribute("data-active-run2") || panel.getAttribute("data-default-run2") || "").trim();
373 return { run1: run1, run2: run2 };
376 function setPanelRunTarget(panel, run1, run2) {
377 const run1El = qs(panel, ".dbx-module-images-run1");
378 const run2El = qs(panel, ".dbx-module-images-run2");
380 run1El.value = run1 || "";
383 run2El.value = run2 || "";
385 panel.setAttribute("data-active-run1", run1 || "");
386 panel.setAttribute("data-active-run2", run2 || "");
387 updateFilenamePreview(panel);
390 function updateFilenamePreview(panel) {
391 const preview = qs(panel, ".dbx-module-images-filename-preview");
395 const modul = preview.getAttribute("data-modul") || panel.getAttribute("data-modul") || "";
396 const target = getPanelRunTarget(panel);
397 if (!modul || !target.run1) {
398 preview.textContent = modul ? modul + "__run1.*" : "modul__run1.*";
401 preview.textContent = stemForRuns(modul, target.run1, target.run2) + ".*";
404 function applyRunTargetToPanel(panel, run1, run2) {
405 setPanelRunTarget(panel, run1, run2);
408 function selectGalleryItem(panel, item) {
409 if (!panel || !item) {
412 setActiveGalleryItem(panel, item);
413 setPanelRunTarget(panel, item.getAttribute("data-run1") || "", item.getAttribute("data-run2") || "");
414 const url = item.getAttribute("data-url") || "";
415 const label = qs(item, ".dbx-module-images-file") ? qs(item, ".dbx-module-images-file").textContent : "";
417 updatePanelPreview(panel, [{ url: url, label: label }]);
421 function bindModuleImagesPanel(panel) {
422 if (!panel || panel.dataset.modimgTargetBound === "1") {
425 panel.dataset.modimgTargetBound = "1";
426 panel.addEventListener("input", function (e) {
427 if (e.target.closest(".dbx-module-images-run1, .dbx-module-images-run2")) {
428 const run1 = String((qs(panel, ".dbx-module-images-run1") || {}).value || "").trim();
429 const run2 = String((qs(panel, ".dbx-module-images-run2") || {}).value || "").trim();
430 panel.setAttribute("data-active-run1", run1);
431 panel.setAttribute("data-active-run2", run2);
432 updateFilenamePreview(panel);
435 updateFilenamePreview(panel);
436 const list = qs(panel, "[data-module-images-list]");
439 qsa(list, ".dbx-module-images-item").forEach(function (item) {
441 file: item.getAttribute("data-file") || "",
442 url: item.getAttribute("data-url") || "",
443 label: item.getAttribute("data-file") || "",
444 default_params: item.getAttribute("data-params") || "",
445 run1: item.getAttribute("data-run1") || "",
446 run2: item.getAttribute("data-run2") || ""
450 updatePanelPreview(panel, items);
451 const first = qs(list, ".dbx-module-images-item");
453 selectGalleryItem(panel, first);
459 function bindModuleImagesPanels(scope) {
460 qsa(scope || document, ".dbx-module-images-panel").forEach(bindModuleImagesPanel);
463 function moduleImagesPanelFromTarget(target) {
464 if (!target || !target.closest) {
467 const panel = target.closest(".dbx-module-images-panel");
471 const modul = target.getAttribute("data-modul") || "";
475 return document.querySelector('.dbx-module-images-panel[data-modul="' + modul.replace(/"/g, '\\"') + '"]');
478 function applyModuleImageItems(panel, items) {
479 const list = qs(panel, "[data-module-images-list]");
480 const galleryHead = qs(panel, ".dbx-module-images-gallery-head .dbx-module-images-count");
481 if (list && Array.isArray(items)) {
482 renderModuleImagesList(list, items);
485 galleryHead.textContent = String((items && items.length) || 0);
487 if (items && items.length) {
488 updatePanelPreview(panel, items);
489 const first = list ? qs(list, ".dbx-module-images-item") : null;
491 selectGalleryItem(panel, first);
494 updatePanelPreview(panel, []);
495 setActiveGalleryItem(panel, null);
499 function addModuleImage(panel, row) {
500 if (row && Array.isArray(row.items)) {
501 applyModuleImageItems(panel, row.items);
502 return Promise.resolve({ ok: 1, items: row.items });
505 const modul = panel.getAttribute("data-modul") || "";
506 const addUrl = panel.getAttribute("data-add-url") || "";
507 const list = qs(panel, "[data-module-images-list]");
508 const target = getPanelRunTarget(panel);
509 if (!modul || !addUrl) {
510 return Promise.resolve(null);
513 window.alert(moduleMsg(panel, "run1_required", "Please enter dbx_run1."));
514 return Promise.resolve(null);
516 const mediaId = parseInt(row && row.id ? row.id : 0, 10);
517 const filePath = row && (row.file_path || row.filePath) ? (row.file_path || row.filePath) : "";
518 const payload = mediaId > 0 ? { media_id: mediaId } : { file_path: filePath };
519 if (!payload.media_id && !payload.file_path) {
520 return Promise.resolve(null);
522 payload.xmodul = modul;
523 payload.run1 = target.run1;
524 payload.run2 = target.run2;
525 return fetchJson(addUrl, {
527 headers: { "Content-Type": "application/json" },
528 body: JSON.stringify(payload)
529 }).then(function (data) {
530 if (data && data.ok && data.items) {
531 applyModuleImageItems(panel, data.items);
532 } else if (data && data.msg) {
533 window.alert(data.msg);
536 }).catch(function (err) {
537 window.alert(err && err.message ? err.message : moduleMsg(panel, "image_import_error", "The image could not be imported."));
542 function addModuleSymbol(panel, row) {
543 const modul = panel.getAttribute("data-modul") || "";
544 const addUrl = panel.getAttribute("data-symbol-add-url") || "";
545 if (!modul || !addUrl) {
546 return Promise.resolve(null);
548 const mediaId = parseInt(row && row.id ? row.id : 0, 10);
549 const filePath = row && (row.file_path || row.filePath) ? (row.file_path || row.filePath) : "";
550 const payload = mediaId > 0 ? { media_id: mediaId } : { file_path: filePath };
551 if (!payload.media_id && !payload.file_path) {
552 return Promise.resolve(null);
554 payload.xmodul = modul;
556 return fetchJson(addUrl, {
558 headers: { "Content-Type": "application/json" },
559 body: JSON.stringify(payload)
560 }).then(function (data) {
561 if (data && data.ok && data.symbol) {
562 updateSymbolPreview(panel, data.symbol);
563 } else if (data && data.msg) {
564 window.alert(data.msg);
567 }).catch(function (err) {
568 window.alert(err && err.message ? err.message : moduleMsg(panel, "symbol_import_error", "The symbol image could not be imported."));
573 function openModuleSymbolBrowser(panel) {
577 const root = panel.closest(".dbx-modules-admin") || document.body;
578 const cfg = Object.assign({}, panelMediaCfg(panel));
579 cfg.media = panel.getAttribute("data-symbol-media-url") || cfg.media;
580 cfg.upload = panel.getAttribute("data-symbol-upload-url") || cfg.upload;
581 cfg.mediafolders = panel.getAttribute("data-symbol-mediafolders-url") || cfg.mediafolders;
582 cfg.mediafoldercreate = panel.getAttribute("data-symbol-mediafoldercreate-url") || cfg.mediafoldercreate;
583 cfg.mediafolderdelete = panel.getAttribute("data-symbol-mediafolderdelete-url") || cfg.mediafolderdelete;
584 cfg.deletemedia = panel.getAttribute("data-symbol-deletemedia-url") || cfg.deletemedia;
586 ensureCmsMediaBrowser().then(function (ok) {
587 if (!ok || !dbx.cmsMediaBrowser) {
590 dbx.cmsMediaBrowser.open(root, cfg, {
593 afterAssign: function (row) {
594 return addModuleSymbol(panel, row);
600 function openModuleImagesBrowser(panel) {
604 const target = getPanelRunTarget(panel);
606 window.alert(moduleMsg(panel, "run1_required", "Please enter dbx_run1 for the image file name."));
609 const modul = panel.getAttribute("data-modul") || "";
610 const root = panel.closest(".dbx-modules-admin") || panel.closest(".dbx-modimg-manager") || document.body;
611 const cfg = Object.assign({}, panelMediaCfg(panel));
612 const moduleUploadUrl = panel.getAttribute("data-module-upload-url") || "";
613 if (moduleUploadUrl) {
614 cfg.upload = moduleUploadUrl;
617 ensureCmsMediaBrowser().then(function (ok) {
618 if (!ok || !dbx.cmsMediaBrowser) {
621 dbx.cmsMediaBrowser.open(root, cfg, {
629 afterAssign: function (row) {
630 return addModuleImage(panel, row);
636 function bindModuleImages(scope) {
637 scope = scope || document;
639 scope.addEventListener("click", function (e) {
640 const symbolBtn = e.target.closest(".dbx-module-symbol-pick");
643 const panel = moduleImagesPanelFromTarget(symbolBtn);
645 openModuleSymbolBrowser(panel);
650 const pickBtn = e.target.closest(".dbx-module-images-pick, .dbx-module-images-pick-btn");
653 const panel = moduleImagesPanelFromTarget(pickBtn);
655 openModuleImagesBrowser(panel);
660 const emptyPick = e.target.closest(".dbx-module-images-empty");
661 if (emptyPick && emptyPick.classList.contains("dbx-module-images-pick")) {
663 const panel = emptyPick.closest(".dbx-module-images-panel");
665 openModuleImagesBrowser(panel);
670 const removeBtn = e.target.closest(".dbx-module-images-remove");
674 const item = removeBtn.closest(".dbx-module-images-item");
675 const panel = removeBtn.closest(".dbx-module-images-panel");
676 if (!item || !panel) {
679 const modul = panel.getAttribute("data-modul") || "";
680 const removeUrl = panel.getAttribute("data-remove-url") || "";
681 const file = item.getAttribute("data-file") || "";
682 if (!modul || !removeUrl || !file) {
685 const panelRoot = panel.closest(".dbx-modules-admin") || panel.closest(".dbx-modimg-manager") || document.body;
686 removeBtn.disabled = true;
687 ensureConfirm().then(function (ok) {
689 window.alert(moduleMsg(panel, "confirm_unavailable", "The confirmation dialog could not be loaded."));
692 return dbx.confirm.open({
693 id: "module-image-remove-" + modul + "-" + Date.now(),
696 title: moduleMsg(panel, "image_delete_title", "Delete module call image"),
697 question: moduleMsg(panel, "image_delete_question", "Really delete the module call image {file}?", { file: "<strong>" + escapeHtml(file) + "</strong>" }),
698 hint: moduleMsg(panel, "image_delete_hint", "The image will be removed from the module image directory."),
700 labelyes: "<i class=\"bi bi-trash\"></i> " + escapeHtml(moduleMsg(panel, "delete_button", "Delete")),
701 labelno: "<i class=\"bi bi-x-lg\"></i> " + escapeHtml(moduleMsg(panel, "cancel_button", "Cancel")),
703 backdropclose: false,
706 }).then(function (result) {
707 if (!result || result.action !== "yes") {
710 return fetchJson(removeUrl, {
712 headers: { "Content-Type": "application/json" },
713 body: JSON.stringify({ xmodul: modul, file: file, delete_file: 1 })
715 }).then(function (data) {
716 if (data && data.ok && data.items) {
717 applyModuleImageItems(panel, data.items);
718 } else if (data && data.msg) {
719 window.alert(data.msg);
721 }).catch(function (err) {
722 if (err && err.message) {
723 window.alert(err.message);
725 }).finally(function () {
726 removeBtn.disabled = false;
731 const galleryItem = e.target.closest(".dbx-module-images-item");
732 if (galleryItem && !e.target.closest(".dbx-module-images-remove, .dbx-module-images-preview")) {
733 const panel = galleryItem.closest(".dbx-module-images-panel");
735 selectGalleryItem(panel, galleryItem);
740 scope.addEventListener("focusin", function (e) {
741 const panel = e.target.closest && e.target.closest(".dbx-module-images-panel");
742 if (panel && !panel.dataset.modimgTargetBound) {
743 bindModuleImagesPanel(panel);
748 function syncAccessGroupScroll(target) {
750 if (target && target.classList && target.classList.contains("dbx-module-access-groups-scroll")) {
752 } else if (target && target.querySelectorAll) {
753 qsa(target, ".dbx-module-access-groups-scroll").forEach(function (wrap) {
757 qsa(document, ".dbx-module-access-groups-scroll").forEach(function (wrap) {
762 wraps.forEach(function (wrap) {
763 const select = qs(wrap, ".dbx-module-access-groups");
767 const h = wrap.clientHeight;
771 const style = window.getComputedStyle(select);
772 const rowH = parseFloat(style.lineHeight) || (parseFloat(style.fontSize) * 1.45) || 20;
773 const padding = (parseFloat(style.paddingTop) || 0) + (parseFloat(style.paddingBottom) || 0);
774 const visible = Math.max(2, Math.floor((h - padding) / rowH));
775 select.size = visible;
779 function bindAccessGroupScroll(scope) {
780 scope = scope || document;
781 qsa(scope, ".dbx-module-access-groups-scroll").forEach(function (wrap) {
782 if (wrap.__dbxAccessScrollBound === "1") {
785 wrap.__dbxAccessScrollBound = "1";
786 const sync = function () {
787 syncAccessGroupScroll(wrap);
790 if (typeof ResizeObserver !== "undefined") {
791 const ro = new ResizeObserver(sync);
793 wrap.__dbxAccessScrollRo = ro;
798 function bindModuleAccess(scope) {
799 scope = scope || document;
800 qsa(scope, ".dbx-module-access-save").forEach(function (btn) {
801 if (btn.dataset.accessBound === "1") {
804 btn.dataset.accessBound = "1";
805 btn.addEventListener("click", function () {
806 const panel = btn.closest(".dbx-module-access-panel");
810 const modul = panel.getAttribute("data-modul") || "";
811 const saveUrl = btn.getAttribute("data-save-url") || "";
812 const select = qs(panel, ".dbx-module-access-groups");
813 const status = qs(panel, ".dbx-module-access-status");
814 if (!modul || !saveUrl || !select) {
817 const groups = Array.prototype.slice.call(select.selectedOptions).map(function (opt) {
822 status.textContent = moduleMsg(panel, "saving", "Saving …");
823 status.className = "dbx-module-access-status small text-muted";
827 headers: { "Content-Type": "application/json" },
828 body: JSON.stringify({ xmodul: modul, groups: groups })
829 }).then(function (data) {
831 if (data && data.ok) {
832 status.textContent = data.msg || moduleMsg(panel, "saved", "Saved");
833 status.className = "dbx-module-access-status small is-ok";
835 status.textContent = (data && data.msg) ? data.msg : moduleMsg(panel, "save_failed", "Saving failed");
836 status.className = "dbx-module-access-status small is-error";
838 } else if (data && data.msg && !data.ok) {
839 window.alert(data.msg);
841 }).catch(function (err) {
843 status.textContent = err && err.message ? err.message : moduleMsg(panel, "save_failed", "Saving failed");
844 status.className = "dbx-module-access-status small is-error";
846 }).finally(function () {
847 btn.disabled = false;
853 function syncModulePanelToggleIcon(panel) {
854 const icon = qs(panel, ".dbx-module-toggle-icon");
855 const btn = qs(panel, ".dbx-module-panel-toggle-btn");
856 const isOpen = panel.classList.contains("is-open");
858 icon.classList.toggle("bi-chevron-down", isOpen);
859 icon.classList.toggle("bi-chevron-right", !isOpen);
862 btn.setAttribute("aria-expanded", isOpen ? "true" : "false");
866 function setModulePanelOpen(panel, open) {
867 panel.classList.toggle("is-open", !!open);
868 syncModulePanelToggleIcon(panel);
870 window.requestAnimationFrame(function () {
871 syncAccessGroupScroll(panel);
876 function bindModulePanels(root) {
877 root = root || document;
878 const saved = dbx.uiGet ? dbx.uiGet(LIB, PANEL_UI_ID, "openPanels", {}) : {};
879 const openPanels = (saved && typeof saved === "object" && !Array.isArray(saved)) ? saved : {};
881 qsa(root, ".dbx-module-panel-wrap").forEach(function (panel, index) {
882 const key = panel.getAttribute("data-module-ui-state") || ("module-" + index);
883 panel.setAttribute("data-module-ui-state", key);
885 if (Object.prototype.hasOwnProperty.call(openPanels, key)) {
886 setModulePanelOpen(panel, !!openPanels[key]);
888 syncModulePanelToggleIcon(panel);
891 if (panel.__dbxModuleToggleReady) {
894 panel.__dbxModuleToggleReady = true;
896 function togglePanel() {
897 const nextOpen = !panel.classList.contains("is-open");
898 setModulePanelOpen(panel, nextOpen);
899 const state = dbx.uiGet ? dbx.uiGet(LIB, PANEL_UI_ID, "openPanels", {}) : {};
900 const next = (state && typeof state === "object" && !Array.isArray(state)) ? Object.assign({}, state) : {};
901 next[key] = nextOpen;
903 dbx.uiSet(LIB, PANEL_UI_ID, "openPanels", next);
907 const toggleBtn = qs(panel, ".dbx-module-panel-toggle-btn");
909 toggleBtn.addEventListener("click", function (e) {
916 const summaryMain = qs(panel, ".dbx-module-summary-main");
918 summaryMain.addEventListener("click", function (e) {
919 if (e.target.closest("input, label, .dbx-module-row-select")) {
929 if (!document.__dbxModulePanelsBound) {
930 document.__dbxModulePanelsBound = true;
931 bindModulePanels(document);
934 function ensureConfirm() {
935 if (dbx.confirm && typeof dbx.confirm.open === "function") {
936 return Promise.resolve(true);
938 return new Promise(function (resolve) {
939 if (!dbx.loadFeature) {
943 dbx.loadFeature("confirm", function () {
944 resolve(!!(dbx.confirm && typeof dbx.confirm.open === "function"));
949 function updateActiveToggleButton(btn, active, responseLabel) {
950 const isActive = String(active) === "1";
951 btn.setAttribute("data-active", isActive ? "1" : "0");
952 btn.classList.toggle("btn-success", isActive);
953 btn.classList.toggle("btn-outline-secondary", !isActive);
954 btn.innerHTML = '<i class="bi ' + (isActive ? "bi-toggle-on" : "bi-toggle-off") + '"></i> '
955 + escapeHtml(responseLabel || moduleMsg(btn, isActive ? "active" : "inactive", isActive ? "Active" : "Inactive"));
958 function bindModuleBarActions(root) {
959 root = root || document;
961 qsa(root, ".dbx-module-active-toggle").forEach(function (btn) {
962 if (btn.dataset.modActiveBound === "1") {
965 btn.dataset.modActiveBound = "1";
966 btn.addEventListener("click", function (e) {
969 const url = btn.getAttribute("data-toggle-url") || "";
970 const modul = btn.getAttribute("data-modul") || "";
971 if (!url || !modul) {
977 headers: { "Content-Type": "application/json" },
978 body: JSON.stringify({ xmodul: modul })
979 }).then(function (data) {
980 if (data && data.ok) {
981 updateActiveToggleButton(btn, data.active, data.active_label);
982 const panel = btn.closest(".dbx-module-panel-wrap");
983 const row = btn.closest(".dbx-module-row");
985 qsa(panel, "[data-module-status-badge]").forEach(function (badge) {
986 const isActive = String(data.active) === "1";
987 badge.textContent = data.active_label || moduleMsg(badge, isActive ? "active" : "inactive", isActive ? "Active" : "Inactive");
988 badge.classList.toggle("text-bg-success", isActive);
989 badge.classList.toggle("text-bg-secondary", !isActive);
993 row.classList.toggle("dbx-module-row-inactive", String(data.active) !== "1");
995 } else if (data && data.msg) {
996 window.alert(data.msg);
998 }).catch(function (err) {
999 window.alert(err && err.message ? err.message : moduleMsg(btn, "status_save_error", "Status could not be saved."));
1000 }).finally(function () {
1001 btn.disabled = false;
1006 qsa(root, ".dbx-module-delete-btn").forEach(function (btn) {
1007 if (btn.dataset.modDeleteBound === "1") {
1010 btn.dataset.modDeleteBound = "1";
1011 btn.addEventListener("click", function (e) {
1013 e.stopPropagation();
1014 const url = btn.getAttribute("data-delete-url") || "";
1015 const modul = btn.getAttribute("data-modul") || "";
1016 const title = btn.getAttribute("data-title") || modul;
1017 const panelRoot = btn.closest(".dbx-modules-admin") || document.body;
1018 if (!url || !modul) {
1022 ensureConfirm().then(function (ok) {
1024 dbx.warn("[modulesAdmin] " + moduleMsg(btn, "confirm_unavailable", "The confirmation dialog could not be loaded."));
1027 return dbx.confirm.open({
1028 id: "module-delete-" + modul + "-" + Date.now(),
1031 title: moduleMsg(btn, "module_delete_title", "Delete module"),
1032 question: moduleMsg(btn, "module_delete_question", "Delete module {module} completely?", { module: "<strong>" + escapeHtml(title) + "</strong>" }),
1033 hint: moduleMsg(btn, "module_delete_hint", "The module directory and all its files will be removed permanently."),
1035 labelyes: "<i class=\"bi bi-trash\"></i> " + escapeHtml(moduleMsg(btn, "delete_button", "Delete")),
1036 labelno: "<i class=\"bi bi-x-lg\"></i> " + escapeHtml(moduleMsg(btn, "cancel_button", "Cancel")),
1038 backdropclose: false,
1041 }).then(function (result) {
1042 if (!result || result.action !== "yes") {
1045 return fetchJson(url, {
1047 headers: { "Content-Type": "application/json" },
1048 body: JSON.stringify({ xmodul: modul })
1050 }).then(function (data) {
1055 const row = btn.closest(".dbx-module-row");
1059 } else if (data.msg) {
1060 window.alert(data.msg);
1062 }).catch(function (err) {
1063 if (err && err.message) {
1064 window.alert(err.message);
1071 function bindModimgManager(root) {
1072 const manager = qs(root, ".dbx-modimg-manager");
1073 if (!manager || manager.dataset.modimgBound === "1") {
1076 manager.dataset.modimgBound = "1";
1077 manager.classList.add("dbx-module-images-panel");
1078 if (!manager.getAttribute("data-module-images-list")) {
1079 const gallery = qs(manager, "[data-module-images-list]");
1081 manager.setAttribute("data-module-images-list", "1");
1086 function initModulesAdmin(root) {
1087 bindModuleDdGallery(root || document);
1088 bindModimgManager(root || document);
1089 bindModuleImagesPanels(root || document);
1090 bindModuleAccess(root || document);
1091 bindAccessGroupScroll(root || document);
1092 bindModulePanels(root || document);
1093 bindModuleBarActions(root || document);
1094 syncAccessGroupScroll(root || document);
1097 if (!document.__dbxModuleImagesBound) {
1098 document.__dbxModuleImagesBound = true;
1099 bindModuleImages(document);
1102 dbx.modulesAdmin = {
1103 init: initModulesAdmin,
1104 openImagesBrowser: openModuleImagesBrowser,
1105 rescan: function (ctx) {
1106 const root = ctx && ctx.root ? ctx.root : (ctx || document);
1107 initModulesAdmin(root);
1111 dbx.feature.register("modulesAdmin", {
1114 ["js", "lib", "openWin.js"],
1115 ["js", "lib", "confirm.js"]
1118 ["css", "root", "modules/dbxAdmin/tpl/css/modules-admin.css"],
1119 ["css", "design", "c-cms.css"]
1121 init: function (el) {
1122 dbx.modulesAdmin.init(el);
1124 rescan: function (ctx) {
1125 dbx.modulesAdmin.rescan(ctx);
1128})(window, document);