3 * Shop-Administration: CMS-Medienbrowser fuer Artikelbilder.
5(function (window, document) {
8 if (!window.dbx || !window.dbx.feature) {
12 const dbx = window.dbx;
13 const LIB = "shopAdmin";
14 let reloadTimer = null;
17 root = root || document.querySelector(".dbx-shop-media-manager") || document;
19 media: root.getAttribute("data-shop-media") || "",
20 upload: root.getAttribute("data-shop-upload") || "",
21 externalvideo: root.getAttribute("data-shop-externalvideo") || "",
22 mediafolders: root.getAttribute("data-shop-mediafolders") || "",
23 mediafoldercreate: root.getAttribute("data-shop-mediafoldercreate") || "",
24 mediafolderdelete: root.getAttribute("data-shop-mediafolderdelete") || "",
25 mediafolderrename: root.getAttribute("data-shop-mediafolderrename") || "",
26 mediamove: root.getAttribute("data-shop-mediamove") || "",
27 mediaunused: root.getAttribute("data-shop-mediaunused") || "",
28 mediaprocess: root.getAttribute("data-shop-mediaprocess") || "",
29 deletemedia: root.getAttribute("data-shop-deletemedia") || "",
30 editmedia: root.getAttribute("data-shop-editmedia") || "",
31 uploadmediafolder: root.getAttribute("data-shop-uploadmediafolder") || "",
32 assignurl: root.getAttribute("data-shop-assignurl") || ""
36 function loadSharedMediaBrowserAssets() {
37 return new Promise(function (resolve) {
43 ["css", "design", "c-cms.css"],
44 ["js", "lib", "ajax.js"],
45 ["js", "lib", "openWin.js"],
46 ["js", "lib", "cms.js"]
48 resolve(!!(dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function"));
53 function ensureCmsMediaBrowser() {
54 if (dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function") {
55 return loadSharedMediaBrowserAssets();
57 return loadSharedMediaBrowserAssets();
60 function ensureAjax() {
61 return new Promise(function (resolve) {
62 if (dbx.ajax && typeof dbx.ajax.request === "function") {
70 dbx.load([["js", "lib", "ajax.js"]], function () {
71 resolve(!!(dbx.ajax && typeof dbx.ajax.request === "function"));
76 function fetchJson(url, options) {
77 options = options || {};
78 if (!dbx.ajax || typeof dbx.ajax.request !== "function") {
79 return Promise.reject(new Error("ajax.js nicht geladen."));
81 return dbx.ajax.request({
83 method: options.method || "GET",
85 body: typeof options.body !== "undefined" ? options.body : null,
86 headers: options.headers || {},
87 timeout: Number(options.timeout || 20000)
91 function scheduleReload() {
93 window.clearTimeout(reloadTimer);
95 reloadTimer = window.setTimeout(function () {
96 if (window.dbx && dbx.utilities && dbx.utilities.leaveGuard) {
97 dbx.utilities.leaveGuard.allowOnce();
99 window.location.reload();
103 function assignMedia(root, row) {
104 const settings = cfg(root);
105 if (!settings.assignurl) {
106 return Promise.reject(new Error("Shop-Zuordnung ist nicht konfiguriert."));
108 const product = root.querySelector("[data-shop-product-select]");
109 const group = root.querySelector("[data-shop-group-select]");
110 const sorter = root.querySelector("[data-shop-sorter]");
111 const primary = root.querySelector("[data-shop-primary]");
112 const productId = product ? parseInt(product.value || "0", 10) || 0 : 0;
113 const groupId = group ? parseInt(group.value || "0", 10) || 0 : 0;
114 if (productId <= 0 && groupId <= 0) {
115 window.alert("Bitte zuerst einen Artikel oder eine Artikelgruppe auswaehlen.");
116 return Promise.resolve(false);
119 product_id: productId,
121 media_id: parseInt(row.id || row.media_id || "0", 10) || 0,
122 title: row.title || row.label || row.file_name || "",
123 alt: row.alt || row.title || row.label || "",
124 sorter: sorter ? (parseInt(sorter.value || "100", 10) || 100) : 100,
125 is_primary: primary && primary.checked ? 1 : 0
127 if (payload.media_id <= 0) {
128 window.alert("Das ausgewaehlte Medium hat keine gueltige ID.");
129 return Promise.resolve(false);
131 return fetchJson(settings.assignurl, {
133 headers: { "Content-Type": "application/json" },
134 body: JSON.stringify(payload)
135 }).then(function (data) {
136 if (!data || !data.ok) {
137 window.alert((data && data.msg) ? data.msg : "Bild konnte nicht zugeordnet werden.");
142 }).catch(function (err) {
143 window.alert(err && err.message ? err.message : "Bild konnte nicht zugeordnet werden.");
148 function openMediaBrowser(root, button) {
149 ensureCmsMediaBrowser().then(function (ok) {
150 if (!ok || !dbx.cmsMediaBrowser) {
151 window.alert("CMS-Medienbrowser konnte nicht geladen werden.");
154 const settings = cfg(root);
155 dbx.cmsMediaBrowser.open(root, settings, {
157 mediaFolder: button.getAttribute("data-shop-media-folder") || settings.uploadmediafolder || "all",
158 afterAssign: function (row) {
159 return assignMedia(root, row);
165 function readDragPayload(event) {
167 return JSON.parse(event.dataTransfer.getData("application/json") || "{}");
173 function moveTreeNode(root, drop, payload) {
174 const url = root.getAttribute("data-shop-tree-moveurl") || "";
175 const targetGroupId = parseInt(drop.getAttribute("data-shop-tree-drop") || "0", 10) || 0;
176 if (!url || !payload || !payload.type) {
179 if (payload.type === "product" && targetGroupId <= 0) {
180 window.alert("Artikel koennen nur auf eine Artikelgruppe gezogen werden.");
185 target_group_id: targetGroupId
187 if (payload.type === "product") {
188 body.product_id = parseInt(payload.id || "0", 10) || 0;
189 } else if (payload.type === "group") {
190 body.group_id = parseInt(payload.id || "0", 10) || 0;
192 ensureAjax().then(function (ok) {
194 window.alert("AJAX konnte nicht geladen werden.");
197 return fetchJson(url, {
199 headers: { "Content-Type": "application/json" },
200 body: JSON.stringify(body)
201 }).then(function (data) {
202 if (!data || !data.ok) {
203 window.alert((data && data.msg) ? data.msg : "Verschieben nicht moeglich.");
208 }).catch(function (err) {
209 window.alert(err && err.message ? err.message : "Verschieben nicht moeglich.");
213 function normalizeSearch(value) {
214 return String(value || "")
217 .replace(/[\u0300-\u036f]/g, "")
218 .replace(/\s+/g, " ")
222 function setGroupCollapsed(group, collapsed) {
224 group.classList.toggle("is-collapsed", !!collapsed);
225 const button = group.querySelector(":scope > .dbx-shop-tree-group-head [data-shop-tree-group-toggle]");
227 button.setAttribute("aria-expanded", collapsed ? "false" : "true");
228 const icon = button.querySelector("i");
230 icon.classList.toggle("bi-chevron-right", !!collapsed);
231 icon.classList.toggle("bi-chevron-down", !collapsed);
236 function clearTreeSearchState(root) {
237 root.querySelectorAll(".is-search-match,.is-search-path").forEach(function (node) {
238 node.classList.remove("is-search-match", "is-search-path");
240 root.querySelectorAll("[data-shop-tree-hidden]").forEach(function (node) {
242 node.removeAttribute("data-shop-tree-hidden");
246 function applyTreeSearch(root) {
247 const input = root.querySelector("[data-shop-tree-search]");
248 const term = normalizeSearch(input ? input.value : "");
249 root.classList.toggle("is-searching", term !== "");
250 clearTreeSearchState(root);
255 function nodeText(node) {
256 return normalizeSearch(node.getAttribute("data-shop-tree-search-text") || node.textContent || "");
259 function filterProduct(product) {
260 const match = nodeText(product).includes(term);
261 product.hidden = !match;
263 product.setAttribute("data-shop-tree-hidden", "1");
265 product.classList.add("is-search-match");
270 function filterProductList(list) {
272 list.querySelectorAll(":scope > .dbx-shop-tree-products > .dbx-shop-tree-product").forEach(function (product) {
273 any = filterProduct(product) || any;
277 list.setAttribute("data-shop-tree-hidden", "1");
282 function filterGroup(group) {
283 const ownMatch = nodeText(group).includes(term);
284 let childMatch = false;
285 const children = group.querySelector(":scope > .dbx-shop-tree-children");
287 Array.prototype.forEach.call(children.children, function (child) {
288 if (child.matches(".dbx-shop-tree-group")) {
289 childMatch = filterGroup(child) || childMatch;
290 } else if (child.matches(".dbx-shop-tree-product-list")) {
291 childMatch = filterProductList(child) || childMatch;
295 const visible = ownMatch || childMatch;
296 group.hidden = !visible;
298 group.setAttribute("data-shop-tree-hidden", "1");
299 } else if (ownMatch) {
300 group.classList.add("is-search-match");
302 children.querySelectorAll("[data-shop-tree-hidden]").forEach(function (node) {
304 node.removeAttribute("data-shop-tree-hidden");
308 group.classList.add("is-search-path");
313 root.querySelectorAll(".dbx-shop-tree-list > .dbx-shop-tree-group").forEach(filterGroup);
314 const ungrouped = root.querySelector(".dbx-shop-tree-ungrouped");
316 let anyUngrouped = false;
317 ungrouped.querySelectorAll(".dbx-shop-tree-product").forEach(function (product) {
318 anyUngrouped = filterProduct(product) || anyUngrouped;
320 ungrouped.hidden = !anyUngrouped;
322 ungrouped.setAttribute("data-shop-tree-hidden", "1");
327 function initProductTree(root) {
328 if (root.__dbxShopProductTreeBound) return;
329 root.__dbxShopProductTreeBound = true;
331 root.querySelectorAll("[data-shop-tree-node]").forEach(function (node) {
332 node.setAttribute("draggable", "true");
335 root.addEventListener("dragstart", function (event) {
336 if (event.target.closest("button,a,input,select,textarea")) return;
337 const node = event.target.closest("[data-shop-tree-node]");
338 if (!node || !root.contains(node)) return;
339 const type = node.getAttribute("data-shop-tree-node");
340 const id = type === "product"
341 ? node.getAttribute("data-shop-tree-product")
342 : node.getAttribute("data-shop-tree-group");
343 event.dataTransfer.effectAllowed = "move";
344 event.dataTransfer.setData("application/json", JSON.stringify({ type: type, id: id }));
345 event.dataTransfer.setData("text/plain", type + ":" + id);
346 node.classList.add("is-dragging");
349 root.addEventListener("dragend", function () {
350 root.querySelectorAll(".is-dragging,.is-drop-target").forEach(function (el) {
351 el.classList.remove("is-dragging", "is-drop-target");
355 root.addEventListener("dragover", function (event) {
356 const drop = event.target.closest("[data-shop-tree-drop]");
357 if (!drop || !root.contains(drop)) return;
358 event.preventDefault();
359 event.dataTransfer.dropEffect = "move";
360 root.querySelectorAll(".is-drop-target").forEach(function (el) {
361 if (el !== drop) el.classList.remove("is-drop-target");
363 drop.classList.add("is-drop-target");
366 root.addEventListener("dragleave", function (event) {
367 const drop = event.target.closest("[data-shop-tree-drop]");
368 if (drop) drop.classList.remove("is-drop-target");
371 root.addEventListener("drop", function (event) {
372 const drop = event.target.closest("[data-shop-tree-drop]");
373 if (!drop || !root.contains(drop)) return;
374 event.preventDefault();
375 drop.classList.remove("is-drop-target");
376 moveTreeNode(root, drop, readDragPayload(event));
379 const search = root.querySelector("[data-shop-tree-search]");
381 search.addEventListener("input", function () {
382 applyTreeSearch(root);
387 function findProductTreeShell(element, fallbackRoot) {
389 const direct = element.closest("[data-shop-tree-shell]");
390 if (direct) return direct;
391 const panel = element.closest(".dbx-panel");
393 const shell = panel.querySelector("[data-shop-tree-shell]");
394 if (shell) return shell;
397 if (fallbackRoot && fallbackRoot.querySelector) {
398 return fallbackRoot.querySelector("[data-shop-tree-shell]") || null;
403 function setProductTreeOpen(shell, open) {
405 const container = shell.closest(".dbx-panel") || shell;
406 shell.classList.toggle("is-shop-tree-collapsed", !open);
407 shell.classList.toggle("is-shop-tree-open", !!open);
408 container.querySelectorAll("[data-shop-tree-toggle]").forEach(function (button) {
409 button.setAttribute("aria-expanded", open ? "true" : "false");
410 button.setAttribute("aria-label", open ? "Artikelgruppen-Baum ausblenden" : "Artikelgruppen-Baum anzeigen");
411 button.setAttribute("title", open ? "Artikelgruppen-Baum ausblenden" : "Artikelgruppen-Baum anzeigen");
412 const icon = button.querySelector("i");
414 icon.classList.toggle("bi-diagram-3", !open);
415 icon.classList.toggle("bi-x-lg", !!open);
420 function toggleProductTree(shell) {
421 setProductTreeOpen(shell, shell.classList.contains("is-shop-tree-collapsed"));
424 function init(root) {
425 root = root || document;
426 root.querySelectorAll("[data-shop-tree-moveurl]").forEach(initProductTree);
427 if (root.matches && root.matches("[data-shop-tree-moveurl]")) {
428 initProductTree(root);
430 root.addEventListener("click", function (event) {
431 const stopPropagation = event.target.closest("[data-shop-stop-propagation]");
432 if (stopPropagation) {
433 event.stopPropagation();
435 const toggle = event.target.closest("[data-shop-tree-toggle]");
437 event.preventDefault();
438 toggleProductTree(findProductTreeShell(toggle, root));
441 const close = event.target.closest("[data-shop-tree-close]");
443 event.preventDefault();
444 setProductTreeOpen(findProductTreeShell(close, root), false);
447 const groupToggle = event.target.closest("[data-shop-tree-group-toggle]");
449 event.preventDefault();
450 event.stopPropagation();
451 const group = groupToggle.closest("[data-shop-tree-group-wrap]");
452 setGroupCollapsed(group, !group.classList.contains("is-collapsed"));
455 const searchClear = event.target.closest(".dbx-clear-btn");
456 if (searchClear && searchClear.closest(".dbx-shop-tree-search-wrap")) {
457 window.setTimeout(function () {
458 const tree = searchClear.closest("[data-shop-tree-panel]");
459 if (tree) applyTreeSearch(tree);
463 const button = event.target.closest(".dbx-shop-media-pick");
467 event.preventDefault();
468 const panel = button.closest(".dbx-shop-media-manager") || root;
469 openMediaBrowser(panel, button);
473 dbx.feature.register(LIB, {