dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
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
16 dbx.process = dbx.process || {};
17
18 function readAttr(el, name, def = "") {
19 if (!el || !el.getAttribute) return def;
20 const value = el.getAttribute(name);
21 return value == null ? def : String(value).trim();
22 }
23
24 function bool(value, def = false) {
25 if (value === undefined || value === null || value === "") return def;
26 if (value === true || value === 1 || value === "1" || value === "on" || value === "true") return true;
27 if (value === false || value === 0 || value === "0" || value === "off" || value === "false") return false;
28 return def;
29 }
30
31 function clampPercent(value) {
32 const num = parseInt(value, 10);
33 if (Number.isNaN(num)) return 0;
34 return Math.max(0, Math.min(100, num));
35 }
36
37 function emit(name, data) {
38 if (dbx.event && typeof dbx.event.emit === "function") {
39 dbx.event.emit(name, data || {});
40 }
41 }
42
43 function status(root) {
44 return readAttr(root, "data-process-status", "running").toLowerCase();
45 }
46
47 function isWaiting(root) {
48 return ["paused", "canceled", "finished", "error"].includes(status(root));
49 }
50
51 function clearTimer(root) {
52 const timer = timers.get(root);
53 if (timer) {
54 window.clearTimeout(timer);
55 timers.delete(root);
56 }
57 }
58
59 function setBusy(root, busy) {
60 if (!root || !root.classList) return;
61 root.classList.toggle("is-loading", !!busy);
62 root.setAttribute("aria-busy", busy ? "true" : "false");
63 }
64
65 function setProgress(root, name, value) {
66 const percent = clampPercent(value);
67 const bar = root.querySelector("[data-process-bar='" + name + "']");
68 const label = root.querySelector("[data-process-percent='" + name + "']");
69
70 if (bar) {
71 bar.style.width = percent + "%";
72 bar.setAttribute("aria-valuenow", String(percent));
73 if (bar.parentElement && bar.parentElement.getAttribute("role") === "progressbar") {
74 bar.parentElement.setAttribute("aria-valuenow", String(percent));
75 }
76 }
77
78 if (label) {
79 label.textContent = percent + "%";
80 }
81 }
82
83 function syncUi(root) {
84 setProgress(root, "overall", readAttr(root, "data-process-percent", "0"));
85 setProgress(root, "step", readAttr(root, "data-process-step-percent", "0"));
86
87 const currentStatus = status(root);
88 root.querySelectorAll("[data-process-visible]").forEach(el => {
89 const list = readAttr(el, "data-process-visible")
90 .split(",")
91 .map(item => item.trim().toLowerCase())
92 .filter(Boolean);
93 el.hidden = list.length ? !list.includes(currentStatus) : false;
94 });
95 }
96
97 function requestHtml(url) {
98 if (!url) return Promise.reject(new Error("missing_url"));
99
100 if (dbx.ajax && typeof dbx.ajax.request === "function") {
101 return dbx.ajax.request({
102 url: url,
103 method: "GET",
104 mode: "html",
105 timeout: 30000
106 });
107 }
108
109 return Promise.reject(new Error("ajax.js nicht geladen."));
110 }
111
112 function findReplacement(root, html) {
113 const tpl = document.createElement("template");
114 tpl.innerHTML = String(html || "").trim();
115
116 if (!tpl.content.childNodes.length) return null;
117
118 let next = null;
119
120 if (root.id) {
121 const id = (window.CSS && CSS.escape)
122 ? CSS.escape(root.id)
123 : root.id.replace(/([^A-Za-z0-9_-])/g, "\\$1");
124 next = tpl.content.querySelector("#" + id);
125 }
126
127 if (!next) {
128 next = tpl.content.querySelector("[data-dbx-process-root='1'], .dbx-process");
129 }
130
131 if (!next) {
132 next = tpl.content.firstElementChild;
133 }
134
135 return next;
136 }
137
138 function replaceRoot(root, html) {
139 const next = findReplacement(root, html);
140 if (!next) {
141 root.innerHTML = String(html || "");
142 syncUi(root);
143 return root;
144 }
145
146 root.replaceWith(next);
147
148 if (dbx.scan) {
149 dbx.scan(next);
150 }
151
152 return next;
153 }
154
155 function loadIntoRoot(root, url, reason) {
156 clearTimer(root);
157 setBusy(root, true);
158
159 emit("process:before", {
160 root: root,
161 url: url,
162 reason: reason || "tick"
163 });
164
165 return requestHtml(url)
166 .then(html => {
167 const next = replaceRoot(root, html);
168 emit("process:after", {
169 root: next,
170 url: url,
171 reason: reason || "tick"
172 });
173 return next;
174 })
175 .catch(err => {
176 dbx.warn("[process] request failed", err);
177 root.classList.add("has-error");
178 const msg = root.querySelector("[data-process-message]");
179 if (msg) msg.textContent = "Prozess-Anfrage fehlgeschlagen.";
180 return root;
181 })
182 .finally(() => setBusy(root, false));
183 }
184
185 function actionUrl(root, action) {
186 return readAttr(root, "data-process-" + action + "-url", "");
187 }
188
189 function ensureConfirm() {
190 if (dbx.confirm && typeof dbx.confirm.open === "function") {
191 return Promise.resolve(true);
192 }
193
194 if (typeof dbx.resolveFeature !== "function") {
195 return Promise.resolve(false);
196 }
197
198 return new Promise(resolve => {
199 dbx.resolveFeature("confirm", ok => {
200 resolve(ok === true && dbx.confirm && typeof dbx.confirm.open === "function");
201 });
202 });
203 }
204
205 function confirmAction(root, action) {
206 if (action !== "cancel" && action !== "restart") {
207 return Promise.resolve(true);
208 }
209
210 const title = action === "cancel" ? "Prozess abbrechen" : "Prozess neu starten";
211 const question = action === "cancel"
212 ? "Diesen Prozess abbrechen?"
213 : "Diesen Prozess neu starten?";
214
215 return ensureConfirm().then(ok => {
216 if (!ok) {
217 return window.confirm(question);
218 }
219
220 return dbx.confirm.open({
221 id: "process-" + action + "-" + Date.now(),
222 root: root,
223 title: title,
224 question: question,
225 buttons: "yesno",
226 labelyes: "<i class='bi bi-check-lg'></i> Ja",
227 labelno: "<i class='bi bi-x-lg'></i> Nein"
228 }).then(result => result && result.action === "yes");
229 });
230 }
231
232 function runAction(root, action) {
233 action = String(action || "").toLowerCase();
234 if (!action || root.__dbxProcessBusy) return;
235
236 const url = actionUrl(root, action);
237 if (!url) return;
238
239 confirmAction(root, action).then(ok => {
240 if (!ok) return;
241
242 root.__dbxProcessBusy = true;
243 loadIntoRoot(root, url, action).finally(() => {
244 root.__dbxProcessBusy = false;
245 });
246 });
247 }
248
249 function schedule(root, cfg) {
250 clearTimer(root);
251 if (isWaiting(root)) return;
252
253 const auto = bool(readAttr(root, "data-process-autostart", cfg.autostart), true);
254 if (!auto) return;
255
256 const url = readAttr(root, "data-process-next-url", cfg.url || "");
257 if (!url) return;
258
259 const interval = Math.max(250, parseInt(readAttr(root, "data-process-interval", cfg.interval || "800"), 10) || 800);
260
261 const timer = window.setTimeout(function () {
262 loadIntoRoot(root, url, "tick");
263 }, interval);
264
265 timers.set(root, timer);
266 }
267
268 dbx.process.init = function (root, cfg) {
269 if (!root) return;
270
271 root.setAttribute("data-dbx-process-root", "1");
272 syncUi(root);
273
274 if (root.__dbxProcessBound !== true) {
275 root.__dbxProcessBound = true;
276 root.addEventListener("click", function (e) {
277 const btn = e.target.closest("[data-process-action]");
278 if (!btn || !root.contains(btn)) return;
279
280 e.preventDefault();
281 runAction(root, readAttr(btn, "data-process-action"));
282 });
283 }
284
285 schedule(root, cfg || {});
286 };
287
288 dbx.process.refresh = function (root) {
289 if (!root) return Promise.resolve(null);
290 const url = readAttr(root, "data-process-next-url", "");
291 return loadIntoRoot(root, url, "refresh");
292 };
293
294 dbx.feature.register("process", {
295 scope: "element",
296 priority: "mid",
297 css: [
298 ["css", "design", "c-process.css"]
299 ],
300 js: [
301 ["js", "lib", "ajax.js"]
302 ],
303 init(el, cfg) {
304 if (!el) return;
305 dbx.process.init(el, cfg || {});
306 },
307 destroy(el) {
308 clearTimer(el);
309 }
310 });
311
312})(window, document);