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
JavaScript
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 };
}
};