UNPKG

persist-ui

Version:

Universal input persistence for Vanilla, React, Vue, Astro, Next.js etc. with secure AES encryption.

184 lines (183 loc) 6.36 kB
import CryptoJS from "crypto-js"; const ENCRYPT_PREFIX = 'persistUI::enc::'; function encrypt(data, secret) { try { const encrypted = CryptoJS.AES.encrypt(data, secret).toString(); return ENCRYPT_PREFIX + encrypted; } catch (err) { console.error("persistUI: Encryption failed", err); return data; // fallback to unencrypted data } } function decrypt(data, secret) { if (!data || !data.startsWith(ENCRYPT_PREFIX)) return data; try { const encrypted = data.replace(ENCRYPT_PREFIX, ''); const bytes = CryptoJS.AES.decrypt(encrypted, secret); const decrypted = bytes.toString(CryptoJS.enc.Utf8); return decrypted; } catch (err) { console.error("persistUI: Decryption failed", err); return ""; // return empty string on decryption failure } } function safeGet(key) { try { return localStorage.getItem(key); } catch { return null; } } function safeSet(key, value) { try { localStorage.setItem(key, value); } catch { } } function safeRemove(key) { try { localStorage.removeItem(key); } catch { } } export let persistUI = { attach(selectors, options) { const nodes = Array.isArray(selectors) ? selectors.map(sel => typeof sel === "string" ? document.querySelector(sel) : sel).filter(Boolean) : typeof selectors === "string" ? [document.querySelector(selectors)].filter(Boolean) : [selectors].filter(Boolean); const ignoreTypes = options.ignoreTypes || []; const debounce = options.debounce ?? 0; const encryptFlag = options.encrypt ?? false; const secret = encryptFlag ? (options.encryptionSecret || options.key) : ""; let timer = null; function save() { const data = {}; for (const node of nodes) { if (node.type && ignoreTypes.includes(node.type)) continue; const key = node.id || node.name || node.tagName; if (node instanceof HTMLInputElement) { if (node.type === "checkbox" || node.type === "radio") { data[key] = node.checked; } else { data[key] = node.value; } } else if (node instanceof HTMLTextAreaElement) { data[key] = node.value; } else if (node instanceof HTMLSelectElement) { data[key] = node.value; } else if ("checked" in node) { data[key] = node.checked; } } let str = JSON.stringify(data); if (encryptFlag) str = encrypt(str, secret); safeSet(options.key, str); } function restore() { const raw = safeGet(options.key); if (!raw) return; let data; try { const json = encryptFlag ? decrypt(raw, secret) : raw; data = JSON.parse(json); } catch { return; } for (const node of nodes) { const key = node.id || node.name || node.tagName; if (data[key] !== undefined) { if (node instanceof HTMLInputElement) { if (node.type === "checkbox" || node.type === "radio") { node.checked = !!data[key]; } else { node.value = data[key]; } } else if (node instanceof HTMLTextAreaElement) { node.value = data[key]; } else if (node instanceof HTMLSelectElement) { node.value = data[key]; } else if ("checked" in node) { node.checked = !!data[key]; } } } if (typeof options.onRestore === "function") options.onRestore(data); } function reset() { safeRemove(options.key); for (const node of nodes) { if (node instanceof HTMLInputElement) { if (node.type === "checkbox" || node.type === "radio") { node.checked = false; } else { node.value = ""; } } else if (node instanceof HTMLTextAreaElement) { node.value = ""; } else if (node instanceof HTMLSelectElement) { node.value = ""; } else if ("checked" in node) { node.checked = false; } } } nodes.forEach(node => { const handler = () => { if (debounce > 0) { if (timer) clearTimeout(timer); timer = setTimeout(save, debounce); } else { save(); } }; node.addEventListener("input", handler); node.addEventListener("change", handler); }); restore(); if (options.resetSelector) { const resetBtn = document.querySelector(options.resetSelector); if (resetBtn) resetBtn.addEventListener("click", reset); } if (options.clearOnSubmit && options.submitSelector) { const submitBtn = document.querySelector(options.submitSelector); if (submitBtn) { submitBtn.addEventListener("click", () => { safeRemove(options.key); }); } } window.addEventListener("beforeunload", () => { if (options.clearOnSubmit) { safeRemove(options.key); } }); return { save, restore, reset }; } };