dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
process.js
Go to the documentation of this file.
1/*!
2 * dbxapp process.js
3 * Prozess-UI: Fortschritt, Auto-Tick und Steuerung.
4 */
5(function (window, document) {
6 "use strict";
7
8 if (!window.dbx || !window.dbx.feature) {
9 console.error("[dbx][process] dbx core missing");
10 return;
11 }
12
13 const dbx = window.dbx;
14 const timers = new WeakMap();
15 const retryTimers = new WeakMap();
16
17 dbx.process = dbx.process || {};
18
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();
23 }
24
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;
29 return def;
30 }
31
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));
36 }
37
38 function emit(name, data) {
39 if (dbx.event && typeof dbx.event.emit === "function") {
40 dbx.event.emit(name, data || {});
41 }
42 }
43
44 function status(root) {
45 return readAttr(root, "data-process-status", "running").toLowerCase();
46 }
47
48 function isWaiting(root) {
49 return ["paused", "canceled", "finished", "error"].includes(status(root));
50 }
51
52 function clearTimer(root) {
53 const timer = timers.get(root);
54 if (timer) {
55 window.clearTimeout(timer);
56 timers.delete(root);
57 }
58 const retryTimer = retryTimers.get(root);
59 if (retryTimer) {
60 window.clearTimeout(retryTimer);
61 retryTimers.delete(root);
62 }
63 }
64
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");
69 }
70
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 + "']");
75
76 if (bar) {
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));
81 }
82 }
83
84 if (label) {
85 label.textContent = percent + "%";
86 }
87 }
88
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"));
92
93 const currentStatus = status(root);
94 root.querySelectorAll("[data-process-visible]").forEach(el => {
95 const list = readAttr(el, "data-process-visible")
96 .split(",")
97 .map(item => item.trim().toLowerCase())
98 .filter(Boolean);
99 el.hidden = list.length ? !list.includes(currentStatus) : false;
100 });
101 }
102
103 function requestHtml(url) {
104 if (!url) return Promise.reject(new Error("missing_url"));
105
106 if (dbx.ajax && typeof dbx.ajax.request === "function") {
107 return dbx.ajax.request({
108 url: url,
109 method: "GET",
110 mode: "html",
111 timeout: 45000
112 });
113 }
114
115 return Promise.reject(new Error("ajax.js nicht geladen."));
116 }
117
118 function findReplacement(root, html) {
119 const tpl = document.createElement("template");
120 tpl.innerHTML = String(html || "").trim();
121
122 if (!tpl.content.childNodes.length) return null;
123
124 let next = null;
125
126 if (root.id) {
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);
131 }
132
133 if (!next) {
134 next = tpl.content.querySelector("[data-dbx-process-root='1'], .dbx-process");
135 }
136
137 if (!next) {
138 next = tpl.content.firstElementChild;
139 }
140
141 return next;
142 }
143
144 function replaceRoot(root, html) {
145 const next = findReplacement(root, html);
146 if (!next) {
147 root.innerHTML = String(html || "");
148 syncUi(root);
149 return root;
150 }
151
152 root.replaceWith(next);
153
154 if (dbx.scan) {
155 dbx.scan(next);
156 }
157
158 return next;
159 }
160
161 function loadIntoRoot(root, url, reason) {
162 clearTimer(root);
163 setBusy(root, true);
164
165 emit("process:before", {
166 root: root,
167 url: url,
168 reason: reason || "tick"
169 });
170
171 return requestHtml(url)
172 .then(html => {
173 root.__dbxProcessRetryCount = 0;
174 const next = replaceRoot(root, html);
175 emit("process:after", {
176 root: next,
177 url: url,
178 reason: reason || "tick"
179 });
180 return next;
181 })
182 .catch(err => {
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);
196 } else if (msg) {
197 msg.textContent = "Prozess-Anfrage fehlgeschlagen. Bitte erneut laden.";
198 }
199 return root;
200 })
201 .finally(() => setBusy(root, false));
202 }
203
204 function actionUrl(root, action) {
205 return readAttr(root, "data-process-" + action + "-url", "");
206 }
207
208 function ensureConfirm() {
209 if (dbx.confirm && typeof dbx.confirm.open === "function") {
210 return Promise.resolve(true);
211 }
212
213 if (typeof dbx.resolveFeature !== "function") {
214 return Promise.resolve(false);
215 }
216
217 return new Promise(resolve => {
218 dbx.resolveFeature("confirm", ok => {
219 resolve(ok === true && dbx.confirm && typeof dbx.confirm.open === "function");
220 });
221 });
222 }
223
224 function confirmAction(root, action) {
225 if (action !== "cancel" && action !== "restart") {
226 return Promise.resolve(true);
227 }
228
229 const title = action === "cancel" ? "Prozess abbrechen" : "Prozess neu starten";
230 const question = action === "cancel"
231 ? "Diesen Prozess abbrechen?"
232 : "Diesen Prozess neu starten?";
233
234 return ensureConfirm().then(ok => {
235 if (!ok) {
236 dbx.warn("[process] confirm.js konnte nicht geladen werden; Aktion abgebrochen.");
237 return false;
238 }
239
240 return dbx.confirm.open({
241 id: "process-" + action + "-" + Date.now(),
242 root: root,
243 title: title,
244 question: question,
245 buttons: "yesno",
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");
249 });
250 }
251
252 function runAction(root, action) {
253 action = String(action || "").toLowerCase();
254 if (!action || root.__dbxProcessBusy) return;
255
256 const url = actionUrl(root, action);
257 if (!url) return;
258
259 confirmAction(root, action).then(ok => {
260 if (!ok) return;
261
262 root.__dbxProcessBusy = true;
263 loadIntoRoot(root, url, action).finally(() => {
264 root.__dbxProcessBusy = false;
265 });
266 });
267 }
268
269 function schedule(root, cfg) {
270 clearTimer(root);
271 if (isWaiting(root)) return;
272
273 const auto = bool(readAttr(root, "data-process-autostart", cfg.autostart), true);
274 if (!auto) return;
275
276 const url = readAttr(root, "data-process-next-url", cfg.url || "");
277 if (!url) return;
278
279 const interval = Math.max(250, parseInt(readAttr(root, "data-process-interval", cfg.interval || "800"), 10) || 800);
280
281 const timer = window.setTimeout(function () {
282 loadIntoRoot(root, url, "tick");
283 }, interval);
284
285 timers.set(root, timer);
286 }
287
288 dbx.process.init = function (root, cfg) {
289 if (!root) return;
290
291 root.setAttribute("data-dbx-process-root", "1");
292 syncUi(root);
293
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;
299
300 e.preventDefault();
301 runAction(root, readAttr(btn, "data-process-action"));
302 });
303 }
304
305 schedule(root, cfg || {});
306 };
307
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");
312 };
313
314 dbx.feature.register("process", {
315 scope: "element",
316 priority: "mid",
317 css: [
318 ["css", "design", "c-process.css"]
319 ],
320 js: [
321 ["js", "lib", "ajax.js"]
322 ],
323 init(el, cfg) {
324 if (!el) return;
325 dbx.process.init(el, cfg || {});
326 },
327 destroy(el) {
328 clearTimer(el);
329 }
330 });
331
332})(window, document);