Files
radixOS/public/javascript/tutorial.js
2026-04-22 11:55:23 +02:00

354 lines
8.6 KiB
JavaScript

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();
}
}