3 * Compact dbxKi briefing helpers.
5(function (window, document) {
8 if (!window.dbx || !window.dbx.feature) {
9 console.error("[dbx][kiBriefing] dbx core missing");
13 const dbx = window.dbx;
14 const LIB = "kiBriefing";
16 function qs(root, sel) {
17 return root ? root.querySelector(sel) : null;
20 function qsa(root, sel) {
21 return root ? Array.from(root.querySelectorAll(sel)) : [];
24 function closest(target, sel) {
25 const el = target && target.nodeType === 1 ? target : target && target.parentElement;
26 return el && el.closest ? el.closest(sel) : null;
29 function escapeHtml(value) {
30 const div = document.createElement("div");
31 div.textContent = value == null ? "" : String(value);
35 function apiUrl(url, params) {
36 const out = new URL(String(url || window.location.href), window.location.href);
37 Object.keys(params || {}).forEach(key => out.searchParams.set(key, params[key]));
38 out.searchParams.set("_", Date.now());
39 return out.toString();
42 function fetchJson(url) {
43 if (!dbx.ajax || typeof dbx.ajax.request !== "function") {
44 return Promise.reject(new Error("ajax.js nicht geladen."));
46 return dbx.ajax.request({ url: url, method: "GET", mode: "json", timeout: 20000 });
49 function state(root) {
50 root.__dbxKiBriefing = root.__dbxKiBriefing || {
57 return root.__dbxKiBriefing;
60 function setStatus(root, msg, type) {
61 const el = qs(root, "[data-ki-status]");
63 el.textContent = msg || "";
64 el.className = type === "error" ? "text-danger" : type === "success" ? "text-success" : "";
67 function currentLng(root, cfg) {
68 const select = qs(root, "[data-ki-lng]");
69 return String((select && select.value) || (cfg && cfg.lng) || "").toLowerCase();
72 function mode(root, cfg) {
73 return String((cfg && cfg.mode) || root.getAttribute("data-ki-mode") || "").toLowerCase();
76 function syncTargetLanguageModes(root, cfg) {
77 const lng = currentLng(root, cfg);
78 qsa(root, "[data-ki-target-lng]").forEach(label => {
79 const target = String(label.getAttribute("data-ki-target-lng") || "").toLowerCase();
80 const mode = qs(label, "[data-ki-target-mode]");
81 const isSource = target && target === lng;
82 label.classList.toggle("is-source", isSource);
83 if (mode) mode.textContent = isSource ? "Rechtschreibung/Grammatik" : "Uebersetzung";
87 function nodeMatches(node, search) {
88 if (!search) return true;
89 return String(node._title || "").toLowerCase().includes(search);
92 function childrenMatch(node, search) {
93 return Array.isArray(node._children) && node._children.some(child => nodeMatches(child, search) || childrenMatch(child, search));
96 function renderTree(root) {
97 const s = state(root);
98 const box = qs(root, "[data-ki-tree]");
100 const search = String((qs(root, "[data-ki-tree-search]") || {}).value || "").toLowerCase();
102 function rowHtml(node, type, active, collapsed, hasChildren) {
103 const id = Number(node._id || 0);
104 const toggle = type === "folder" && hasChildren
105 ? `<span class="dbx-cms-tree-toggle" data-ki-tree-toggle data-id="${id}" title="Ordner ein- oder ausklappen"><i class="bi ${collapsed ? "bi-chevron-right" : "bi-chevron-down"}"></i></span>`
106 : '<span class="dbx-cms-tree-toggle-spacer"></span>';
107 let html = String(node._row_html || "");
109 const icon = type === "folder" ? "bi-folder2-open" : "bi-file-earmark-text";
110 const title = type === "page"
111 ? `<span class="dbx-cms-page-id">(${id})</span> ${escapeHtml(node._title || "")}`
112 : escapeHtml(node._title || "");
113 html = `<div role="button" tabindex="0" class="dbx-cms-tree-row dbx-cms-tree-${type}" data-type="${type}" data-id="${id}" data-folder="${escapeHtml(node._parent || 0)}">${toggle}<span class="dbx-cms-tree-icons"><i class="bi ${icon}"></i></span><span class="dbx-cms-tree-label">${title}</span><span class="dbx-cms-tree-meta"></span></div>`;
115 html = html.replace(/\sdraggable="true"/g, "");
116 html = html.replace(/<button[\s\S]*?data-cms-folder-edit-btn[\s\S]*?<\/button>/g, "");
117 if (!html.includes("data-ki-tree-toggle") && !html.includes("dbx-cms-tree-toggle-spacer")) {
118 html = html.replace('<span class="dbx-cms-tree-icons">', toggle + '<span class="dbx-cms-tree-icons">');
121 if (active && !html.includes(" is-active")) {
122 html = html.replace("dbx-cms-tree-row", "dbx-cms-tree-row is-active");
124 if (type === "folder" && collapsed && !html.includes(" is-collapsed")) {
125 html = html.replace("dbx-cms-tree-row", "dbx-cms-tree-row is-collapsed");
130 function renderNode(node) {
131 if (!node || typeof node !== "object") return "";
132 const children = Array.isArray(node._children) ? node._children : [];
133 const type = node._type === "folder" ? "folder" : "page";
134 const visible = nodeMatches(node, search) || childrenMatch(node, search);
135 if (!visible) return "";
136 const id = Number(node._id || 0);
137 const collapsed = type === "folder" && !search && s.collapsed.has(id);
138 const active = mode(root, {}) === "create"
139 ? ((type === "page" && Number(s.selectedPage || 0) === id) || (type === "folder" && Number(s.selectedFolder || 0) === id && !Number(s.selectedPage || 0)))
140 : (type === "page" && Number(s.selectedPage || 0) === id);
141 let html = rowHtml(node, type, active, collapsed, children.length > 0);
142 if (children.length) {
143 html += `<div class="dbx-cms-tree-children"${collapsed ? " hidden" : ""}>`;
144 children.forEach(child => { html += renderNode(child); });
150 box.innerHTML = s.nodes.length
151 ? s.nodes.map(renderNode).join("")
152 : '<div class="dbx-cms-empty">Keine Content-Struktur vorhanden.</div>';
155 function loadTree(root, cfg) {
156 const box = qs(root, "[data-ki-tree]");
157 if (box) box.innerHTML = '<div class="dbx-cms-empty">Tree wird geladen...</div>';
158 return fetchJson(apiUrl(cfg.tree, { dbx_lng: currentLng(root, cfg) }))
160 if (!data || !data.ok) throw new Error("tree failed");
161 const s = state(root);
162 s.nodes = Array.isArray(data.nodes) ? data.nodes : [];
163 s.flat = Array.isArray(data.flat) ? data.flat : [];
167 dbx.error("[kiBriefing] tree load failed", err);
168 if (box) box.innerHTML = '<div class="dbx-cms-empty">Tree konnte nicht geladen werden.</div>';
172 function rowLabel(row) {
173 const label = qs(row, ".dbx-cms-tree-label");
174 return String(label ? label.textContent : "").replace(/\s+/g, " ").trim();
177 function nodeTitle(root, type, id) {
178 const s = state(root);
179 const found = (s.flat || []).find(node => String(node._type || "") === type && Number(node._id || 0) === Number(id || 0));
180 return found ? String(found._title || "").replace(/\s+/g, " ").trim() : "";
183 function updateCreateContext(root, folderId, folderLabel, pageId, pageLabel) {
184 const context = qs(root, "[data-ki-context]");
185 const folderHidden = qs(root, "[data-ki-folder-id]");
186 const pageHidden = qs(root, "[data-ki-page-id]");
187 const title = qs(root, ".dbx-ki-update-head h2");
188 folderId = Number(folderId || 0);
189 pageId = Number(pageId || 0);
190 folderLabel = folderLabel || (folderId > 0 ? ("Ordner #" + folderId) : "Noch kein Zielordner gewaehlt");
191 pageLabel = pageLabel || "";
192 if (folderHidden) folderHidden.value = folderId > 0 ? String(folderId) : "";
193 if (pageHidden) pageHidden.value = pageId > 0 ? String(pageId) : "";
194 if (title) title.textContent = pageId > 0
195 ? `Unter ${pageLabel} in ${folderLabel}`
196 : (folderId > 0 ? `${folderLabel} / am Ende` : "Zielposition waehlen");
198 context.innerHTML = `<div class="dbx-ki-page-context dbx-ki-create-placement">
199 <div class="dbx-ki-page-context-head"><strong>Zielposition</strong><span>neu</span></div>
200 <dl class="dbx-ki-page-context-meta">
201 <div><dt>Ordner</dt><dd>${escapeHtml(folderLabel)} <span class="text-muted">#${folderId || "-"}</span></dd></div>
202 <div><dt>Unter</dt><dd>${pageId > 0 ? escapeHtml(pageLabel) : "Keine Seite als Sortieranker"} <span class="text-muted">#${pageId || "-"}</span></dd></div>
203 <div><dt>Sorter</dt><dd>${pageId > 0 ? "direkt unter dieser Seite" : "automatisch am Ende"}</dd></div>
209 function selectCreatePlacement(root, folderId, folderLabel, pageId, pageLabel) {
210 const s = state(root);
211 s.selectedFolder = Number(folderId || 0);
212 s.selectedPage = Number(pageId || 0);
213 updateCreateContext(root, s.selectedFolder, folderLabel, s.selectedPage, pageLabel);
215 setStatus(root, s.selectedFolder > 0 ? "Zielposition gewaehlt." : "Bitte Zielordner waehlen.", s.selectedFolder > 0 ? "success" : "error");
218 function selectPage(root, cfg, pageId) {
219 pageId = Number(pageId || 0);
220 if (!pageId) return Promise.resolve();
221 const s = state(root);
222 s.selectedPage = pageId;
223 const hidden = qs(root, "[data-ki-page-id]");
224 if (hidden) hidden.value = String(pageId);
226 setStatus(root, "Seite wird geladen...", "info");
227 const preview = qs(root, "[data-ki-preview]");
228 if (preview) preview.innerHTML = '<div class="dbx-cms-empty">Seite wird geladen...</div>';
230 return fetchJson(apiUrl(cfg.preview, { id: pageId, dbx_lng: currentLng(root, cfg) }))
232 if (!data || !data.ok) throw new Error((data && data.error) || "preview failed");
233 const title = data.title || ("Seite #" + pageId);
234 const titleEl = qs(root, "[data-ki-selected-title]");
235 const headTitle = qs(root, ".dbx-ki-update-head h2");
236 if (titleEl) titleEl.textContent = title;
237 if (headTitle) headTitle.textContent = title;
239 preview.innerHTML = data.html || '<div class="dbx-cms-empty">Keine Vorschau vorhanden.</div>';
240 if (dbx.rescan) dbx.rescan(preview);
242 const context = qs(root, "[data-ki-context]");
243 if (context) context.innerHTML = data.context_html || "";
244 setStatus(root, "Gerenderte dbxContent-Vorschau", "success");
247 dbx.error("[kiBriefing] preview load failed", err);
248 if (preview) preview.innerHTML = '<div class="dbx-cms-empty">Seite konnte nicht geladen werden.</div>';
249 setStatus(root, "Seite konnte nicht geladen werden.", "error");
253 function bind(root, cfg) {
254 if (cfg && cfg.mode) root.setAttribute("data-ki-mode", cfg.mode);
255 const isCreate = mode(root, cfg) === "create";
256 const initial = Number(cfg.pageid || 0);
257 if (initial > 0) state(root).selectedPage = initial;
258 const initialFolder = Number(cfg.folderid || 0);
259 if (initialFolder > 0) state(root).selectedFolder = initialFolder;
261 const search = qs(root, "[data-ki-tree-search]");
262 if (search) search.addEventListener("input", () => renderTree(root));
264 const lng = qs(root, "[data-ki-lng]");
266 lng.addEventListener("change", () => {
267 const selected = Number((qs(root, "[data-ki-page-id]") || {}).value || 0);
268 syncTargetLanguageModes(root, cfg);
269 loadTree(root, cfg).then(() => {
270 if (!isCreate && selected > 0) selectPage(root, cfg, selected);
275 root.addEventListener("click", e => {
276 const toggle = closest(e.target, "[data-ki-tree-toggle]");
277 if (toggle && root.contains(toggle)) {
280 const id = Number(toggle.getAttribute("data-id") || 0);
281 const s = state(root);
282 if (s.collapsed.has(id)) s.collapsed.delete(id);
283 else s.collapsed.add(id);
288 const row = closest(e.target, ".dbx-cms-tree-row");
289 if (!row || !root.contains(row)) return;
290 const type = row.getAttribute("data-type");
291 const id = Number(row.getAttribute("data-id") || 0);
292 if (type === "folder") {
294 selectCreatePlacement(root, id, rowLabel(row), 0, "");
297 const s = state(root);
298 if (s.collapsed.has(id)) s.collapsed.delete(id);
299 else s.collapsed.add(id);
303 if (type === "page") {
305 const folderId = Number(row.getAttribute("data-folder") || 0);
306 selectCreatePlacement(root, folderId, nodeTitle(root, "folder", folderId) || ("Ordner #" + folderId), id, rowLabel(row));
309 selectPage(root, cfg, id);
313 root.addEventListener("keydown", e => {
314 if (e.key !== "Enter" && e.key !== " ") return;
315 const row = closest(e.target, ".dbx-cms-tree-row");
316 if (!row || !root.contains(row)) return;
321 const form = qs(root, "[data-ki-form]");
323 form.addEventListener("submit", e => {
324 const folderField = qs(root, "[data-ki-folder-id]");
326 const folderId = Number(folderField.value || 0);
329 setStatus(root, "Bitte zuerst einen Zielordner im Content Tree waehlen.", "error");
333 const pageId = Number((qs(root, "[data-ki-page-id]") || {}).value || 0);
336 setStatus(root, "Bitte zuerst eine Seite im Content Tree waehlen.", "error");
337 const tree = qs(root, "[data-ki-tree]");
338 if (tree) tree.focus();
341 const targets = qsa(root, "input[name='target_lngs[]']");
342 if (targets.length && !targets.some(input => input.checked)) {
344 setStatus(root, "Bitte mindestens eine Ziel- oder Korrektursprache waehlen.", "error");
349 syncTargetLanguageModes(root, cfg);
350 loadTree(root, cfg).then(() => {
351 if (isCreate && initialFolder > 0) {
352 updateCreateContext(root, initialFolder, "Ordner #" + initialFolder, initial, initial > 0 ? ("Seite #" + initial) : "");
357 dbx.feature.register(LIB, {
361 ["css", "design", "c-cms.css"],
362 ["css", "design", "c-form.css"]
365 ["js", "lib", "ajax.js"]
368 if (!el || el.__dbxKiBriefingReady) return;
369 el.__dbxKiBriefingReady = true;
373 (root || document).querySelectorAll(".dbx-ki-update[data-dbx]").forEach(el => {
374 if (el.__dbxKiBriefingReady) return;
375 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
376 const cfg = cfgList.find(item => item.lib === LIB) || {};