354 lines
8.6 KiB
JavaScript
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();
|
|
}
|
|
} |