initial files
This commit is contained in:
361
public/javascript/JSON.js
Normal file
361
public/javascript/JSON.js
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
244
public/javascript/contextMenu.js
Normal file
244
public/javascript/contextMenu.js
Normal 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);
|
||||
}
|
||||
}
|
||||
259
public/javascript/customModal.js
Normal file
259
public/javascript/customModal.js
Normal 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
|
||||
|
||||
97
public/javascript/loadOnce.js
Normal file
97
public/javascript/loadOnce.js
Normal 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(' ') : data.message.split('\r\n').join('<br>').split('\t').join(' '))}`,
|
||||
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(' ') : data.message.split('\r\n').join('<br>').split('\t').join(' '))}`,
|
||||
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
1494
public/javascript/main.js
Normal file
File diff suppressed because it is too large
Load Diff
157
public/javascript/notifyBubble.js
Normal file
157
public/javascript/notifyBubble.js
Normal 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
1081
public/javascript/os.js
Normal file
File diff suppressed because it is too large
Load Diff
92
public/javascript/pluginAPI.js
Normal file
92
public/javascript/pluginAPI.js
Normal 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);
|
||||
}
|
||||
}
|
||||
89
public/javascript/requiredFields.js
Normal file
89
public/javascript/requiredFields.js
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
450
public/javascript/tableFilter.js
Normal file
450
public/javascript/tableFilter.js
Normal 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
|
||||
354
public/javascript/tutorial.js
Normal file
354
public/javascript/tutorial.js
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user