initial files

This commit is contained in:
2026-04-22 11:55:23 +02:00
commit 92444ff38c
85 changed files with 16324 additions and 0 deletions

361
public/javascript/JSON.js Normal file
View File

@@ -0,0 +1,361 @@
/*
createJsonTree({
container: domElement,
data: jsonData,
onChange: (data) => { }, // optional: callback on change
onSave: (data) => { } // optional: if set, a save button will be added
});
*/function createJsonTree({
container,
data,
onChange = () => {},
onSave = null
}) {
container.innerHTML = "";
container.classList.add("json-tree-root");
const history = [];
const redoStack = [];
let lastSnapshot = clone(data);
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function diff(oldObj, newObj) {
const changes = {};
function walk(o, n, path = "") {
const keys = new Set([
...Object.keys(o || {}),
...Object.keys(n || {})
]);
for (const key of keys) {
const oldVal = o?.[key];
const newVal = n?.[key];
const currentPath = path ? `${path}.${key}` : key;
if (
typeof oldVal === "object" &&
oldVal !== null &&
typeof newVal === "object" &&
newVal !== null &&
!Array.isArray(oldVal)
) {
walk(oldVal, newVal, currentPath);
continue;
}
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
changes[currentPath] = newVal;
}
}
}
walk(oldObj, newObj);
return changes;
}
function pushHistory() {
history.push(clone(data));
redoStack.length = 0;
}
function undo() {
if (!history.length) return;
redoStack.push(clone(data));
data = history.pop();
render();
}
function redo() {
if (!redoStack.length) return;
history.push(clone(data));
data = redoStack.pop();
render();
}
function save() {
if (onSave) onSave(data);
}
function autoResize(input) {
input.style.width = "10px";
input.style.width = input.scrollWidth + "px";
}
function remove(key, parent) {
if (key === null || parent == null) return;
if (!confirm("Wirklich löschen?")) return;
pushHistory();
if (Array.isArray(parent)) {
parent.splice(key, 1);
} else {
delete parent[key];
}
onChange(data);
render();
}
function render() {
container.innerHTML = "";
const controls = document.createElement("div");
controls.className = "json-controls";
const undoBtn = document.createElement("button");
undoBtn.className = "monolyth";
undoBtn.textContent = "Undo";
undoBtn.onclick = undo;
const redoBtn = document.createElement("button");
redoBtn.className = "monolyth";
redoBtn.textContent = "Redo";
redoBtn.onclick = redo;
controls.appendChild(undoBtn);
controls.appendChild(redoBtn);
if (onSave) {
const saveBtn = document.createElement("button");
saveBtn.className = "monolyth";
saveBtn.textContent = "Save";
saveBtn.onclick = save;
controls.appendChild(saveBtn);
}
container.appendChild(controls);
container.appendChild(renderNode(data, null, null, "root", 0));
}
function renderNode(value, key, parent, path, level) {
const wrapper = document.createElement("div");
wrapper.className = "json-line";
wrapper.style.marginLeft = `${level * 18}px`;
const keySpan = document.createElement("span");
keySpan.className = "json-key";
if (key !== null) {
keySpan.textContent = `"${key}": `;
}
const removeBtn = document.createElement("span");
removeBtn.className = "json-remove";
removeBtn.textContent = " [x]";
removeBtn.onclick = (evt) => {
evt.stopPropagation();
remove(key, parent);
};
// OBJECT / ARRAY
if (typeof value === "object" && value !== null) {
const isArray = Array.isArray(value);
const header = document.createElement("div");
header.className = "json-header";
const toggle = document.createElement("span");
toggle.className = "json-toggle";
toggle.textContent = isArray ? "[ ]" : "{ }";
const keyLabel = document.createElement("span");
keyLabel.className = "json-key";
if (key !== null) keyLabel.textContent = `"${key}": `;
const addBtn = document.createElement("span");
addBtn.className = "json-add";
addBtn.textContent = " [+]";
const children = document.createElement("div");
children.className = "json-children";
keyLabel.onclick = toggle.onclick = () => {
children.classList.toggle("collapsed");
};
addBtn.onclick = () => {
let newKey = "";
let newValue;
feedbox({
title: `<span style="color:#f44336">Key</span> hinzufügen`,
message: `
<div style="display:flex;flex-direction:column;gap:8px;">
${!isArray ? `
<input id="newJsonKeyName" placeholder="Key Name" style="padding:6px;" />
` : ``}
<select id="newJsonValueType">
<option value="text">Text</option>
<option value="number">Zahl</option>
<option value="boolean">Boolean</option>
<option value="object">Objekt</option>
<option value="array">Array</option>
</select>
</div>
`,
buttons: {
yes: {
text: '<b>Anlegen</b>',
onClick: () => {
pushHistory();
if (!isArray) {
const input = document.querySelector('#newJsonKeyName');
newKey = input?.value?.trim();
if (!newKey) return;
}
const selectedType = document.querySelector('#newJsonValueType').value;
switch (selectedType) {
case "number": newValue = 0; break;
case "boolean": newValue = false; break;
case "object": newValue = {}; break;
case "array": newValue = []; break;
default: newValue = "";
}
if (isArray) {
value.push(newValue);
} else {
value[newKey] = newValue;
}
onChange(data);
render();
}
},
no: { text: 'Abbrechen' }
}
});
};
const inner = document.createElement("div");
const entries = isArray
? value.map((v, i) => [i, v])
: Object.entries(value);
entries.forEach(([k, v]) => {
inner.appendChild(
renderNode(v, k, value, `${path}.${k}`, level + 1)
);
});
children.appendChild(inner);
header.appendChild(keyLabel);
header.appendChild(toggle);
header.appendChild(addBtn);
if (key !== null) header.appendChild(removeBtn);
wrapper.appendChild(header);
wrapper.appendChild(children);
return wrapper;
}
// STRING
if (typeof value === "string") {
const input = document.createElement("input");
input.className = "json-input string";
input.value = value;
input.oninput = () => {
pushHistory();
parent[key] = input.value;
autoResize(input);
onChange(data);
};
setTimeout(() => autoResize(input), 0);
wrapper.appendChild(keySpan);
wrapper.appendChild(input);
if (key !== null) wrapper.appendChild(removeBtn);
return wrapper;
}
// NUMBER
if (typeof value === "number") {
const input = document.createElement("input");
input.type = "number";
input.className = "json-input number";
input.value = value;
input.oninput = () => {
pushHistory();
parent[key] = Number(input.value);
autoResize(input);
onChange(data);
};
setTimeout(() => autoResize(input), 0);
wrapper.appendChild(keySpan);
wrapper.appendChild(input);
if (key !== null) wrapper.appendChild(removeBtn);
return wrapper;
}
// BOOLEAN
if (typeof value === "boolean") {
const input = document.createElement("input");
input.type = "checkbox";
input.checked = value;
input.onchange = () => {
pushHistory();
parent[key] = input.checked;
onChange(data);
};
wrapper.appendChild(keySpan);
wrapper.appendChild(input);
if (key !== null) wrapper.appendChild(removeBtn);
return wrapper;
}
const span = document.createElement("span");
span.textContent = String(value);
wrapper.appendChild(keySpan);
wrapper.appendChild(span);
if (key !== null) wrapper.appendChild(removeBtn);
return wrapper;
}
pushHistory();
render();
return {
undo,
redo,
save,
refresh: render,
getData: () => data,
getChanges() {
return diff(lastSnapshot, data);
},
commit() {
lastSnapshot = clone(data);
},
update(newData) {
data = newData;
lastSnapshot = clone(newData);
pushHistory();
render();
}
};
}

View File

@@ -0,0 +1,244 @@
/*
const ctx = new ContextMenu();
ctx.setItems([
{ label: "Öffnen", disable: true, onClick: () => alert("Öffnen!") },
{ label: "Öffnen", onClick: () => alert("Öffnen!") },
{ label: "Bearbeiten", onClick: () => alert("Bearbeiten…") },
{ type: "divider" },
{
label: "Mehr Optionen",
color: 'rgb()|color-name|color-value',
children: [
{ label: "Duplizieren", onClick: () => alert("Dupliziert!") },
{ label: "Umbenennen", onClick: () => alert("Rename…") },
{
label: "Exportieren",
children: [
{ label: "PDF", onClick: () => alert("PDF export") },
{ label: "CSV", onClick: () => alert("CSV export") },
]
}
]
},
{ label: "Löschen", onClick: () => alert("Gelöscht!") },
]);
ctx.show(element, or location, { position: "right" });
*/
class ContextMenu {
constructor() {
this.menu = document.createElement("div");
this.menu.className = "ctx-menu hidden";
document.body.appendChild(this.menu);
// close on click outside → sofort
document.addEventListener("click", (e) => {
if (!this.menu.contains(e.target)) {
this.closeAllSubmenus();
this.hide();
}
});
window.addEventListener("resize", () => this.hide());
window.addEventListener("scroll", () => this.hide());
}
setItems(items) {
this.menu.innerHTML = "";
this.menu.appendChild(this.buildMenu(items));
}
buildMenu(items) {
const ul = document.createElement("ul");
ul.className = "ctx-list";
items.forEach(item => {
if (item.type === "divider") {
const divider = document.createElement("li");
divider.className = "ctx-divider";
ul.appendChild(divider);
return;
}
const li = document.createElement("li");
li.className = "ctx-item";
if (item.color) li.style.boxShadow = `inset 5px 0px 0px 0px ${item.color}`;
if (item.disabled) li.classList.add("ctx-disabled");
li.innerHTML = `
<span class="ctx-label">${item.label}</span>
${item.children ? "<span class='ctx-arrow'>▶</span>" : ""}
`;
if (item.onClick && !item.children && !item.disabled) {
li.addEventListener("click", e => {
e.stopPropagation();
item.onClick(e);
this.hide(); // Hauptmenü sofort schließen
});
}
if (item.children) {
const submenu = this.buildMenu(item.children);
submenu.classList.add("ctx-submenu");
li.appendChild(submenu);
let hideTimeout;
const openSubmenu = () => {
clearTimeout(hideTimeout);
submenu.classList.add("open");
submenu.style.left = "";
submenu.style.top = "";
const liRect = li.getBoundingClientRect();
const submenuRect = submenu.getBoundingClientRect();
let left = li.offsetWidth;
if (liRect.right + submenuRect.width > window.innerWidth) left = -submenuRect.width;
submenu.style.left = left + "px";
let top = 0;
if (liRect.top + submenuRect.height > window.innerHeight) {
top = window.innerHeight - (liRect.top + submenuRect.height) - 4;
}
submenu.style.top = top + "px";
};
const closeSubmenu = (delay = 500) => {
clearTimeout(hideTimeout);
hideTimeout = setTimeout(() => {
submenu.classList.remove("open");
submenu.style.left = "";
submenu.style.top = "";
}, delay);
};
li.addEventListener("mouseenter", openSubmenu);
li.addEventListener("mouseleave", () => closeSubmenu(500));
submenu.addEventListener("mouseenter", () => clearTimeout(hideTimeout));
submenu.addEventListener("mouseleave", () => closeSubmenu(500));
}
ul.appendChild(li);
});
return ul;
}
closeAllSubmenus() {
this.menu.querySelectorAll(".ctx-submenu.open").forEach(sm => {
sm.classList.remove("open");
sm.style.left = "";
sm.style.top = "";
});
}
show(target, options = {}) {
this.closeAllSubmenus();
let x, y;
const position = options.position || "right"; // default
const offset = options.offset || 4;
// 🖱️ Mausposition
if (typeof target === "number") {
x = target;
y = options.y;
}
// 📦 Element als Anchor
else if (target instanceof HTMLElement) {
const rect = target.getBoundingClientRect();
switch (position) {
case "right":
x = rect.right + offset;
y = rect.top;
break;
case "left":
x = rect.left - this.menu.offsetWidth - offset;
y = rect.top;
break;
case "top":
x = rect.left;
y = rect.top - this.menu.offsetHeight - offset;
break;
case "bottom":
default:
x = rect.left;
y = rect.bottom + offset;
break;
}
}
// ❌ invalid fallback
else {
return;
}
// 👉 erstmal anzeigen (wichtig für width/height!)
this.menu.style.left = "0px";
this.menu.style.top = "0px";
this.menu.classList.add("show");
this.menu.classList.remove("hidden");
const rect = this.menu.getBoundingClientRect();
// 🧠 Reposition nach echten Maßen
if (target instanceof HTMLElement) {
const anchor = target.getBoundingClientRect();
switch (position) {
case "right":
x = anchor.right + offset;
y = anchor.top;
break;
case "left":
x = anchor.left - rect.width - offset;
y = anchor.top;
break;
case "top":
x = anchor.left;
y = anchor.top - rect.height - offset;
break;
case "bottom":
x = anchor.left;
y = anchor.bottom + offset;
break;
}
}
// 🧱 Screen Bounds
if (x + rect.width > window.innerWidth) {
x = window.innerWidth - rect.width - 4;
}
if (y + rect.height > window.innerHeight) {
y = window.innerHeight - rect.height - 4;
}
if (x < 0) x = 4;
if (y < 0) y = 4;
this.menu.style.left = x + "px";
this.menu.style.top = y + "px";
}
hide() {
this.menu.classList.remove("show");
setTimeout(() => this.menu.classList.add("hidden"), 200);
}
}

View File

@@ -0,0 +1,259 @@
let activeFeedbox = null;
//#region Messagebox
function getMessageColorByLevelId(levelId) {
return levelId == -1 ? 'test' :
levelId == 0 ? 'success' :
levelId == 1 ? 'info' :
levelId == 2 ? 'warn' :
levelId == 4 ? 'error' : 'throw_exception';
}
/**
* Shows messages in a containerbox on the right
* @param {string} title - Title
* @param {string} text - Message content
* @param {string} levelId - 1=info | 2=warn | 4=error | 8=throw_exception | 16=success
* @param {string} [targetId] - Which element is getting focused
* @param {number} duration - Time in ms until auto-close
*/
// function showMessage(title, text, levelId = 1, targetId = null, duration = 4000) {
function showMessage(title, text, levelId = 1, onclick = null, duration = 4000) {
// Falls kein Container existiert → automatisch anlegen
let container = document.getElementById('message-container');
if (!container) {
container = document.createElement('div');
container.id = 'message-container';
container.style.position = 'fixed';
container.style.top = '20px';
container.style.right = '20px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'flex-end';
container.style.zIndex = '9999';
document.body.appendChild(container);
}
const message = document.createElement('div');
message.classList.add('message', getMessageColorByLevelId(levelId));
// --- HEADER ---
const header = document.createElement('div');
header.className = 'message-header';
const titleContainer = document.createElement('div');
titleContainer.className = 'message-title';
titleContainer.innerHTML = title;
const pinDiv = document.createElement('div');
pinDiv.className = 'pin-div';
pinDiv.innerHTML = '📌';
pinDiv.title = 'Anpinnen';
const countdown = document.createElement('span');
countdown.className = 'countdown';
countdown.textContent = `${(duration / 1000).toFixed(1)}s`;
// Titel + Countdown + Pin in eine Zeile
header.appendChild(titleContainer);
header.appendChild(countdown);
header.appendChild(pinDiv);
// --- BODY ---
const body = document.createElement('div');
body.className = 'message-text';
body.innerHTML = text;
message.appendChild(header);
message.appendChild(body);
container.appendChild(message);
// --- LOGIK ---
let pinned = false;
let remainingTime = duration;
let interval = null;
let lastTick = Date.now();
function startCountdown() {
lastTick = Date.now();
clearInterval(interval);
interval = setInterval(() => {
const now = Date.now();
const delta = now - lastTick;
lastTick = now;
remainingTime -= delta;
if (remainingTime <= 0 && !pinned) {
clearInterval(interval);
slideOutMessage(message);
} else if (!pinned) {
countdown.textContent = `${(remainingTime / 1000).toFixed(1)}s`;
}
}, 100);
}
function stopCountdown() {
clearInterval(interval);
}
// Start Countdown
startCountdown();
// Hover pausiert Countdown
message.addEventListener('mouseenter', stopCountdown);
message.addEventListener('mouseleave', () => {
if (!pinned) startCountdown();
});
// Klick auf Nachricht → schließen (nur wenn nicht gepinnt)
message.addEventListener('click', (e) => {
if(onclick) {
onclick();
}
// if (e.target === pinDiv) return;
// if (targetId) {
// const target = document.getElementById(targetId);
// if (target) target.scrollIntoView({ behavior: 'smooth' });
// }
if (!pinned) slideOutMessage(message);
});
// Klick auf Pin → anheften / lösen
pinDiv.addEventListener('click', (e) => {
e.stopPropagation();
pinned = !pinned;
if (pinned) {
pinDiv.classList.add('pinned');
pinDiv.title = 'Loslösen';
stopCountdown();
countdown.textContent = '📍 Angeheftet';
} else {
pinDiv.classList.remove('pinned');
pinDiv.title = 'Anpinnen';
startCountdown();
}
});
}
function slideOutMessage(message) {
if (!message) return;
message.style.animation = 'slideOut 0.5s forwards';
setTimeout(() => {
if (message.parentNode) message.parentNode.removeChild(message);
}, 300);
}
//#region Feedbox
/**
* feedbox({
* title: `<span style="color:#f44336">⚠ Upload abbrechen?</span>`,
* message: `
* <p>Es laufen noch <b>aktive Uploads</b>.</p>
* <p>Möchtest du wirklich <u>alle abbrechen</u>?</p>
* `,
* buttons: {
* yes: {
* text: '<b>Ja</b>, abbrechen',
* onClick: () => stopUploadQueue()
* },
* no: {
* text: 'Weiter hochladen'
* },
* cancel: {
* text: 'Zurück'
* }
* }
*});
*/
function feedbox({
title = '',
message = '',
buttons = {}, // buttons: { yes: { text: 'Yes', onClick: () => { } } }
primary = null, // name of the primary button, to accept with enter-key
lock = false, // locks desktop
replace = false // replaces an actually shown feedbox
}) {
// 🚫 Nested verhindern
if (activeFeedbox) {
if (!replace) {
return Promise.resolve('blocked');
}
activeFeedbox.close('replaced');
}
return new Promise(resolve => {
const overlay = document.createElement('div');
overlay.className = 'feedbox-overlay';
const box = document.createElement('div');
box.className = 'feedbox';
const h = document.createElement('h3');
h.innerHTML = title;
const msg = document.createElement('div');
msg.className = 'feedbox-message';
msg.innerHTML = message;
const actions = document.createElement('div');
actions.className = 'feedbox-actions';
const btnMap = {};
Object.entries(buttons).forEach(([key, cfg]) => {
if (!cfg) return;
const btn = document.createElement('button');
btn.className = `feedbox-btn ${key}`;
btn.innerHTML = cfg.text ?? key;
btn.onclick = () => {
cfg.onClick?.();
close(key);
};
btnMap[key] = btn;
actions.appendChild(btn);
});
function close(result) {
document.removeEventListener('keydown', keyHandler);
overlay.remove();
activeFeedbox = null;
resolve(result);
}
function keyHandler(e) {
if (e.key === 'Escape') close('cancel');
if (e.key === 'Enter' && primary && btnMap[primary]) {
btnMap[primary].click();
}
}
if (!lock) {
overlay.onclick = e => {
if (e.target === overlay) close('cancel');
};
}
document.addEventListener('keydown', keyHandler);
box.append(h, msg, actions);
overlay.appendChild(box);
document.body.appendChild(overlay);
// Fokus
if (primary && btnMap[primary]) {
setTimeout(() => btnMap[primary].focus(), 0);
}
// 🔐 Singleton setzen
activeFeedbox = { close };
});
}
//#endregion

View File

@@ -0,0 +1,97 @@
const auth = { objectGuid: getCookie('ObjectGUID'), sAMAccountName: getCookie('sAMAccountName') };
const mainSocket = io.connect('/', { reconnect: true, auth: auth });
const adminSocket = io.connect('/admin', { reconnect: true, auth: auth });
const container = document.getElementById('message-container');
adminSocket.on('eventlog', (data) => {
showMessage('EventLog',
`${data.datetime}<br>${[-1,0].includes(data.levelId) ? '' : data.trace}<br><br>${(Array.isArray(data.message) ? data.message.split('\r\n').join('<br>').split('\t').join('&ensp;') : data.message.split('\r\n').join('<br>').split('\t').join('&ensp;'))}`,
data.levelId,
() => {
} ,
10000);
});
mainSocket.on('event', (data) => {
showMessage(data.pluginName,
`[${data.datetime}]<br><br>${(Array.isArray(data.message) ? data.message.split('\r\n').join('<br>').split('\t').join('&ensp;') : data.message.split('\r\n').join('<br>').split('\t').join('&ensp;'))}`,
data.levelId,
() => {
} ,
10000);
});
//-1=test, 0=success, 1=log, 2=warn, 4=error, 8=throw_exception
function writeEventLog(levelId, pluginName, message) {
adminSocket.emit('eventlog', {
objectGuid: getCookie('ObjectGUID'),
levelId: levelId,
pluginName: pluginName,
message: message.stack === undefined ? message : { message: message.message, stack: message.stack }
});
}
// levelId: if -1, then write no log entry
// sendToParams: where clause to find objectGUIDs to send
function sendUserEvent(pluginName, message, sendToParams, levelId = -1) {
mainSocket.emit('event', {
objectGuid: getCookie('ObjectGUID'),
levelId: levelId,
pluginName: pluginName,
message: message.stack === undefined ? message : { message: message.message },
sendToParams: sendToParams
});
}
/*
{
"status":"unload",
"pluginName":"user_management",
"metadata":{
"name":"user_management",
"menuName":"User Management",
"description":"Beschreibung hier einfügen",
"version":"0.9.25.11.14",
"icon":"",
"permissions":[],
"config":{
},
"active":false
},
"levelId":0,
"message":"Plugin user_management entladen",
"authorized": true
}
*/
mainSocket.on('plugin_status', payload => {
const startMenuItem = document.querySelector(`[data-appname=${payload.metadata.name}]`);
if(['load', 'unload', 'update'].includes(payload.status)) {
if(payload.status == 'load') {
if(payload.authorized) {
startMenuItem.classList.remove('unload');
startMenuItem.setAttribute('data-active', true);
} else {
startMenuItem.classList.add('unload');
startMenuItem.setAttribute('data-active', false);
}
} else if(payload.status == 'unload') {
startMenuItem.classList.add('unload');
startMenuItem.setAttribute('data-active', false);
} else if(payload.status == 'update') {
if(payload.authorized && payload.metadata.active) {
startMenuItem.classList.remove('unload');
startMenuItem.setAttribute('data-active', true);
} else if(!payload.authorized){
startMenuItem.classList.add('unload');
startMenuItem.setAttribute('data-active', false);
}
}
}
});

1494
public/javascript/main.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
class NotifyBubble {
constructor(trayButton, bubbleSelector) {
this.button = trayButton;
this.bubble = document.querySelector(bubbleSelector);
this.hideTimeout = null;
this.initEvents();
this.initExistingItems();
this.updateCounter(); // 🔥 initialer Counter
}
/* =========================
SHOW / HIDE
========================== */
show() {
clearTimeout(this.hideTimeout);
this.button.classList.add("active");
// Tooltip entfernen beim Öffnen
this.button.removeAttribute('data-tooltip');
}
hide(delay = 1000) {
clearTimeout(this.hideTimeout);
this.hideTimeout = setTimeout(() => {
this.button.classList.remove("active");
// 🔥 Counter nach Schließen anzeigen
this.updateCounter();
}, delay);
}
/* =========================
COUNTER / TOOLTIP
========================== */
updateCounter() {
const items = this.bubble.querySelectorAll(".bubble-item");
const count = items.length;
if (!this.button.classList.contains("active") && count > 0) {
this.button.setAttribute('data-tooltip', `${count} neue Benachrichtigung${count > 1 ? 'en' : ''}`);
} else {
this.button.setAttribute('data-tooltip', "Keine Benachrichtigungen");
}
}
/* =========================
EVENTS
========================== */
initEvents() {
// Toggle bei Klick auf Button
this.button.addEventListener("click", (e) => {
e.stopPropagation();
if (this.button.classList.contains("active")) {
this.hide(0);
} else {
this.show();
}
});
// Klick innerhalb der Bubble soll NICHT schließen
this.bubble.addEventListener("click", (e) => {
e.stopPropagation();
});
// Klick irgendwo anders → schließen
document.addEventListener("click", () => {
if (this.button.classList.contains("active")) {
this.hide(0);
}
});
}
/* =========================
INIT EXISTING ITEMS
========================== */
initExistingItems() {
this.bubble.querySelectorAll(".bubble-item").forEach(item => {
this.attachItemEvents(item);
});
}
/* =========================
ITEM EVENTS
========================== */
attachItemEvents(item) {
item.addEventListener("click", () => {
const checkbox = item.querySelector("input[type='checkbox']");
if (checkbox) {
checkbox.checked = !checkbox.checked;
fetch('/api/NotifyTray/markAsSeen', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: item.dataset.id,
value: checkbox.checked
})
});
// 🔥 Counter ggf. aktualisieren
this.updateCounter();
}
});
}
/* =========================
ADD SINGLE ITEM
========================== */
addItem(notification) {
const item = document.createElement("label");
item.className = "bubble-item";
item.dataset.id = notification.ID;
item.innerHTML = `
<div>${notification.Message}</div>
<div style="display:flex;flex-direction:row;flex-wrap:no-wrap;justify-content:flex-end;gap:10px;align-items:center;">
<div>
<div>${dateFormat(notification.CreatedAt, 'dd.mm.yyyy HH:MM:SS')}</div>
<div>${notification.PluginName}</div>
</div>
<div>
<label class="cb cb-modern">
<input id="notification_${notification.ID}" type="checkbox">
<span class="cb-box"></span>
</label>
</div>
</div>
`;
this.attachItemEvents(item);
this.bubble.appendChild(item);
// 🔥 Counter aktualisieren
this.updateCounter();
}
/* =========================
ADD MULTIPLE ITEMS
========================== */
addItems(notifications) {
notifications.forEach(n => this.addItem(n));
}
/* =========================
CLEAR
========================== */
clear() {
this.bubble.innerHTML = "";
this.updateCounter();
}
}

1081
public/javascript/os.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
const pluginAPI = {
async update(name, updates) {
if(Object.keys(updates)[0] && Object.values(updates)[0] == "") {
updates[Object.keys(updates)[0]] = [ ];
}
return await this._request(
`/api/plugins/${name}/update`,
'POST',
{ updates }
);
},
async activation(name, state) {
return await this._request(
`/api/plugins/activation`,
'POST',
{ name, state }
);
},
async create(name) {
return await this._request(
`/api/plugins/${name}/create`,
'POST'
);
},
async rename(name, newName) {
return await this._request(
`/api/plugins/${name}/rename`,
'POST',
{ newName }
);
},
async delete(name) {
return await this._request(
`/api/plugins/${name}/delete`,
'POST'
);
},
// --- zentrale Request-Funktion ---
async _request(url, method, body) {
try {
const options = { method };
if (body) {
options.headers = { 'Content-Type': 'application/json' };
options.body = JSON.stringify(body);
}
const res = await fetch(url, options);
if (!res.ok) {
const text = await res.text();
writeEventLog(4, 'CLIENT', `Request fehlgeschlagen: ${text}`);
throw new Error(`HTTP ${res.status}: ${text}`);
}
return res.json();
} catch (err) {
writeEventLog(4, 'CLIENT', err);
throw err;
}
}
};
class AttachOnBlurChange {
constructor(el, callback) {
this.el = el;
this.callback = callback;
this.initial = this.el.value;
this.el.addEventListener('blur', () => {
this.handleBlur();
});
}
handleBlur() {
if (this.el.value === this.initial) return;
const oldValue = this.initial;
const newValue = this.el.value;
this.initial = newValue;
this.callback(newValue, oldValue, this.el);
}
}

View File

@@ -0,0 +1,89 @@
function createRequiredProgress({ container, progress, onChange }) {
let total = 0;
let filled = 0;
const body = typeof container === 'string'
? document.querySelector(container)
: container;
const progressEl = typeof progress === 'string'
? document.querySelector(progress)
: progress;
if (!body || !progressEl) return;
function isElementVisible(el) {
const style = window.getComputedStyle(el);
return el.offsetParent !== null && style.opacity !== '0';
}
function updateRequiredProgress() {
const requiredElements = Array.from(
body.querySelectorAll('input[required], select[required], textarea[required]')
).filter(isElementVisible);
total = requiredElements.length;
filled = requiredElements.filter(el =>
el.classList.contains('is-required-filled')
).length;
progressEl.textContent = `${filled} / ${total} Pflichtfelder ausgefüllt`;
progressEl.classList.toggle('complete', filled === total);
progressEl.classList.toggle('incomplete', filled !== total);
}
function updateRequiredState(el) {
const isEmpty =
(el instanceof HTMLSelectElement && !el.value) ||
(el instanceof HTMLInputElement && el.type !== 'checkbox' && !el.value.trim()) ||
(el instanceof HTMLTextAreaElement && !el.value.trim());
el.classList.toggle('is-required-empty', isEmpty);
el.classList.toggle('is-required-filled', !isEmpty);
updateRequiredProgress();
if (typeof onChange === 'function') {
onChange({
element: el,
isEmpty,
isFilled: !isEmpty,
isFinished: total === filled
});
}
}
function initialize() {
const elements = Array.from(
body.querySelectorAll('input[required], select[required], textarea[required]')
).filter(isElementVisible);
elements.forEach(el => {
updateRequiredState(el);
el.addEventListener('input', () => updateRequiredState(el));
el.addEventListener('change', () => updateRequiredState(el));
});
updateRequiredProgress();
}
// 🔥 Reagiere auf Sichtbarkeitsänderungen
const observer = new MutationObserver(updateRequiredProgress);
observer.observe(body, {
attributes: true,
subtree: true,
attributeFilter: ['style', 'class', 'hidden']
});
initialize();
// Optional: API zurückgeben
return {
initialize: initialize,
refresh: updateRequiredProgress,
destroy() {
observer.disconnect();
}
};
}

View File

@@ -0,0 +1,450 @@
/*
//Generic table filter module
// Usage:
TableFilter({
table: document.querySelector('#myTable'),
exceptedColumns: [ 'Status_ID' ],
filterConfig: {
columnModes: {
ID: 'text', // Textsuche in dieser Spalte
Status: 'dropdown', // Dropdown-Werte aus Tabelle sammeln
Objekt: 'text', // Textsuche
Priorität: 'dropdown',
Gewerk: 'dropdown',
Typ: 'dropdown',
Bedarfsmelder: 'text'
Bearbeiter: 'text'
Genehmiger: 'text'
},
checkboxFilter: {
column: 'Status_ID',
rules: [
{ label: 'Bearbeitung', test: v => [1,4,6,7,12,13,14].includes(parseInt(v)) },
{ label: 'Genehmigt', test: v => [2,5,9,10,11].includes(parseInt(v)) },
{ label: 'Abgelehnt', test: v => [3].includes(parseInt(v)) },
{ label: 'Abgeschlossen', test: v => [8].includes(parseInt(v)) }
]
}
}
});
// Generic table filter module (one textbox OR column-based dropdown)
// New features:
// - Selecting a column in the column-selector decides: textbox OR dropdown (configured in options)
// - If column-selector = empty value ⇒ full-row search
// - No extra checkbox needed
// - UI stays compact (only ONE input element + ONE dropdown)
*/
function TableFilter(options) {
try {
const table = options.table;
const cfg = options.filterConfig;
const exceptedColumns = options.exceptedColumns || [];
// Gruppenerkennung + Verwaltung
const groupMap = new Map(); // groupRow → childRows[]
let groupsInitialized = false;
const state = {
selectedColumn: '',
searchText: '',
dropdownValue: '',
dropdownCache: {},
checkbox: new Set(),
};
//------------------------------------
// Sticky Container für Filter + Header
//------------------------------------
if(document.querySelectorAll('.table-filter-container').length > 0) {
// Array.from(document.querySelectorAll('.table-filter-container')).forEach(filterContainer => {
// filterContainer.remove();
// })
const existing = table.previousElementSibling;
if (existing && existing.classList.contains('table-filter-container')) {
existing.remove();
}
}
const container = document.createElement('div');
container.className = 'table-filter-container';
container.style.position = 'sticky';
container.style.top = '0px';
container.style.left = '0px';
container.style.zIndex = 20;
// Filter UI wird hier reingesetzt
table.before(container);
// Tabelle selbst: header sticky
const thead = table.querySelector('thead');
const headerCells = Array.from(table.querySelectorAll('thead th'));
const getColumnIndex = name => headerCells.findIndex(c => c.textContent.trim() === name);
//------------------------------------
// Dynamisches Filter-UI
//------------------------------------
function createDynamicFilterUI() {
const wrap = document.createElement('div');
wrap.style="display:flex;flex-direction:row;align-items:center;gap:10px"
const colSelectLabel = document.createElement('label');
const colSelect = document.createElement('select');
const emptyOpt = document.createElement('option');
emptyOpt.value = '';
emptyOpt.textContent = '-- Alles --';
colSelect.appendChild(emptyOpt);
colSelectLabel.innerText = 'Spalte';
headerCells.forEach(cell => {
if(!exceptedColumns.includes(cell.textContent.trim())) {
const name = cell.textContent.trim();
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
colSelect.appendChild(opt);
}
});
const input = document.createElement('input');
input.type = 'search';
input.style.display = 'none';
const select = document.createElement('select');
select.style.display = 'none';
select.innerHTML = '<option value="">-- Alle --</option>';
function populateDropdownForColumn(colName) {
const colIndex = getColumnIndex(colName);
const rows = Array.from(table.querySelectorAll('tbody tr')).filter(r => !r.classList.contains('grouprow'));
const values = new Set();
rows.forEach(r => {
const cell = r.children[colIndex];
if (cell) values.add(cell.textContent.trim());
});
const sorted = new Set([...values].sort((a, b) => a.localeCompare(b)));
select.innerHTML = '<option value="">-- Alle --</option>' + [...sorted].map(v => `<option value="${v}">${v}</option>`).join('');
}
colSelect.addEventListener('change', () => {
state.selectedColumn = colSelect.value;
state.searchText = '';
state.dropdownValue = '';
input.value = '';
select.value = '';
if (!state.selectedColumn) {
input.style.display = '';
select.style.display = 'none';
return applyFilters();
}
const mode = cfg.columnModes[state.selectedColumn];
if (mode === 'text') {
input.style.display = '';
select.style.display = 'none';
} else if (mode === 'dropdown') {
input.style.display = 'none';
select.style.display = '';
populateDropdownForColumn(state.selectedColumn);
} else {
input.style.display = '';
select.style.display = 'none';
}
applyFilters();
});
input.style.display = '';
input.addEventListener('input', () => { state.searchText = input.value.toLowerCase(); applyFilters(); });
select.addEventListener('change', () => { state.dropdownValue = select.value; applyFilters(); });
wrap.append(colSelectLabel, colSelect, document.createElement('br'));
wrap.append(input, select);
container.appendChild(wrap);
}
//--------------------------------------------
// Filterbreite an Parent minus Scrollbar
//--------------------------------------------
function updateFilterWidth() {
if (!table || !container) return;
const wrapper = table.parentElement; // z.B. .table-wrapper
if (!wrapper) return;
const style = getComputedStyle(table.querySelector('thead th'));
const rect = wrapper.getBoundingClientRect();
const scrollbarWidth = wrapper.offsetWidth - wrapper.clientWidth;
container.style.width = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px";
container.style.maxWidth = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px";
}
// Initial setzen
updateFilterWidth();
// Fenstergröße beobachten
window.addEventListener('resize', updateFilterWidth);
// Dynamische Anpassung bei Wrapper Resize
const wrapper = table.parentElement;
if (wrapper) {
new ResizeObserver(updateFilterWidth).observe(wrapper);
}
//------------------------------------
// Checkbox Filter
//------------------------------------
function createCheckboxFilter() {
const cfgCheck = cfg.checkboxFilter;
const idx = getColumnIndex(cfgCheck.column);
if (idx < 0) return;
const wrapper = document.createElement('div');
cfgCheck.rules.forEach(rule => {
const checkWrapper = document.createElement('label');
const checkInput = document.createElement('input');
const checkTrack = document.createElement('span');
const checkThumb = document.createElement('span');
checkWrapper.classList.add("cb", "cb-switch");
checkWrapper.style.marginRight = '15px'
checkInput.type = 'checkbox';
checkInput.addEventListener('change', () => {
if (checkInput.checked) state.checkbox.add(rule);
else state.checkbox.delete(rule);
applyFilters();
});
checkTrack.classList.add('switch-track');
checkTrack.setAttribute("aria-hidden", 'true');
checkThumb.classList.add('switch-thumb');
checkThumb.setAttribute("aria-hidden", 'true');
checkTrack.append(checkThumb)
checkWrapper.append(rule.label, checkInput, checkTrack);
wrapper.appendChild(checkWrapper);
});
container.appendChild(wrapper);
}
//------------------------------------
// Live Counter
//------------------------------------
const counter = document.createElement('div');
counter.className = 'live-counter';
container.appendChild(counter);
//------------------------------------
// Filter Logik
//------------------------------------
function detectGroups() {
if (groupsInitialized) return;
groupsInitialized = true;
const rows = Array.from(table.querySelectorAll('tbody tr'));
let currentGroupRow = null;
let childBuffer = [];
rows.forEach(row => {
if (row.classList.contains('grouprow')) {
// vorige Gruppe speichern
if (currentGroupRow && childBuffer.length > 0) {
groupMap.set(currentGroupRow, childBuffer);
}
// neue Gruppe starten
currentGroupRow = row;
childBuffer = [];
}
else if (currentGroupRow) {
childBuffer.push(row);
}
});
// letzte Gruppe speichern
if (currentGroupRow && childBuffer.length > 0) {
groupMap.set(currentGroupRow, childBuffer);
}
// jedem groupRow ein Toggle verpassen (wenn nicht vorhanden)
groupMap.forEach((childRows, groupRow) => {
const toggle = groupRow.querySelector("span");
if (!toggle || toggle._groupToggleBound) return;
toggle._groupToggleBound = true;
toggle.addEventListener("click", () => toggleGroup(groupRow));
groupRow.addEventListener("dblclick", () => toggleGroup(groupRow));
});
}
function toggleGroup(groupRow) {
const childRows = groupMap.get(groupRow);
if (!childRows) return;
const toggle = groupRow.querySelector("span");
const collapsed = toggle.textContent === '+';
if (collapsed) {
// ausklappen → nur gefilterte Rows zeigen
childRows.forEach(r => {
if (!r._filteredOut) r.style.display = '';
});
toggle.textContent = '-';
} else {
// einklappen → alle verstecken
childRows.forEach(r => r.style.display = 'none');
toggle.textContent = '+';
}
}
function applyFilters() {
detectGroups();
const rows = Array.from(table.querySelectorAll('tbody tr'));
let visible = 0;
rows.forEach(row => {
if (row.classList.contains('grouprow')) { row.style.display = ''; return; }
let show = true;
if (state.selectedColumn === '') {
if (state.searchText) show = [...row.cells].some(c => c.textContent.toLowerCase().includes(state.searchText));
} else {
const idx = getColumnIndex(state.selectedColumn);
const mode = cfg.columnModes[state.selectedColumn];
if (mode === 'text' && state.searchText) {
show = row.cells[idx]?.textContent.toLowerCase().includes(state.searchText);
}
if (mode === 'dropdown' && state.dropdownValue) {
show = row.cells[idx]?.textContent === state.dropdownValue;
}
}
if (state.checkbox.size > 0) {
const idx = getColumnIndex(cfg.checkboxFilter.column);
const cellVal = row.cells[idx]?.textContent ?? '';
const pass = [...state.checkbox].some(rule => rule.test(cellVal));
if (!pass) show = false;
}
row._filteredOut = !show; // fürs Gruppensystem speichern
// Wenn Gruppen existieren:
if (groupMap.size > 0) {
// Prüfen ob row zu einer Gruppe gehört
let parentGroup = null;
for (const [g, list] of groupMap.entries()) {
if (list.includes(row)) {
parentGroup = g;
break;
}
}
if (parentGroup) {
const toggle = parentGroup.querySelector("span");
const collapsed = toggle.textContent === '+';
if (collapsed) {
// eingeklappt → immer ausblenden
row.style.display = 'none';
if (show) visible++;
} else {
// ausgeklappt → nur gefilterte anzeigen
row.style.display = show ? '' : 'none';
if (show) visible++;
}
return;
}
} else {
if (show) visible++;
}
// normale Zeile ohne Gruppe:
row.style.display = show ? '' : 'none';
});
counter.textContent = `${visible} Treffer`;
}
//------------------------------------
// Init
//------------------------------------
createDynamicFilterUI();
if(options.filterConfig.checkboxFilter) createCheckboxFilter();
applyFilters();
if (thead) {
thead.style.position = 'sticky';
thead.style.top = (container.offsetHeight - 1) + 'px';
thead.style.zIndex = 10;
}
//------------------------------------
// Public API
//------------------------------------
return {
refreshDropdown() { if (state.selectedColumn && cfg.columnModes[state.selectedColumn] === 'dropdown') {} },
reapply() { applyFilters(); }
};
} catch(err) {
alert(err.stack)
}
}
//#region Sort table
// let sortDirection = {};
function sortTable(tableId,colIndex) {
const sortDirection = {};
const table = document.getElementById(tableId);
const tbody = table.tBodies[0];
const rows = Array.from(tbody.querySelectorAll("tr"));
// Toggle sort direction
sortDirection[colIndex] = !sortDirection[colIndex];
rows.sort((a, b) => {
const cellA = a.cells[colIndex].textContent.trim();
const cellB = b.cells[colIndex].textContent.trim();
// Numeric sort if possible
const numA = parseFloat(cellA);
const numB = parseFloat(cellB);
if (!isNaN(numA) && !isNaN(numB)) {
return sortDirection[colIndex] ? numA - numB : numB - numA;
} else {
return sortDirection[colIndex]
? cellA.localeCompare(cellB)
: cellB.localeCompare(cellA);
}
});
// Remove old rows
tbody.innerHTML = "";
rows.forEach(row => tbody.appendChild(row));
// Update sort arrows
const th = table.querySelectorAll("th");
th.forEach((header, i) => {
header.classList.remove("sort-asc", "sort-desc");
if (i === colIndex) {
header.classList.add(sortDirection[colIndex] ? "sort-asc" : "sort-desc");
}
});
}
//#endregion

View File

@@ -0,0 +1,354 @@
class Tutorial {
constructor(steps, options = {}) {
this.steps = steps;
this.currentStep = 0;
this.isRunning = false;
this.isWaitingForEvent = false;
this.options = {
overlayOpacity: 0.7,
...options
};
this.zIndexBase = 1000;
this.zIndexStep = 0;
this.modifiedElements = new Map();
this.baseElement = null;
this.currentElement = null;
this.tooltipObserver = null;
this.tooltipRAF = null;
this.currentStepElement = null;
}
// ---------------- UI ----------------
createUI() {
document.addEventListener("keydown", e => {
if (this.isWaitingForEvent) return;
if (e.key === "ArrowRight") this.next();
if (e.key === "ArrowLeft") this.prev();
});
this.tooltip = document.createElement("div");
this.tooltip.id = "tutorial-tooltip";
Object.assign(this.tooltip.style, {
position: "absolute",
display: "flex",
padding: "12px",
borderRadius: "8px",
zIndex: 9999,
maxWidth: "350px",
boxShadow: "0 10px 30px rgba(0,0,0,0.2)",
backdropFilter: "blur(5px)",
justifyContent: "center"
});
this.text = document.createElement("p");
this.nextBtn = document.createElement("button");
this.nextBtn.innerHTML = ">";
this.nextBtn.classList.add("bluebutton");
this.prevBtn = document.createElement("button");
this.prevBtn.innerHTML = "<";
this.prevBtn.classList.add("yellowbutton");
this.nextBtn.onclick = () => this.next();
this.prevBtn.onclick = () => this.prev();
this.tooltip.appendChild(this.text);
this.tooltip.appendChild(this.prevBtn);
this.tooltip.appendChild(this.nextBtn);
document.body.appendChild(this.tooltip);
}
// ---------------- Element wait ----------------
waitForElement(selector, timeout = 5000) {
return new Promise(resolve => {
const start = Date.now();
const check = () => document.querySelector(selector);
let el = check();
if (el) return resolve(el);
const observer = new MutationObserver(() => {
el = check();
if (el) {
observer.disconnect();
resolve(el);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
const timer = setInterval(() => {
if (Date.now() - start > timeout) {
clearInterval(timer);
observer.disconnect();
resolve(null);
}
el = check();
if (el) {
clearInterval(timer);
observer.disconnect();
resolve(el);
}
}, 100);
});
}
// ---------------- Highlight ----------------
highlightElement(el) {
this.removeHighlight();
if (!this.modifiedElements.has(el)) {
this.modifiedElements.set(el, {
zIndex: el.style.zIndex,
position: el.style.position,
boxShadow: el.style.boxShadow
});
}
if (this.currentStep === 0 && !this.baseElement) {
this.baseElement = el;
}
let zIndex;
if (el === this.baseElement) {
zIndex = this.zIndexBase;
} else {
this.zIndexStep++;
zIndex = this.zIndexBase + this.zIndexStep;
}
el.style.position = "relative";
el.style.zIndex = zIndex;
el.style.boxShadow =
"0 0 0 4px rgba(255, 0, 0, 1), 0 0 20px rgba(255,0,0,0.7)";
this.currentElement = el;
this.startTooltipTracking(el);
}
removeHighlight() {
if (!this.currentElement) return;
this.currentElement.style.boxShadow = "";
this.stopTooltipTracking();
}
// ---------------- Event system (NEW) ----------------
getEventConfig(type) {
const map = {
click: () => new MouseEvent("click", { bubbles: true, cancelable: true, view: window }),
contextmenu: () => new MouseEvent("contextmenu", { bubbles: true, cancelable: true, view: window, button: 2 }),
dblclick: () => new MouseEvent("dblclick", { bubbles: true, cancelable: true, view: window }),
mouseenter: () => new MouseEvent("mouseenter", { bubbles: true, cancelable: true, view: window }),
mouseover: () => new MouseEvent("mouseover", { bubbles: true, cancelable: true, view: window })
};
return map[type] ? map[type]() : null;
}
async executeStepOnly(step, el) {
if (!step.action) return;
const event = this.getEventConfig(step.action);
if (event) {
el.dispatchEvent(event);
}
}
waitForUserEvent(el, type) {
return new Promise(resolve => {
const handler = (e) => {
el.removeEventListener(type, handler);
resolve(e);
};
el.addEventListener(type, handler);
});
}
// ---------------- Tooltip tracking ----------------
startTooltipTracking(el) {
this.stopTooltipTracking();
this.currentStepElement = el;
const update = () => {
if (!this.currentStepElement || !this.tooltip) return;
const rect = this.currentStepElement.getBoundingClientRect();
this.tooltip.style.top = rect.bottom + 10 + "px";
this.tooltip.style.left = rect.left + "px";
this.tooltipRAF = requestAnimationFrame(update);
};
this.tooltipObserver = new ResizeObserver(() => {
if (!this.currentStepElement) return;
const rect = this.currentStepElement.getBoundingClientRect();
this.tooltip.style.top = rect.bottom + 10 + "px";
this.tooltip.style.left = rect.left + "px";
});
this.tooltipObserver.observe(el);
this.tooltipRAF = requestAnimationFrame(update);
}
stopTooltipTracking() {
if (this.tooltipObserver) {
this.tooltipObserver.disconnect();
this.tooltipObserver = null;
}
if (this.tooltipRAF) {
cancelAnimationFrame(this.tooltipRAF);
this.tooltipRAF = null;
}
this.currentStepElement = null;
}
// ---------------- Steps ----------------
async showStep() {
let step = this.steps[this.currentStep];
this.updateButtons(step);
this.isWaitingForEvent = false;
while (step && (!step.text || step.text.trim() === "")) {
const elSilent = await this.waitForElement(step.element);
if (elSilent) {
await this.executeStepOnly(step, elSilent);
}
this.currentStep++;
if (this.currentStep >= this.steps.length) {
this.destroy();
return;
}
step = this.steps[this.currentStep];
}
const el = await this.waitForElement(step.element);
if (!el) {
this.next();
return;
}
this.highlightElement(el);
el.scrollIntoView({ behavior: "smooth", block: "center" });
const rect = el.getBoundingClientRect();
this.tooltip.style.display = "block";
this.text.innerHTML = step.text;
this.tooltip.style.top = rect.bottom + 10 + "px";
this.tooltip.style.left = rect.left + "px";
this.isWaitingForEvent = !!step.waitFor;
this.nextBtn.style.display = step.waitFor ? "none" : "inline-block";
this.prevBtn.style.display =
this.currentStep === 0 || step.waitFor ? "none" : "inline-block";
if (step.waitFor) {
await this.waitForUserEvent(el, step.waitFor);
this.isWaitingForEvent = false;
this.next();
}
}
updateButtons(step) {
const isLastStep = this.currentStep === this.steps.length - 1;
this.nextBtn.innerHTML = isLastStep ? "Fertig" : ">";
}
// ---------------- Navigation ----------------
start() {
if (this.isRunning) return;
this.isRunning = true;
this.createUI();
this.currentStep = 0;
this.showStep();
}
next() {
if (this.isWaitingForEvent) return;
this.removeHighlight();
if (this.currentStep < this.steps.length - 1) {
this.currentStep++;
this.showStep();
} else {
this.destroy();
}
}
prev() {
if (this.isWaitingForEvent) return;
this.removeHighlight();
if (this.currentStep > 0) {
this.currentStep--;
this.showStep();
}
}
// ---------------- Cleanup ----------------
destroy() {
this.isRunning = false;
this.stopTooltipTracking();
this.modifiedElements.forEach((styles, el) => {
el.style.zIndex = styles.zIndex || "";
el.style.position = styles.position || "";
el.style.boxShadow = styles.boxShadow || "";
});
this.modifiedElements.clear();
this.baseElement = null;
this.zIndexStep = 0;
this.tooltip?.remove();
}
}