dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
modulesAdmin.js
Go to the documentation of this file.
1/*!
2 * @file modulesAdmin.js
3 * Modul-Verwaltung: DD-Gallery, Modul-Bilder (CMS-Medienbrowser), openWin
4 */
5(function (window, document) {
6 "use strict";
7
8 if (!window.dbx || !window.dbx.feature) {
9 return;
10 }
11
12 const dbx = window.dbx;
13 const LIB = "modulesAdmin";
14 const PANEL_UI_ID = "modules-admin";
15
16 function qs(el, sel) {
17 return el ? el.querySelector(sel) : null;
18 }
19
20 function qsa(el, sel) {
21 return el ? Array.prototype.slice.call(el.querySelectorAll(sel)) : [];
22 }
23
24 function modimgMediaCfg(root) {
25 root = root || document.querySelector(".dbx-modules-admin") || document;
26 return {
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") || ""
34 };
35 }
36
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");
41 }
42 if (panel.getAttribute("data-modimg-upload")) {
43 cfg.upload = panel.getAttribute("data-modimg-upload");
44 }
45 if (panel.getAttribute("data-modimg-mediafolders")) {
46 cfg.mediafolders = panel.getAttribute("data-modimg-mediafolders");
47 }
48 if (panel.getAttribute("data-modimg-mediafoldercreate")) {
49 cfg.mediafoldercreate = panel.getAttribute("data-modimg-mediafoldercreate");
50 }
51 if (panel.getAttribute("data-modimg-mediafolderdelete")) {
52 cfg.mediafolderdelete = panel.getAttribute("data-modimg-mediafolderdelete");
53 }
54 if (panel.getAttribute("data-modimg-deletemedia")) {
55 cfg.deletemedia = panel.getAttribute("data-modimg-deletemedia");
56 }
57 if (panel.getAttribute("data-modimg-uploadmax")) {
58 cfg.uploadmax = panel.getAttribute("data-modimg-uploadmax");
59 }
60 return cfg;
61 }
62
63 function ensureCmsMediaBrowser() {
64 if (dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function") {
65 return Promise.resolve(true);
66 }
67 return new Promise(function (resolve) {
68 if (!dbx.load) {
69 resolve(false);
70 return;
71 }
72 dbx.load([
73 ["js", "lib", "ajax.js"],
74 ["js", "lib", "openWin.js"],
75 ["js", "lib", "cms.js"]
76 ], function () {
77 resolve(!!(dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function"));
78 });
79 });
80 }
81
82 function getActiveDdItem(galleryId) {
83 const gallery = document.getElementById(galleryId);
84 if (!gallery) {
85 return null;
86 }
87 return gallery.querySelector(".dbx-module-dd-item.is-active") || gallery.querySelector(".dbx-module-dd-item");
88 }
89
90 function openDdItem(item) {
91 if (!item || !dbx.openWin) {
92 return;
93 }
94 const url = item.getAttribute("data-edit-url") || "";
95 const title = item.getAttribute("data-edit-title") || "DD";
96 if (!url) {
97 return;
98 }
99 dbx.openWin.open({
100 url: url,
101 title: title,
102 width: "960",
103 height: "92%",
104 position: "center-top",
105 minimizable: 1,
106 maximizable: 1
107 });
108 }
109
110 function bindModuleDdGallery(root) {
111 const scope = root || document;
112
113 scope.querySelectorAll(".dbx-module-dd-gallery").forEach(function (gallery) {
114 if (gallery.dataset.ddBound === "1") {
115 return;
116 }
117 gallery.dataset.ddBound = "1";
118
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");
124 });
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;
129 if (btn) {
130 btn.setAttribute("data-dd-active", item.getAttribute("data-edit-url") || "");
131 }
132 });
133 item.addEventListener("dblclick", function () {
134 openDdItem(item);
135 });
136 });
137 });
138
139 scope.querySelectorAll(".dbx-module-dd-open-btn").forEach(function (btn) {
140 if (btn.dataset.ddBound === "1") {
141 return;
142 }
143 btn.dataset.ddBound = "1";
144 const galleryId = btn.getAttribute("data-dd-select") || "";
145 btn.addEventListener("click", function () {
146 const item = getActiveDdItem(galleryId);
147 openDdItem(item);
148 });
149 });
150 }
151
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."));
156 }
157
158 return dbx.ajax.request({
159 url: url,
160 method: options.method || "GET",
161 mode: "json",
162 body: (typeof options.body !== "undefined") ? options.body : null,
163 headers: options.headers || {},
164 timeout: Number(options.timeout || 20000)
165 });
166 }
167
168 function updatePanelPreview(panel, items) {
169 const previewCol = qs(panel, "[data-module-images-preview]");
170 if (!previewCol) {
171 return;
172 }
173 const modul = panel.getAttribute("data-modul") || "";
174 const placeholder = panel.getAttribute("data-placeholder-url") || "";
175 const placeholderAlt = panel.getAttribute("data-placeholder-alt") || modul;
176 let url = "";
177 let label = 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;
183 }
184 if (!url) {
185 url = placeholder;
186 }
187 if (url) {
188 previewCol.innerHTML = '<img src="' + escapeAttr(url) + '" alt="' + escapeAttr(isPlaceholder ? placeholderAlt : label) + '" class="dbx-module-images-preview-img' + (isPlaceholder ? " is-placeholder" : "") + '" loading="lazy">';
189 return;
190 }
191 previewCol.innerHTML = '<span class="dbx-module-images-placeholder-icon" aria-hidden="true"><i class="bi bi-box-seam"></i></span>';
192 }
193
194 function updateSymbolPreview(panel, symbol) {
195 const previewCol = qs(panel, "[data-module-symbol-preview]");
196 if (!previewCol) {
197 return;
198 }
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);
205 if (url) {
206 previewCol.innerHTML = '<img src="' + escapeAttr(url) + '" alt="' + escapeAttr(alt) + '" class="dbx-module-symbol-img' + (symbol.url ? "" : " is-placeholder") + '" loading="lazy">';
207 return;
208 }
209 previewCol.innerHTML = '<span class="dbx-module-images-placeholder-icon" aria-hidden="true"><i class="bi bi-box-seam"></i></span>';
210 }
211
212 function renderModuleImagesEmptyListHtml() {
213 return '<div class="dbx-module-images-empty-list text-muted small">Keine Modulbilder</div>';
214 }
215
216 const MOD_FILENAME_SEP = "__";
217
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) {
223 return name;
224 }
225 name += MOD_FILENAME_SEP + run1;
226 if (run2) {
227 name += MOD_FILENAME_SEP + run2;
228 }
229 return name;
230 }
231
232 function buildModuleImageCall(modul, params) {
233 const call = "dbx_modul=" + encodeURIComponent(modul || "");
234 if (params) {
235 return call + "&" + params;
236 }
237 return call;
238 }
239
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=([^&]+)/);
247 if (!run1 && m1) {
248 run1 = decodeURIComponent(m1[1].replace(/\+/g, " "));
249 }
250 if (!run2 && m2) {
251 run2 = decodeURIComponent(m2[1].replace(/\+/g, " "));
252 }
253 }
254 if (!run1 && panel) {
255 run1 = String(panel.getAttribute("data-default-run1") || "").trim();
256 }
257 if (!run2 && panel) {
258 run2 = String(panel.getAttribute("data-default-run2") || "").trim();
259 }
260 let url = "?dbx_modul=" + encodeURIComponent(modul || "");
261 if (run1) {
262 url += "&dbx_run1=" + encodeURIComponent(run1);
263 }
264 if (run2) {
265 url += "&dbx_run2=" + encodeURIComponent(run2);
266 }
267 return { url: url, run1: run1, run2: run2 };
268 }
269
270 function setActiveGalleryItem(panel, item) {
271 const list = qs(panel, "[data-module-images-list]");
272 if (!list) {
273 return;
274 }
275 qsa(list, ".dbx-module-images-item").forEach(function (el) {
276 el.classList.toggle("is-active", item && el === item);
277 });
278 }
279
280 function escapeHtml(text) {
281 return String(text || "")
282 .replace(/&/g, "&amp;")
283 .replace(/</g, "&lt;")
284 .replace(/>/g, "&gt;")
285 .replace(/"/g, "&quot;");
286 }
287
288 function escapeAttr(text) {
289 return escapeHtml(text);
290 }
291
292 function renderModuleImagesList(container, items) {
293 if (!container) {
294 return;
295 }
296 if (!items || !items.length) {
297 container.innerHTML = renderModuleImagesEmptyListHtml();
298 return;
299 }
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>'
315 : "";
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">'
321 + previewBtn
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>'
323 + '</span></div>';
324 }).join("");
325 }
326
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() : "";
332 if (!run1) {
333 run1 = String(panel.getAttribute("data-active-run1") || panel.getAttribute("data-default-run1") || "").trim();
334 }
335 if (!run2) {
336 run2 = String(panel.getAttribute("data-active-run2") || panel.getAttribute("data-default-run2") || "").trim();
337 }
338 return { run1: run1, run2: run2 };
339 }
340
341 function setPanelRunTarget(panel, run1, run2) {
342 const run1El = qs(panel, ".dbx-module-images-run1");
343 const run2El = qs(panel, ".dbx-module-images-run2");
344 if (run1El) {
345 run1El.value = run1 || "";
346 }
347 if (run2El) {
348 run2El.value = run2 || "";
349 }
350 panel.setAttribute("data-active-run1", run1 || "");
351 panel.setAttribute("data-active-run2", run2 || "");
352 updateFilenamePreview(panel);
353 }
354
355 function updateFilenamePreview(panel) {
356 const preview = qs(panel, ".dbx-module-images-filename-preview");
357 if (!preview) {
358 return;
359 }
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.*";
364 return;
365 }
366 preview.textContent = stemForRuns(modul, target.run1, target.run2) + ".*";
367 }
368
369 function applyRunTargetToPanel(panel, run1, run2) {
370 setPanelRunTarget(panel, run1, run2);
371 }
372
373 function selectGalleryItem(panel, item) {
374 if (!panel || !item) {
375 return;
376 }
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 : "";
381 if (url) {
382 updatePanelPreview(panel, [{ url: url, label: label }]);
383 }
384 }
385
386 function bindModuleImagesPanel(panel) {
387 if (!panel || panel.dataset.modimgTargetBound === "1") {
388 return;
389 }
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);
398 }
399 });
400 updateFilenamePreview(panel);
401 const list = qs(panel, "[data-module-images-list]");
402 if (list) {
403 const items = [];
404 qsa(list, ".dbx-module-images-item").forEach(function (item) {
405 items.push({
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") || ""
412 });
413 });
414 if (items.length) {
415 updatePanelPreview(panel, items);
416 const first = qs(list, ".dbx-module-images-item");
417 if (first) {
418 selectGalleryItem(panel, first);
419 }
420 }
421 }
422 }
423
424 function bindModuleImagesPanels(scope) {
425 qsa(scope || document, ".dbx-module-images-panel").forEach(bindModuleImagesPanel);
426 }
427
428 function moduleImagesPanelFromTarget(target) {
429 if (!target || !target.closest) {
430 return null;
431 }
432 const panel = target.closest(".dbx-module-images-panel");
433 if (panel) {
434 return panel;
435 }
436 const modul = target.getAttribute("data-modul") || "";
437 if (!modul) {
438 return null;
439 }
440 return document.querySelector('.dbx-module-images-panel[data-modul="' + modul.replace(/"/g, '\\"') + '"]');
441 }
442
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);
448 }
449 if (galleryHead) {
450 galleryHead.textContent = String((items && items.length) || 0);
451 }
452 if (items && items.length) {
453 updatePanelPreview(panel, items);
454 const first = list ? qs(list, ".dbx-module-images-item") : null;
455 if (first) {
456 selectGalleryItem(panel, first);
457 }
458 } else {
459 updatePanelPreview(panel, []);
460 setActiveGalleryItem(panel, null);
461 }
462 }
463
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 });
468 }
469
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);
476 }
477 if (!target.run1) {
478 window.alert("Bitte dbx_run1 angeben.");
479 return Promise.resolve(null);
480 }
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);
486 }
487 payload.xmodul = modul;
488 payload.run1 = target.run1;
489 payload.run2 = target.run2;
490 return fetchJson(addUrl, {
491 method: "POST",
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);
499 }
500 return data;
501 }).catch(function (err) {
502 window.alert(err && err.message ? err.message : "Bild konnte nicht uebernommen werden.");
503 throw err;
504 });
505 }
506
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);
512 }
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);
518 }
519 payload.xmodul = modul;
520
521 return fetchJson(addUrl, {
522 method: "POST",
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);
530 }
531 return data;
532 }).catch(function (err) {
533 window.alert(err && err.message ? err.message : "Symbolbild konnte nicht uebernommen werden.");
534 throw err;
535 });
536 }
537
538 function openModuleSymbolBrowser(panel) {
539 if (!panel) {
540 return;
541 }
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;
550
551 ensureCmsMediaBrowser().then(function (ok) {
552 if (!ok || !dbx.cmsMediaBrowser) {
553 return;
554 }
555 dbx.cmsMediaBrowser.open(root, cfg, {
556 mode: "pick",
557 mediaFolder: "img",
558 afterAssign: function (row) {
559 return addModuleSymbol(panel, row);
560 }
561 });
562 });
563 }
564
565 function openModuleImagesBrowser(panel) {
566 if (!panel) {
567 return;
568 }
569 const target = getPanelRunTarget(panel);
570 if (!target.run1) {
571 window.alert("Bitte dbx_run1 fuer den Bildnamen angeben.");
572 return;
573 }
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;
580 }
581
582 ensureCmsMediaBrowser().then(function (ok) {
583 if (!ok || !dbx.cmsMediaBrowser) {
584 return;
585 }
586 dbx.cmsMediaBrowser.open(root, cfg, {
587 mode: "pick",
588 mediaFolder: "mod",
589 formDataExtra: {
590 xmodul: modul,
591 run1: target.run1,
592 run2: target.run2
593 },
594 afterAssign: function (row) {
595 return addModuleImage(panel, row);
596 }
597 });
598 });
599 }
600
601 function bindModuleImages(scope) {
602 scope = scope || document;
603
604 scope.addEventListener("click", function (e) {
605 const symbolBtn = e.target.closest(".dbx-module-symbol-pick");
606 if (symbolBtn) {
607 e.preventDefault();
608 const panel = moduleImagesPanelFromTarget(symbolBtn);
609 if (panel) {
610 openModuleSymbolBrowser(panel);
611 }
612 return;
613 }
614
615 const pickBtn = e.target.closest(".dbx-module-images-pick, .dbx-module-images-pick-btn");
616 if (pickBtn) {
617 e.preventDefault();
618 const panel = moduleImagesPanelFromTarget(pickBtn);
619 if (panel) {
620 openModuleImagesBrowser(panel);
621 }
622 return;
623 }
624
625 const emptyPick = e.target.closest(".dbx-module-images-empty");
626 if (emptyPick && emptyPick.classList.contains("dbx-module-images-pick")) {
627 e.preventDefault();
628 const panel = emptyPick.closest(".dbx-module-images-panel");
629 if (panel) {
630 openModuleImagesBrowser(panel);
631 }
632 return;
633 }
634
635 const removeBtn = e.target.closest(".dbx-module-images-remove");
636 if (removeBtn) {
637 e.preventDefault();
638 e.stopPropagation();
639 const item = removeBtn.closest(".dbx-module-images-item");
640 const panel = removeBtn.closest(".dbx-module-images-panel");
641 if (!item || !panel) {
642 return;
643 }
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) {
648 return;
649 }
650 const panelRoot = panel.closest(".dbx-modules-admin") || panel.closest(".dbx-modimg-manager") || document.body;
651 removeBtn.disabled = true;
652 ensureConfirm().then(function (ok) {
653 if (!ok) {
654 window.alert("confirm.js konnte nicht geladen werden.");
655 return null;
656 }
657 return dbx.confirm.open({
658 id: "module-image-remove-" + modul + "-" + Date.now(),
659 root: panelRoot,
660 source: removeBtn,
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.",
664 buttons: "yesno",
665 labelyes: "<i class=\"bi bi-trash\"></i> Loeschen",
666 labelno: "<i class=\"bi bi-x-lg\"></i> Abbrechen",
667 closable: true,
668 backdropclose: false,
669 escclose: true
670 });
671 }).then(function (result) {
672 if (!result || result.action !== "yes") {
673 return null;
674 }
675 return fetchJson(removeUrl, {
676 method: "POST",
677 headers: { "Content-Type": "application/json" },
678 body: JSON.stringify({ xmodul: modul, file: file, delete_file: 1 })
679 });
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);
685 }
686 }).catch(function (err) {
687 if (err && err.message) {
688 window.alert(err.message);
689 }
690 }).finally(function () {
691 removeBtn.disabled = false;
692 });
693 return;
694 }
695
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");
699 if (panel) {
700 selectGalleryItem(panel, galleryItem);
701 }
702 }
703 });
704
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);
709 }
710 });
711 }
712
713 function syncAccessGroupScroll(target) {
714 const wraps = [];
715 if (target && target.classList && target.classList.contains("dbx-module-access-groups-scroll")) {
716 wraps.push(target);
717 } else if (target && target.querySelectorAll) {
718 qsa(target, ".dbx-module-access-groups-scroll").forEach(function (wrap) {
719 wraps.push(wrap);
720 });
721 } else {
722 qsa(document, ".dbx-module-access-groups-scroll").forEach(function (wrap) {
723 wraps.push(wrap);
724 });
725 }
726
727 wraps.forEach(function (wrap) {
728 const select = qs(wrap, ".dbx-module-access-groups");
729 if (!select) {
730 return;
731 }
732 const h = wrap.clientHeight;
733 if (h < 1) {
734 return;
735 }
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;
741 });
742 }
743
744 function bindAccessGroupScroll(scope) {
745 scope = scope || document;
746 qsa(scope, ".dbx-module-access-groups-scroll").forEach(function (wrap) {
747 if (wrap.__dbxAccessScrollBound === "1") {
748 return;
749 }
750 wrap.__dbxAccessScrollBound = "1";
751 const sync = function () {
752 syncAccessGroupScroll(wrap);
753 };
754 sync();
755 if (typeof ResizeObserver !== "undefined") {
756 const ro = new ResizeObserver(sync);
757 ro.observe(wrap);
758 wrap.__dbxAccessScrollRo = ro;
759 }
760 });
761 }
762
763 function bindModuleAccess(scope) {
764 scope = scope || document;
765 qsa(scope, ".dbx-module-access-save").forEach(function (btn) {
766 if (btn.dataset.accessBound === "1") {
767 return;
768 }
769 btn.dataset.accessBound = "1";
770 btn.addEventListener("click", function () {
771 const panel = btn.closest(".dbx-module-access-panel");
772 if (!panel) {
773 return;
774 }
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) {
780 return;
781 }
782 const groups = Array.prototype.slice.call(select.selectedOptions).map(function (opt) {
783 return opt.value;
784 });
785 btn.disabled = true;
786 if (status) {
787 status.textContent = "Speichern…";
788 status.className = "dbx-module-access-status small text-muted";
789 }
790 fetchJson(saveUrl, {
791 method: "POST",
792 headers: { "Content-Type": "application/json" },
793 body: JSON.stringify({ xmodul: modul, groups: groups })
794 }).then(function (data) {
795 if (status) {
796 if (data && data.ok) {
797 status.textContent = data.msg || "Gespeichert";
798 status.className = "dbx-module-access-status small is-ok";
799 } else {
800 status.textContent = (data && data.msg) ? data.msg : "Speichern fehlgeschlagen";
801 status.className = "dbx-module-access-status small is-error";
802 }
803 } else if (data && data.msg && !data.ok) {
804 window.alert(data.msg);
805 }
806 }).catch(function (err) {
807 if (status) {
808 status.textContent = err && err.message ? err.message : "Speichern fehlgeschlagen";
809 status.className = "dbx-module-access-status small is-error";
810 }
811 }).finally(function () {
812 btn.disabled = false;
813 });
814 });
815 });
816 }
817
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");
822 if (icon) {
823 icon.classList.toggle("bi-chevron-down", isOpen);
824 icon.classList.toggle("bi-chevron-right", !isOpen);
825 }
826 if (btn) {
827 btn.setAttribute("aria-expanded", isOpen ? "true" : "false");
828 }
829 }
830
831 function setModulePanelOpen(panel, open) {
832 panel.classList.toggle("is-open", !!open);
833 syncModulePanelToggleIcon(panel);
834 if (open) {
835 window.requestAnimationFrame(function () {
836 syncAccessGroupScroll(panel);
837 });
838 }
839 }
840
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 : {};
845
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);
849
850 if (Object.prototype.hasOwnProperty.call(openPanels, key)) {
851 setModulePanelOpen(panel, !!openPanels[key]);
852 } else {
853 syncModulePanelToggleIcon(panel);
854 }
855
856 if (panel.__dbxModuleToggleReady) {
857 return;
858 }
859 panel.__dbxModuleToggleReady = true;
860
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;
867 if (dbx.uiSet) {
868 dbx.uiSet(LIB, PANEL_UI_ID, "openPanels", next);
869 }
870 }
871
872 const toggleBtn = qs(panel, ".dbx-module-panel-toggle-btn");
873 if (toggleBtn) {
874 toggleBtn.addEventListener("click", function (e) {
875 e.preventDefault();
876 e.stopPropagation();
877 togglePanel();
878 });
879 }
880
881 const summaryMain = qs(panel, ".dbx-module-summary-main");
882 if (summaryMain) {
883 summaryMain.addEventListener("click", function (e) {
884 if (e.target.closest("input, label, .dbx-module-row-select")) {
885 return;
886 }
887 e.preventDefault();
888 togglePanel();
889 });
890 }
891 });
892 }
893
894 if (!document.__dbxModulePanelsBound) {
895 document.__dbxModulePanelsBound = true;
896 bindModulePanels(document);
897 }
898
899 function ensureConfirm() {
900 if (dbx.confirm && typeof dbx.confirm.open === "function") {
901 return Promise.resolve(true);
902 }
903 return new Promise(function (resolve) {
904 if (!dbx.loadFeature) {
905 resolve(false);
906 return;
907 }
908 dbx.loadFeature("confirm", function () {
909 resolve(!!(dbx.confirm && typeof dbx.confirm.open === "function"));
910 });
911 });
912 }
913
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");
921 }
922
923 function bindModuleBarActions(root) {
924 root = root || document;
925
926 qsa(root, ".dbx-module-active-toggle").forEach(function (btn) {
927 if (btn.dataset.modActiveBound === "1") {
928 return;
929 }
930 btn.dataset.modActiveBound = "1";
931 btn.addEventListener("click", function (e) {
932 e.preventDefault();
933 e.stopPropagation();
934 const url = btn.getAttribute("data-toggle-url") || "";
935 const modul = btn.getAttribute("data-modul") || "";
936 if (!url || !modul) {
937 return;
938 }
939 btn.disabled = true;
940 fetchJson(url, {
941 method: "POST",
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");
949 if (panel) {
950 qsa(panel, ".dbx-module-row-badges .badge").forEach(function (badge) {
951 const text = String(badge.textContent || "").trim();
952 if (text !== "Aktiv" && text !== "Inaktiv") {
953 return;
954 }
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);
959 });
960 }
961 if (row) {
962 row.classList.toggle("dbx-module-row-inactive", String(data.active) !== "1");
963 }
964 } else if (data && data.msg) {
965 window.alert(data.msg);
966 }
967 }).catch(function (err) {
968 window.alert(err && err.message ? err.message : "Status konnte nicht gespeichert werden.");
969 }).finally(function () {
970 btn.disabled = false;
971 });
972 });
973 });
974
975 qsa(root, ".dbx-module-delete-btn").forEach(function (btn) {
976 if (btn.dataset.modDeleteBound === "1") {
977 return;
978 }
979 btn.dataset.modDeleteBound = "1";
980 btn.addEventListener("click", function (e) {
981 e.preventDefault();
982 e.stopPropagation();
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) {
988 return;
989 }
990
991 ensureConfirm().then(function (ok) {
992 if (!ok) {
993 if (!window.confirm('Modul "' + title + '" wirklich loeschen?')) {
994 return null;
995 }
996 return { action: "yes" };
997 }
998 return dbx.confirm.open({
999 id: "module-delete-" + modul + "-" + Date.now(),
1000 root: panelRoot,
1001 source: btn,
1002 title: "Modul loeschen",
1003 question: "Modul <strong>" + escapeHtml(title) + "</strong> komplett loeschen?",
1004 hint: "Das Modulverzeichnis inklusive aller Dateien wird unwiderruflich entfernt.",
1005 buttons: "yesno",
1006 labelyes: "<i class=\"bi bi-trash\"></i> Loeschen",
1007 labelno: "<i class=\"bi bi-x-lg\"></i> Abbrechen",
1008 closable: true,
1009 backdropclose: false,
1010 escclose: true
1011 });
1012 }).then(function (result) {
1013 if (!result || result.action !== "yes") {
1014 return null;
1015 }
1016 return fetchJson(url, {
1017 method: "POST",
1018 headers: { "Content-Type": "application/json" },
1019 body: JSON.stringify({ xmodul: modul })
1020 });
1021 }).then(function (data) {
1022 if (!data) {
1023 return;
1024 }
1025 if (data.ok) {
1026 const row = btn.closest(".dbx-module-row");
1027 if (row) {
1028 row.remove();
1029 }
1030 } else if (data.msg) {
1031 window.alert(data.msg);
1032 }
1033 }).catch(function (err) {
1034 if (err && err.message) {
1035 window.alert(err.message);
1036 }
1037 });
1038 });
1039 });
1040 }
1041
1042 function bindModimgManager(root) {
1043 const manager = qs(root, ".dbx-modimg-manager");
1044 if (!manager || manager.dataset.modimgBound === "1") {
1045 return;
1046 }
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]");
1051 if (gallery) {
1052 manager.setAttribute("data-module-images-list", "1");
1053 }
1054 }
1055 }
1056
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);
1066 }
1067
1068 if (!document.__dbxModuleImagesBound) {
1069 document.__dbxModuleImagesBound = true;
1070 bindModuleImages(document);
1071 }
1072
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);
1079 }
1080 };
1081
1082 dbx.feature.register("modulesAdmin", {
1083 scope: "element",
1084 js: [
1085 ["js", "lib", "openWin.js"],
1086 ["js", "lib", "confirm.js"]
1087 ],
1088 css: [
1089 ["css", "root", "modules/dbxAdmin/tpl/css/modules-admin.css"],
1090 ["css", "design", "c-cms.css"]
1091 ],
1092 init: function (el) {
1093 dbx.modulesAdmin.init(el);
1094 },
1095 rescan: function (ctx) {
1096 dbx.modulesAdmin.rescan(ctx);
1097 }
1098 });
1099})(window, document);