From aa8894df5940e9cb9e37d724792eb4124a917464 Mon Sep 17 00:00:00 2001 From: Dmitry Belan Date: Thu, 24 Sep 2026 21:48:11 +0300 Subject: [PATCH] Add autosave script for forms to prevent data loss on reload --- lmsege/lms/static/js/autosave.js | 128 +++++++++++++++++++++++++++++ lmsege/lms/templates/lms/base.html | 2 + lmsege/lmsege/settings.py | 3 + 3 files changed, 133 insertions(+) create mode 100644 lmsege/lms/static/js/autosave.js diff --git a/lmsege/lms/static/js/autosave.js b/lmsege/lms/static/js/autosave.js new file mode 100644 index 0000000..2134fff --- /dev/null +++ b/lmsege/lms/static/js/autosave.js @@ -0,0 +1,128 @@ +document.addEventListener('DOMContentLoaded', function() { + // Unique key for the current page URL + const pageKey = window.location.pathname + window.location.search; + const STORAGE_KEY = 'autosave_' + pageKey; + // Expiration time: 12 hours + const EXPIRATION_MS = 12 * 60 * 60 * 1000; + + function saveFormData() { + const forms = document.querySelectorAll('form'); + let formData = {}; + let hasData = false; + + forms.forEach((form, formIndex) => { + const inputs = form.querySelectorAll('input:not([type="password"]):not([type="hidden"]):not([type="submit"]):not([type="file"]), textarea, select'); + let formState = {}; + inputs.forEach(input => { + // Ignore inputs without a name + if (!input.name) return; + + if (input.type === 'checkbox' || input.type === 'radio') { + formState[input.name] = input.checked; + } else { + formState[input.name] = input.value; + } + }); + if (Object.keys(formState).length > 0) { + formData[formIndex] = formState; + hasData = true; + } + }); + + if (hasData) { + const dataToSave = { + timestamp: new Date().getTime(), + data: formData + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(dataToSave)); + } + } + + function loadFormData() { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + try { + const parsed = JSON.parse(saved); + const now = new Date().getTime(); + + // Discard data if it's older than expiration time + if (now - parsed.timestamp > EXPIRATION_MS) { + localStorage.removeItem(STORAGE_KEY); + return; + } + + const formData = parsed.data; + const forms = document.querySelectorAll('form'); + + forms.forEach((form, formIndex) => { + if (formData[formIndex]) { + const formState = formData[formIndex]; + const inputs = form.querySelectorAll('input:not([type="password"]):not([type="hidden"]):not([type="submit"]):not([type="file"]), textarea, select'); + + inputs.forEach(input => { + if (input.name && formState[input.name] !== undefined) { + if (input.type === 'checkbox' || input.type === 'radio') { + // Set checked state + if (input.checked !== formState[input.name]) { + input.checked = formState[input.name]; + } + } else { + // Set value + if (input.value !== formState[input.name]) { + input.value = formState[input.name]; + } + } + } + }); + } + }); + + // Show a small non-intrusive toast notification + showToast("Восстановлены несохраненные данные"); + } catch (e) { + console.error("Error parsing autosave data", e); + } + } + } + + function showToast(message) { + const toast = document.createElement('div'); + toast.textContent = message; + toast.style.position = 'fixed'; + toast.style.bottom = '20px'; + toast.style.right = '20px'; + toast.style.backgroundColor = '#494bd6'; + toast.style.color = 'white'; + toast.style.padding = '10px 20px'; + toast.style.borderRadius = '5px'; + toast.style.zIndex = '9999'; + toast.style.fontFamily = '"Plus Jakarta Sans", sans-serif'; + toast.style.fontSize = '12px'; + toast.style.boxShadow = '0 4px 6px rgba(0,0,0,0.3)'; + toast.style.transition = 'opacity 0.5s ease'; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = '0'; + setTimeout(() => toast.remove(), 500); + }, 3000); + } + + // Load data on page load + loadFormData(); + + // Debounce function for saving data so it doesn't trigger on every single keystroke excessively + let timeout; + document.addEventListener('input', function(e) { + if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) { + clearTimeout(timeout); + timeout = setTimeout(saveFormData, 500); + } + }); + + // Clear saved data on successful form submission + document.addEventListener('submit', function(e) { + localStorage.removeItem(STORAGE_KEY); + }); +}); diff --git a/lmsege/lms/templates/lms/base.html b/lmsege/lms/templates/lms/base.html index d5f4b4d..c639ca2 100644 --- a/lmsege/lms/templates/lms/base.html +++ b/lmsege/lms/templates/lms/base.html @@ -143,5 +143,7 @@ + {% load static %} + diff --git a/lmsege/lmsege/settings.py b/lmsege/lmsege/settings.py index 59693f9..71c52cd 100644 --- a/lmsege/lmsege/settings.py +++ b/lmsege/lmsege/settings.py @@ -64,6 +64,9 @@ UNFOLD = { "STYLES": [ lambda request: "/static/css/unfold_custom.css", ], + "SCRIPTS": [ + lambda request: "/static/js/autosave.js", + ], "COLORS": { "primary": { "50": "#f2f2ff",