dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
shopAdmin.js
Go to the documentation of this file.
1/*!
2 * @file shopAdmin.js
3 * Shop-Administration: CMS-Medienbrowser fuer Artikelbilder.
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 = "shopAdmin";
14 let reloadTimer = null;
15
16 function cfg(root) {
17 root = root || document.querySelector(".dbx-shop-media-manager") || document;
18 return {
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") || ""
33 };
34 }
35
36 function loadSharedMediaBrowserAssets() {
37 return new Promise(function (resolve) {
38 if (!dbx.load) {
39 resolve(false);
40 return;
41 }
42 dbx.load([
43 ["css", "design", "c-cms.css"],
44 ["js", "lib", "ajax.js"],
45 ["js", "lib", "openWin.js"],
46 ["js", "lib", "cms.js"]
47 ], function () {
48 resolve(!!(dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function"));
49 });
50 });
51 }
52
53 function ensureCmsMediaBrowser() {
54 if (dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function") {
55 return loadSharedMediaBrowserAssets();
56 }
57 return loadSharedMediaBrowserAssets();
58 }
59
60 function ensureAjax() {
61 return new Promise(function (resolve) {
62 if (dbx.ajax && typeof dbx.ajax.request === "function") {
63 resolve(true);
64 return;
65 }
66 if (!dbx.load) {
67 resolve(false);
68 return;
69 }
70 dbx.load([["js", "lib", "ajax.js"]], function () {
71 resolve(!!(dbx.ajax && typeof dbx.ajax.request === "function"));
72 });
73 });
74 }
75
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."));
80 }
81 return dbx.ajax.request({
82 url: url,
83 method: options.method || "GET",
84 mode: "json",
85 body: typeof options.body !== "undefined" ? options.body : null,
86 headers: options.headers || {},
87 timeout: Number(options.timeout || 20000)
88 });
89 }
90
91 function scheduleReload() {
92 if (reloadTimer) {
93 window.clearTimeout(reloadTimer);
94 }
95 reloadTimer = window.setTimeout(function () {
96 window.location.reload();
97 }, 900);
98 }
99
100 function assignMedia(root, row) {
101 const settings = cfg(root);
102 if (!settings.assignurl) {
103 return Promise.reject(new Error("Shop-Zuordnung ist nicht konfiguriert."));
104 }
105 const product = root.querySelector("[data-shop-product-select]");
106 const group = root.querySelector("[data-shop-group-select]");
107 const sorter = root.querySelector("[data-shop-sorter]");
108 const primary = root.querySelector("[data-shop-primary]");
109 const productId = product ? parseInt(product.value || "0", 10) || 0 : 0;
110 const groupId = group ? parseInt(group.value || "0", 10) || 0 : 0;
111 if (productId <= 0 && groupId <= 0) {
112 window.alert("Bitte zuerst einen Artikel oder eine Artikelgruppe auswaehlen.");
113 return Promise.resolve(false);
114 }
115 const payload = {
116 product_id: productId,
117 group_id: groupId,
118 media_id: parseInt(row.id || row.media_id || "0", 10) || 0,
119 title: row.title || row.label || row.file_name || "",
120 alt: row.alt || row.title || row.label || "",
121 sorter: sorter ? (parseInt(sorter.value || "100", 10) || 100) : 100,
122 is_primary: primary && primary.checked ? 1 : 0
123 };
124 if (payload.media_id <= 0) {
125 window.alert("Das ausgewaehlte Medium hat keine gueltige ID.");
126 return Promise.resolve(false);
127 }
128 return fetchJson(settings.assignurl, {
129 method: "POST",
130 headers: { "Content-Type": "application/json" },
131 body: JSON.stringify(payload)
132 }).then(function (data) {
133 if (!data || !data.ok) {
134 window.alert((data && data.msg) ? data.msg : "Bild konnte nicht zugeordnet werden.");
135 return false;
136 }
137 scheduleReload();
138 return true;
139 }).catch(function (err) {
140 window.alert(err && err.message ? err.message : "Bild konnte nicht zugeordnet werden.");
141 return false;
142 });
143 }
144
145 function openMediaBrowser(root, button) {
146 ensureCmsMediaBrowser().then(function (ok) {
147 if (!ok || !dbx.cmsMediaBrowser) {
148 window.alert("CMS-Medienbrowser konnte nicht geladen werden.");
149 return;
150 }
151 const settings = cfg(root);
152 dbx.cmsMediaBrowser.open(root, settings, {
153 mode: "pick",
154 mediaFolder: button.getAttribute("data-shop-media-folder") || settings.uploadmediafolder || "all",
155 afterAssign: function (row) {
156 return assignMedia(root, row);
157 }
158 });
159 });
160 }
161
162 function readDragPayload(event) {
163 try {
164 return JSON.parse(event.dataTransfer.getData("application/json") || "{}");
165 } catch (err) {
166 return {};
167 }
168 }
169
170 function moveTreeNode(root, drop, payload) {
171 const url = root.getAttribute("data-shop-tree-moveurl") || "";
172 const targetGroupId = parseInt(drop.getAttribute("data-shop-tree-drop") || "0", 10) || 0;
173 if (!url || !payload || !payload.type) {
174 return;
175 }
176 if (payload.type === "product" && targetGroupId <= 0) {
177 window.alert("Artikel koennen nur auf eine Artikelgruppe gezogen werden.");
178 return;
179 }
180 const body = {
181 type: payload.type,
182 target_group_id: targetGroupId
183 };
184 if (payload.type === "product") {
185 body.product_id = parseInt(payload.id || "0", 10) || 0;
186 } else if (payload.type === "group") {
187 body.group_id = parseInt(payload.id || "0", 10) || 0;
188 }
189 ensureAjax().then(function (ok) {
190 if (!ok) {
191 window.alert("AJAX konnte nicht geladen werden.");
192 return;
193 }
194 return fetchJson(url, {
195 method: "POST",
196 headers: { "Content-Type": "application/json" },
197 body: JSON.stringify(body)
198 }).then(function (data) {
199 if (!data || !data.ok) {
200 window.alert((data && data.msg) ? data.msg : "Verschieben nicht moeglich.");
201 return;
202 }
203 scheduleReload();
204 });
205 }).catch(function (err) {
206 window.alert(err && err.message ? err.message : "Verschieben nicht moeglich.");
207 });
208 }
209
210 function normalizeSearch(value) {
211 return String(value || "")
212 .toLowerCase()
213 .normalize("NFD")
214 .replace(/[\u0300-\u036f]/g, "")
215 .replace(/\s+/g, " ")
216 .trim();
217 }
218
219 function setGroupCollapsed(group, collapsed) {
220 if (!group) return;
221 group.classList.toggle("is-collapsed", !!collapsed);
222 const button = group.querySelector(":scope > .dbx-shop-tree-group-head [data-shop-tree-group-toggle]");
223 if (button) {
224 button.setAttribute("aria-expanded", collapsed ? "false" : "true");
225 const icon = button.querySelector("i");
226 if (icon) {
227 icon.classList.toggle("bi-chevron-right", !!collapsed);
228 icon.classList.toggle("bi-chevron-down", !collapsed);
229 }
230 }
231 }
232
233 function clearTreeSearchState(root) {
234 root.querySelectorAll(".is-search-match,.is-search-path").forEach(function (node) {
235 node.classList.remove("is-search-match", "is-search-path");
236 });
237 root.querySelectorAll("[data-shop-tree-hidden]").forEach(function (node) {
238 node.hidden = false;
239 node.removeAttribute("data-shop-tree-hidden");
240 });
241 }
242
243 function applyTreeSearch(root) {
244 const input = root.querySelector("[data-shop-tree-search]");
245 const term = normalizeSearch(input ? input.value : "");
246 root.classList.toggle("is-searching", term !== "");
247 clearTreeSearchState(root);
248 if (term === "") {
249 return;
250 }
251
252 function nodeText(node) {
253 return normalizeSearch(node.getAttribute("data-shop-tree-search-text") || node.textContent || "");
254 }
255
256 function filterProduct(product) {
257 const match = nodeText(product).includes(term);
258 product.hidden = !match;
259 if (!match) {
260 product.setAttribute("data-shop-tree-hidden", "1");
261 } else {
262 product.classList.add("is-search-match");
263 }
264 return match;
265 }
266
267 function filterProductList(list) {
268 let any = false;
269 list.querySelectorAll(":scope > .dbx-shop-tree-products > .dbx-shop-tree-product").forEach(function (product) {
270 any = filterProduct(product) || any;
271 });
272 list.hidden = !any;
273 if (!any) {
274 list.setAttribute("data-shop-tree-hidden", "1");
275 }
276 return any;
277 }
278
279 function filterGroup(group) {
280 const ownMatch = nodeText(group).includes(term);
281 let childMatch = false;
282 const children = group.querySelector(":scope > .dbx-shop-tree-children");
283 if (children) {
284 Array.prototype.forEach.call(children.children, function (child) {
285 if (child.matches(".dbx-shop-tree-group")) {
286 childMatch = filterGroup(child) || childMatch;
287 } else if (child.matches(".dbx-shop-tree-product-list")) {
288 childMatch = filterProductList(child) || childMatch;
289 }
290 });
291 }
292 const visible = ownMatch || childMatch;
293 group.hidden = !visible;
294 if (!visible) {
295 group.setAttribute("data-shop-tree-hidden", "1");
296 } else if (ownMatch) {
297 group.classList.add("is-search-match");
298 if (children) {
299 children.querySelectorAll("[data-shop-tree-hidden]").forEach(function (node) {
300 node.hidden = false;
301 node.removeAttribute("data-shop-tree-hidden");
302 });
303 }
304 } else {
305 group.classList.add("is-search-path");
306 }
307 return visible;
308 }
309
310 root.querySelectorAll(".dbx-shop-tree-list > .dbx-shop-tree-group").forEach(filterGroup);
311 const ungrouped = root.querySelector(".dbx-shop-tree-ungrouped");
312 if (ungrouped) {
313 let anyUngrouped = false;
314 ungrouped.querySelectorAll(".dbx-shop-tree-product").forEach(function (product) {
315 anyUngrouped = filterProduct(product) || anyUngrouped;
316 });
317 ungrouped.hidden = !anyUngrouped;
318 if (!anyUngrouped) {
319 ungrouped.setAttribute("data-shop-tree-hidden", "1");
320 }
321 }
322 }
323
324 function initProductTree(root) {
325 if (root.__dbxShopProductTreeBound) return;
326 root.__dbxShopProductTreeBound = true;
327
328 root.querySelectorAll("[data-shop-tree-node]").forEach(function (node) {
329 node.setAttribute("draggable", "true");
330 });
331
332 root.addEventListener("dragstart", function (event) {
333 if (event.target.closest("button,a,input,select,textarea")) return;
334 const node = event.target.closest("[data-shop-tree-node]");
335 if (!node || !root.contains(node)) return;
336 const type = node.getAttribute("data-shop-tree-node");
337 const id = type === "product"
338 ? node.getAttribute("data-shop-tree-product")
339 : node.getAttribute("data-shop-tree-group");
340 event.dataTransfer.effectAllowed = "move";
341 event.dataTransfer.setData("application/json", JSON.stringify({ type: type, id: id }));
342 event.dataTransfer.setData("text/plain", type + ":" + id);
343 node.classList.add("is-dragging");
344 });
345
346 root.addEventListener("dragend", function () {
347 root.querySelectorAll(".is-dragging,.is-drop-target").forEach(function (el) {
348 el.classList.remove("is-dragging", "is-drop-target");
349 });
350 });
351
352 root.addEventListener("dragover", function (event) {
353 const drop = event.target.closest("[data-shop-tree-drop]");
354 if (!drop || !root.contains(drop)) return;
355 event.preventDefault();
356 event.dataTransfer.dropEffect = "move";
357 root.querySelectorAll(".is-drop-target").forEach(function (el) {
358 if (el !== drop) el.classList.remove("is-drop-target");
359 });
360 drop.classList.add("is-drop-target");
361 });
362
363 root.addEventListener("dragleave", function (event) {
364 const drop = event.target.closest("[data-shop-tree-drop]");
365 if (drop) drop.classList.remove("is-drop-target");
366 });
367
368 root.addEventListener("drop", function (event) {
369 const drop = event.target.closest("[data-shop-tree-drop]");
370 if (!drop || !root.contains(drop)) return;
371 event.preventDefault();
372 drop.classList.remove("is-drop-target");
373 moveTreeNode(root, drop, readDragPayload(event));
374 });
375
376 const search = root.querySelector("[data-shop-tree-search]");
377 if (search) {
378 search.addEventListener("input", function () {
379 applyTreeSearch(root);
380 });
381 }
382 }
383
384 function findProductTreeShell(element, fallbackRoot) {
385 if (element) {
386 const direct = element.closest("[data-shop-tree-shell]");
387 if (direct) return direct;
388 const panel = element.closest(".dbx-panel");
389 if (panel) {
390 const shell = panel.querySelector("[data-shop-tree-shell]");
391 if (shell) return shell;
392 }
393 }
394 if (fallbackRoot && fallbackRoot.querySelector) {
395 return fallbackRoot.querySelector("[data-shop-tree-shell]") || null;
396 }
397 return null;
398 }
399
400 function setProductTreeOpen(shell, open) {
401 if (!shell) return;
402 const container = shell.closest(".dbx-panel") || shell;
403 shell.classList.toggle("is-shop-tree-collapsed", !open);
404 shell.classList.toggle("is-shop-tree-open", !!open);
405 container.querySelectorAll("[data-shop-tree-toggle]").forEach(function (button) {
406 button.setAttribute("aria-expanded", open ? "true" : "false");
407 button.setAttribute("aria-label", open ? "Artikelgruppen-Baum ausblenden" : "Artikelgruppen-Baum anzeigen");
408 button.setAttribute("title", open ? "Artikelgruppen-Baum ausblenden" : "Artikelgruppen-Baum anzeigen");
409 const icon = button.querySelector("i");
410 if (icon) {
411 icon.classList.toggle("bi-diagram-3", !open);
412 icon.classList.toggle("bi-x-lg", !!open);
413 }
414 });
415 }
416
417 function toggleProductTree(shell) {
418 setProductTreeOpen(shell, shell.classList.contains("is-shop-tree-collapsed"));
419 }
420
421 function init(root) {
422 root = root || document;
423 root.querySelectorAll("[data-shop-tree-moveurl]").forEach(initProductTree);
424 if (root.matches && root.matches("[data-shop-tree-moveurl]")) {
425 initProductTree(root);
426 }
427 root.addEventListener("click", function (event) {
428 const toggle = event.target.closest("[data-shop-tree-toggle]");
429 if (toggle) {
430 event.preventDefault();
431 toggleProductTree(findProductTreeShell(toggle, root));
432 return;
433 }
434 const close = event.target.closest("[data-shop-tree-close]");
435 if (close) {
436 event.preventDefault();
437 setProductTreeOpen(findProductTreeShell(close, root), false);
438 return;
439 }
440 const groupToggle = event.target.closest("[data-shop-tree-group-toggle]");
441 if (groupToggle) {
442 event.preventDefault();
443 event.stopPropagation();
444 const group = groupToggle.closest("[data-shop-tree-group-wrap]");
445 setGroupCollapsed(group, !group.classList.contains("is-collapsed"));
446 return;
447 }
448 const searchClear = event.target.closest(".dbx-clear-btn");
449 if (searchClear && searchClear.closest(".dbx-shop-tree-search-wrap")) {
450 window.setTimeout(function () {
451 const tree = searchClear.closest("[data-shop-tree-panel]");
452 if (tree) applyTreeSearch(tree);
453 }, 0);
454 return;
455 }
456 const button = event.target.closest(".dbx-shop-media-pick");
457 if (!button) {
458 return;
459 }
460 event.preventDefault();
461 const panel = button.closest(".dbx-shop-media-manager") || root;
462 openMediaBrowser(panel, button);
463 });
464 }
465
466 dbx.feature.register(LIB, {
467 scope: "element",
468 js: [],
469 init
470 });
471})(window, document);