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