259 lines
8.4 KiB
JavaScript
259 lines
8.4 KiB
JavaScript
const TOAST_POS_KEY = "pikit-toast-pos";
|
|
const TOAST_ANIM_KEY = "pikit-toast-anim";
|
|
const TOAST_SPEED_KEY = "pikit-toast-speed";
|
|
const TOAST_DURATION_KEY = "pikit-toast-duration";
|
|
const FONT_KEY = "pikit-font";
|
|
|
|
export const ALLOWED_TOAST_POS = [
|
|
"bottom-center",
|
|
"bottom-right",
|
|
"bottom-left",
|
|
"top-right",
|
|
"top-left",
|
|
"top-center",
|
|
];
|
|
export const ALLOWED_TOAST_ANIM = ["slide-in", "fade", "pop", "bounce", "drop", "grow"];
|
|
export const ALLOWED_FONTS = ["redhat", "space", "manrope", "dmsans", "sora", "chivo", "atkinson", "plex"];
|
|
|
|
const clamp = (val, min, max) => Math.min(max, Math.max(min, val));
|
|
|
|
export function createToastManager({
|
|
container,
|
|
posSelect,
|
|
animSelect,
|
|
speedInput,
|
|
durationInput,
|
|
fontSelect,
|
|
testBtn,
|
|
} = {}) {
|
|
const state = {
|
|
position: "bottom-center",
|
|
animation: "slide-in",
|
|
durationMs: 5000,
|
|
speedMs: 300,
|
|
font: "redhat",
|
|
};
|
|
|
|
function applyToastSettings() {
|
|
if (!container) return;
|
|
container.className = `toast-container pos-${state.position}`;
|
|
document.documentElement.style.setProperty("--toast-speed", `${state.speedMs}ms`);
|
|
const dir = state.position.startsWith("top") ? -1 : 1;
|
|
const isLeft = state.position.includes("left");
|
|
const isRight = state.position.includes("right");
|
|
const slideX = isLeft ? -26 : isRight ? 26 : 0;
|
|
const slideY = isLeft || isRight ? 0 : dir * 24;
|
|
document.documentElement.style.setProperty("--toast-slide-offset", `${dir * 24}px`);
|
|
document.documentElement.style.setProperty("--toast-dir", `${dir}`);
|
|
document.documentElement.style.setProperty("--toast-slide-x", `${slideX}px`);
|
|
document.documentElement.style.setProperty("--toast-slide-y", `${slideY}px`);
|
|
if (durationInput) durationInput.value = state.durationMs;
|
|
}
|
|
|
|
function applyFontSetting() {
|
|
document.documentElement.setAttribute("data-font", state.font);
|
|
if (fontSelect) fontSelect.value = state.font;
|
|
}
|
|
|
|
function persistSettings() {
|
|
try {
|
|
localStorage.setItem(TOAST_POS_KEY, state.position);
|
|
localStorage.setItem(TOAST_ANIM_KEY, state.animation);
|
|
localStorage.setItem(TOAST_SPEED_KEY, String(state.speedMs));
|
|
localStorage.setItem(TOAST_DURATION_KEY, String(state.durationMs));
|
|
localStorage.setItem(FONT_KEY, state.font);
|
|
} catch (e) {
|
|
console.warn("Toast settings save failed", e);
|
|
}
|
|
}
|
|
|
|
function loadFromStorage() {
|
|
try {
|
|
const posSaved = localStorage.getItem(TOAST_POS_KEY);
|
|
if (ALLOWED_TOAST_POS.includes(posSaved)) state.position = posSaved;
|
|
const animSaved = localStorage.getItem(TOAST_ANIM_KEY);
|
|
const migrated =
|
|
animSaved === "slide-up" || animSaved === "slide-left" || animSaved === "slide-right"
|
|
? "slide-in"
|
|
: animSaved;
|
|
if (migrated && ALLOWED_TOAST_ANIM.includes(migrated)) state.animation = migrated;
|
|
const savedSpeed = Number(localStorage.getItem(TOAST_SPEED_KEY));
|
|
if (!Number.isNaN(savedSpeed) && savedSpeed >= 100 && savedSpeed <= 3000) {
|
|
state.speedMs = savedSpeed;
|
|
}
|
|
const savedDur = Number(localStorage.getItem(TOAST_DURATION_KEY));
|
|
if (!Number.isNaN(savedDur) && savedDur >= 1000 && savedDur <= 15000) {
|
|
state.durationMs = savedDur;
|
|
}
|
|
const savedFont = localStorage.getItem(FONT_KEY);
|
|
if (ALLOWED_FONTS.includes(savedFont)) state.font = savedFont;
|
|
} catch (e) {
|
|
console.warn("Toast settings load failed", e);
|
|
}
|
|
if (posSelect) posSelect.value = state.position;
|
|
if (animSelect) animSelect.value = state.animation;
|
|
if (speedInput) speedInput.value = state.speedMs;
|
|
if (durationInput) durationInput.value = state.durationMs;
|
|
if (fontSelect) fontSelect.value = state.font;
|
|
applyToastSettings();
|
|
applyFontSetting();
|
|
}
|
|
|
|
function showToast(message, type = "info") {
|
|
if (!container || !message) return;
|
|
const t = document.createElement("div");
|
|
t.className = `toast ${type} anim-${state.animation}`;
|
|
t.textContent = message;
|
|
container.appendChild(t);
|
|
const animOn = document.documentElement.getAttribute("data-anim") !== "off";
|
|
if (!animOn) {
|
|
t.classList.add("show");
|
|
} else {
|
|
requestAnimationFrame(() => t.classList.add("show"));
|
|
}
|
|
const duration = state.durationMs;
|
|
setTimeout(() => {
|
|
const all = Array.from(container.querySelectorAll(".toast"));
|
|
const others = all.filter((el) => el !== t && !el.classList.contains("leaving"));
|
|
const first = new Map(
|
|
others.map((el) => [el, el.getBoundingClientRect()]),
|
|
);
|
|
|
|
t.classList.add("leaving");
|
|
void t.offsetHeight;
|
|
|
|
requestAnimationFrame(() => {
|
|
const second = new Map(
|
|
others.map((el) => [el, el.getBoundingClientRect()]),
|
|
);
|
|
others.forEach((el) => {
|
|
const dy = first.get(el).top - second.get(el).top;
|
|
if (Math.abs(dy) > 0.5) {
|
|
el.style.transition = "transform var(--toast-speed, 0.28s) ease";
|
|
el.style.transform = `translateY(${dy}px)`;
|
|
requestAnimationFrame(() => {
|
|
el.style.transform = "";
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
const removeDelay = animOn ? state.speedMs : 0;
|
|
setTimeout(() => {
|
|
t.classList.remove("show");
|
|
t.remove();
|
|
others.forEach((el) => (el.style.transition = ""));
|
|
}, removeDelay);
|
|
}, duration);
|
|
}
|
|
|
|
function wireControls() {
|
|
posSelect?.addEventListener("change", () => {
|
|
const val = posSelect.value;
|
|
if (ALLOWED_TOAST_POS.includes(val)) {
|
|
state.position = val;
|
|
applyToastSettings();
|
|
persistSettings();
|
|
} else {
|
|
posSelect.value = state.position;
|
|
showToast("Invalid toast position", "error");
|
|
}
|
|
});
|
|
|
|
animSelect?.addEventListener("change", () => {
|
|
let val = animSelect.value;
|
|
if (val === "slide-up" || val === "slide-left" || val === "slide-right") val = "slide-in";
|
|
if (ALLOWED_TOAST_ANIM.includes(val)) {
|
|
state.animation = val;
|
|
persistSettings();
|
|
} else {
|
|
animSelect.value = state.animation;
|
|
showToast("Invalid toast animation", "error");
|
|
}
|
|
});
|
|
|
|
const clampSpeed = (val) => clamp(val, 100, 3000);
|
|
const clampDuration = (val) => clamp(val, 1000, 15000);
|
|
|
|
speedInput?.addEventListener("input", () => {
|
|
const raw = speedInput.value;
|
|
if (raw === "") return;
|
|
const val = Number(raw);
|
|
if (Number.isNaN(val) || val < 100 || val > 3000) return;
|
|
state.speedMs = val;
|
|
applyToastSettings();
|
|
persistSettings();
|
|
});
|
|
speedInput?.addEventListener("blur", () => {
|
|
const raw = speedInput.value;
|
|
if (raw === "") {
|
|
speedInput.value = state.speedMs;
|
|
return;
|
|
}
|
|
const val = Number(raw);
|
|
if (Number.isNaN(val) || val < 100 || val > 3000) {
|
|
state.speedMs = clampSpeed(state.speedMs);
|
|
speedInput.value = state.speedMs;
|
|
showToast("Toast speed must be 100-3000 ms", "error");
|
|
return;
|
|
}
|
|
state.speedMs = val;
|
|
speedInput.value = state.speedMs;
|
|
applyToastSettings();
|
|
persistSettings();
|
|
});
|
|
|
|
durationInput?.addEventListener("input", () => {
|
|
const raw = durationInput.value;
|
|
if (raw === "") return;
|
|
const val = Number(raw);
|
|
if (Number.isNaN(val) || val < 1000 || val > 15000) return;
|
|
state.durationMs = val;
|
|
persistSettings();
|
|
});
|
|
durationInput?.addEventListener("blur", () => {
|
|
const raw = durationInput.value;
|
|
if (raw === "") {
|
|
durationInput.value = state.durationMs;
|
|
return;
|
|
}
|
|
const val = Number(raw);
|
|
if (Number.isNaN(val) || val < 1000 || val > 15000) {
|
|
state.durationMs = clampDuration(state.durationMs);
|
|
durationInput.value = state.durationMs;
|
|
showToast("Toast duration must be 1000-15000 ms", "error");
|
|
return;
|
|
}
|
|
state.durationMs = val;
|
|
durationInput.value = state.durationMs;
|
|
persistSettings();
|
|
});
|
|
|
|
fontSelect?.addEventListener("change", () => {
|
|
const val = fontSelect.value;
|
|
if (!ALLOWED_FONTS.includes(val)) {
|
|
fontSelect.value = state.font;
|
|
showToast("Invalid font choice", "error");
|
|
return;
|
|
}
|
|
state.font = val;
|
|
applyFontSetting();
|
|
persistSettings();
|
|
});
|
|
|
|
testBtn?.addEventListener("click", () => showToast("This is a test toast", "info"));
|
|
}
|
|
|
|
loadFromStorage();
|
|
wireControls();
|
|
|
|
return {
|
|
state,
|
|
showToast,
|
|
applyToastSettings,
|
|
applyFontSetting,
|
|
persistSettings,
|
|
loadFromStorage,
|
|
};
|
|
}
|