3 * Prozess-UI: Fortschritt, Auto-Tick und Steuerung.
5(function (window, document) {
8 if (!window.dbx || !window.dbx.feature) {
9 console.error("[dbx][process] dbx core missing");
13 const dbx = window.dbx;
14 const timers = new WeakMap();
15 const retryTimers = new WeakMap();
17 dbx.process = dbx.process || {};
19 function readAttr(el, name, def = "") {
20 if (!el || !el.getAttribute) return def;
21 const value = el.getAttribute(name);
22 return value == null ? def : String(value).trim();
25 function bool(value, def = false) {
26 if (value === undefined || value === null || value === "") return def;
27 if (value === true || value === 1 || value === "1" || value === "on" || value === "true") return true;
28 if (value === false || value === 0 || value === "0" || value === "off" || value === "false") return false;
32 function clampPercent(value) {
33 const num = parseInt(value, 10);
34 if (Number.isNaN(num)) return 0;
35 return Math.max(0, Math.min(100, num));
38 function emit(name, data) {
39 if (dbx.event && typeof dbx.event.emit === "function") {
40 dbx.event.emit(name, data || {});
44 function status(root) {
45 return readAttr(root, "data-process-status", "running").toLowerCase();
48 function isWaiting(root) {
49 return ["paused", "canceled", "finished", "error"].includes(status(root));
52 function clearTimer(root) {
53 const timer = timers.get(root);
55 window.clearTimeout(timer);
58 const retryTimer = retryTimers.get(root);
60 window.clearTimeout(retryTimer);
61 retryTimers.delete(root);
65 function setBusy(root, busy) {
66 if (!root || !root.classList) return;
67 root.classList.toggle("is-loading", !!busy);
68 root.setAttribute("aria-busy", busy ? "true" : "false");
71 function setProgress(root, name, value) {
72 const percent = clampPercent(value);
73 const bar = root.querySelector("[data-process-bar='" + name + "']");
74 const label = root.querySelector("[data-process-percent='" + name + "']");
77 bar.style.width = percent + "%";
78 bar.setAttribute("aria-valuenow", String(percent));
79 if (bar.parentElement && bar.parentElement.getAttribute("role") === "progressbar") {
80 bar.parentElement.setAttribute("aria-valuenow", String(percent));
85 label.textContent = percent + "%";
89 function syncUi(root) {
90 setProgress(root, "overall", readAttr(root, "data-process-percent", "0"));
91 setProgress(root, "step", readAttr(root, "data-process-step-percent", "0"));
93 const currentStatus = status(root);
94 root.querySelectorAll("[data-process-visible]").forEach(el => {
95 const list = readAttr(el, "data-process-visible")
97 .map(item => item.trim().toLowerCase())
99 el.hidden = list.length ? !list.includes(currentStatus) : false;
103 function requestHtml(url) {
104 if (!url) return Promise.reject(new Error("missing_url"));
106 if (dbx.ajax && typeof dbx.ajax.request === "function") {
107 return dbx.ajax.request({
115 return Promise.reject(new Error("ajax.js nicht geladen."));
118 function findReplacement(root, html) {
119 const tpl = document.createElement("template");
120 tpl.innerHTML = String(html || "").trim();
122 if (!tpl.content.childNodes.length) return null;
127 const id = (window.CSS && CSS.escape)
128 ? CSS.escape(root.id)
129 : root.id.replace(/([^A-Za-z0-9_-])/g, "\\$1");
130 next = tpl.content.querySelector("#" + id);
134 next = tpl.content.querySelector("[data-dbx-process-root='1'], .dbx-process");
138 next = tpl.content.firstElementChild;
144 function replaceRoot(root, html) {
145 const next = findReplacement(root, html);
147 root.innerHTML = String(html || "");
152 root.replaceWith(next);
161 function loadIntoRoot(root, url, reason) {
165 emit("process:before", {
168 reason: reason || "tick"
171 return requestHtml(url)
173 root.__dbxProcessRetryCount = 0;
174 const next = replaceRoot(root, html);
175 emit("process:after", {
178 reason: reason || "tick"
183 dbx.warn("[process] request failed", err);
184 root.classList.add("has-error");
185 const msg = root.querySelector("[data-process-message]");
186 const running = status(root) === "running";
187 const retryCount = (parseInt(root.__dbxProcessRetryCount, 10) || 0) + 1;
188 root.__dbxProcessRetryCount = retryCount;
189 if (running && retryCount <= 3) {
190 if (msg) msg.textContent = "Antwort dauert laenger – Status wird erneut abgefragt.";
191 const retryTimer = window.setTimeout(function () {
192 retryTimers.delete(root);
193 loadIntoRoot(root, url, "retry");
194 }, Math.min(5000, 1000 * retryCount));
195 retryTimers.set(root, retryTimer);
197 msg.textContent = "Prozess-Anfrage fehlgeschlagen. Bitte erneut laden.";
201 .finally(() => setBusy(root, false));
204 function actionUrl(root, action) {
205 return readAttr(root, "data-process-" + action + "-url", "");
208 function ensureConfirm() {
209 if (dbx.confirm && typeof dbx.confirm.open === "function") {
210 return Promise.resolve(true);
213 if (typeof dbx.resolveFeature !== "function") {
214 return Promise.resolve(false);
217 return new Promise(resolve => {
218 dbx.resolveFeature("confirm", ok => {
219 resolve(ok === true && dbx.confirm && typeof dbx.confirm.open === "function");
224 function confirmAction(root, action) {
225 if (action !== "cancel" && action !== "restart") {
226 return Promise.resolve(true);
229 const title = action === "cancel" ? "Prozess abbrechen" : "Prozess neu starten";
230 const question = action === "cancel"
231 ? "Diesen Prozess abbrechen?"
232 : "Diesen Prozess neu starten?";
234 return ensureConfirm().then(ok => {
236 dbx.warn("[process] confirm.js konnte nicht geladen werden; Aktion abgebrochen.");
240 return dbx.confirm.open({
241 id: "process-" + action + "-" + Date.now(),
246 labelyes: "<i class='bi bi-check-lg'></i> Ja",
247 labelno: "<i class='bi bi-x-lg'></i> Nein"
248 }).then(result => result && result.action === "yes");
252 function runAction(root, action) {
253 action = String(action || "").toLowerCase();
254 if (!action || root.__dbxProcessBusy) return;
256 const url = actionUrl(root, action);
259 confirmAction(root, action).then(ok => {
262 root.__dbxProcessBusy = true;
263 loadIntoRoot(root, url, action).finally(() => {
264 root.__dbxProcessBusy = false;
269 function schedule(root, cfg) {
271 if (isWaiting(root)) return;
273 const auto = bool(readAttr(root, "data-process-autostart", cfg.autostart), true);
276 const url = readAttr(root, "data-process-next-url", cfg.url || "");
279 const interval = Math.max(250, parseInt(readAttr(root, "data-process-interval", cfg.interval || "800"), 10) || 800);
281 const timer = window.setTimeout(function () {
282 loadIntoRoot(root, url, "tick");
285 timers.set(root, timer);
288 dbx.process.init = function (root, cfg) {
291 root.setAttribute("data-dbx-process-root", "1");
294 if (root.__dbxProcessBound !== true) {
295 root.__dbxProcessBound = true;
296 root.addEventListener("click", function (e) {
297 const btn = e.target.closest("[data-process-action]");
298 if (!btn || !root.contains(btn)) return;
301 runAction(root, readAttr(btn, "data-process-action"));
305 schedule(root, cfg || {});
308 dbx.process.refresh = function (root) {
309 if (!root) return Promise.resolve(null);
310 const url = readAttr(root, "data-process-next-url", "");
311 return loadIntoRoot(root, url, "refresh");
314 dbx.feature.register("process", {
318 ["css", "design", "c-process.css"]
321 ["js", "lib", "ajax.js"]
325 dbx.process.init(el, cfg || {});