// ==UserScript==
// @name Docketwise Smart Form Export
// @author Zare
// @description export smart form answers to markdown/pdf and prompt to autosave unsaved changes
// @downloadURL https://raw.githubusercontent.com/hamidzr/user-scripts/refs/heads/master/userscripts/docketwise.com/smart-form-export.user.js
// @grant none
// @match https://app.docketwise.com/*/smart_forms/*/edit*
// @match https://app.docketwise.com/smart_forms/*/edit*
// @namespace https://latentbyte.com/products
// @run-at document-idle
// @updateURL https://raw.githubusercontent.com/hamidzr/user-scripts/refs/heads/master/userscripts/docketwise.com/smart-form-export.user.js
// @version 1.4.1
// ==/UserScript==
'use strict';
(() => {
// src/docketwise.com/smart-form-export.css
var smart_form_export_default = `.dw-sfe-wrap {
position: relative;
display: inline-block;
margin-right: 6px;
vertical-align: middle;
}
.dw-sfe-panel {
display: none;
position: absolute;
right: 0;
top: calc(100% + 6px);
z-index: 1050;
min-width: 210px;
padding: 12px;
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
text-align: left;
}
.dw-sfe-panel.open {
display: block;
}
.dw-sfe-opt {
display: block;
margin: 0 0 8px;
font-weight: normal;
white-space: nowrap;
cursor: pointer;
}
.dw-sfe-scope {
width: 100%;
margin-bottom: 10px;
padding: 3px 4px;
}
.dw-sfe-actions {
display: flex;
gap: 6px;
}
.dw-sfe-prompt {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 2000;
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 16px;
background: #0b3140;
color: #fff;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
font-size: 14px;
}
.dw-sfe-print-frame {
position: fixed;
right: 0;
bottom: 0;
width: 0;
height: 0;
border: 0;
}
`;
// src/lib/dom.ts
var waitForEl = (selector, timeoutMs = 5000) => {
return new Promise((resolve, reject) => {
const el = document.querySelector(selector);
if (el)
return resolve(el);
const endTime = Date.now() + timeoutMs;
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
resolve(el);
} else if (Date.now() > endTime) {
observer.disconnect();
reject(new Error(`waitForEl("${selector}") timed out after ${timeoutMs}ms`));
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
observer.disconnect();
const el = document.querySelector(selector);
if (el)
resolve(el);
else
reject(new Error(`waitForEl("${selector}") timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
};
var injectCSS = (id, css) => {
let style = document.getElementById(id);
if (style) {
style.textContent = css;
return style;
}
style = document.createElement("style");
style.id = id;
style.textContent = css;
(document.head ?? document.documentElement).appendChild(style);
return style;
};
var createElement = (tag, attrs, children) => {
const el = document.createElement(tag);
if (attrs) {
for (const [key, value] of Object.entries(attrs)) {
if (key === "className") {
el.className = value;
} else {
el[key] = value;
}
}
}
if (children) {
for (const child of children) {
if (typeof child === "string") {
el.appendChild(document.createTextNode(child));
} else {
el.appendChild(child);
}
}
}
return el;
};
// src/lib/dom-observe.ts
var DEFAULT_INIT = {
childList: true,
subtree: true
};
var observeDomChanges = (run, opts) => {
let timer = null;
let disconnectTimer = null;
let currentTarget = opts?.root ?? null;
let currentInit = opts?.observerInit ?? DEFAULT_INIT;
const disconnect = () => {
if (timer) {
window.clearTimeout(timer);
timer = null;
}
if (disconnectTimer) {
window.clearTimeout(disconnectTimer);
disconnectTimer = null;
}
observer.disconnect();
};
const refreshDisconnectTimer = () => {
if (disconnectTimer)
window.clearTimeout(disconnectTimer);
if (!opts?.disconnectAfterMs)
return;
disconnectTimer = window.setTimeout(() => {
disconnect();
}, opts.disconnectAfterMs);
};
const scheduleRun = () => {
if (timer)
window.clearTimeout(timer);
const debounceMs = opts?.debounceMs ?? 0;
if (debounceMs <= 0) {
run();
refreshDisconnectTimer();
return;
}
timer = window.setTimeout(() => {
timer = null;
run();
refreshDisconnectTimer();
}, debounceMs);
};
const observer = new MutationObserver((mutations) => {
if (opts?.shouldRun && !opts.shouldRun(mutations))
return;
scheduleRun();
});
const observe = (target, options) => {
const nextTarget = target ?? currentTarget ?? document.body;
if (!nextTarget)
return;
currentTarget = nextTarget;
currentInit = options ?? currentInit;
observer.disconnect();
observer.observe(nextTarget, currentInit);
refreshDisconnectTimer();
};
if (opts?.runImmediately)
scheduleRun();
return {
disconnect,
observe
};
};
// src/lib/download.ts
var stringify = (val) => {
if (typeof val === "string")
return val;
if (Array.isArray(val))
return val.map(stringify).join(`
`);
if (val instanceof HTMLElement)
return val.innerText;
return JSON.stringify(val);
};
var download = (filename, text) => {
const content = stringify(text);
const el = document.createElement("a");
el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(content));
el.setAttribute("download", filename);
el.style.display = "none";
document.body.appendChild(el);
el.click();
document.body.removeChild(el);
};
// src/lib/store.ts
var LOCAL_STORAGE_ADAPTER = {
name: "localStorage",
readText: (key) => {
return localStorage.getItem(key);
},
writeText: (key, value) => {
localStorage.setItem(key, value);
}
};
var getAdapters = (adapters) => {
return adapters?.length ? adapters : [LOCAL_STORAGE_ADAPTER];
};
var reportError = (opts, error, ctx) => {
opts?.onError?.(error, ctx);
};
var readRawStore = (key, opts) => {
for (const adapter of getAdapters(opts?.adapters)) {
try {
return adapter.readText(key);
} catch (error) {
reportError(opts, error, {
action: "read",
adapterName: adapter.name,
key
});
}
}
return null;
};
var writeTextStore = (key, value, opts) => {
for (const adapter of getAdapters(opts?.adapters)) {
try {
adapter.writeText(key, value);
return true;
} catch (error) {
reportError(opts, error, {
action: "write",
adapterName: adapter.name,
key
});
}
}
return false;
};
var readJsonStore = (key, fallback, opts) => {
const raw = readRawStore(key, opts);
if (raw === null)
return fallback;
try {
const parsed = JSON.parse(raw);
return opts?.parse ? opts.parse(parsed) : parsed;
} catch (error) {
reportError(opts, error, { action: "parse", key });
return fallback;
}
};
var writeJsonStore = (key, value, opts) => {
try {
return writeTextStore(key, JSON.stringify(value), opts);
} catch (error) {
reportError(opts, error, { action: "stringify", key });
return false;
}
};
// src/lib/text.ts
var escapeHtml = (text) => String(text).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
var normalizeWhitespace = (value) => (value ?? "").replace(/\s+/g, " ").trim();
// src/docketwise.com/smart-form-export.helpers.ts
var SSN_PLACEHOLDER = "[SSN redacted]";
var DASHED_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/g;
var ssnVariants = (raw) => {
const digits = raw.replace(/\D/g, "");
if (digits.length !== 9)
return [];
const [a, b, c] = [digits.slice(0, 3), digits.slice(3, 5), digits.slice(5)];
return [digits, `${a}-${b}-${c}`, `${a} ${b} ${c}`];
};
var redactSsn = (text, known) => {
let out = text;
for (const variant of known.flatMap(ssnVariants))
out = out.split(variant).join(SSN_PLACEHOLDER);
return out.replace(DASHED_SSN_RE, SSN_PLACEHOLDER);
};
var prepareDocument = (doc, opts) => {
const scrub = (s) => opts.ssn ? s : redactSsn(s, doc.ssnValues);
const fields = (list) => list.filter((f) => opts.ssn || !f.ssn || !f.value).map((f) => ({ ...f, label: scrub(f.label), value: scrub(f.value) }));
const ssnOnly = (b) => !opts.ssn && b.kind === "question" && b.fields.length > 0 && b.fields.every((f) => f.ssn && f.value);
const block = (b) => b.kind === "heading" ? { ...b, text: scrub(b.text) } : {
...b,
text: scrub(b.text),
fields: fields(b.fields),
entries: b.entries.map(fields).filter((e) => e.length),
comments: opts.comments ? b.comments.map((c) => ({ ...c, author: scrub(c.author), text: scrub(c.text) })) : []
};
return {
...doc,
title: scrub(doc.title),
sections: doc.sections.map((s) => ({
title: scrub(s.title),
blocks: s.blocks.filter((b) => !ssnOnly(b)).map(block)
})),
ssnValues: []
};
};
var escapeMd = (s) => s.replace(/([\\`*_[\]<>|])/g, "\\$1");
var fieldMd = (f, indent = "") => `${indent}- ${f.label ? `${escapeMd(f.label)}: ` : ""}${f.value ? escapeMd(f.value) : "_(blank)_"}`;
var questionMd = (q) => {
const out = [`**${escapeMd(q.text)}**`, ""];
if (!q.fields.length && !q.entries.length)
out.push("_No answer_");
out.push(...q.fields.map((f) => fieldMd(f)));
q.entries.forEach((entry, i) => {
out.push(`- Entry ${i + 1}`);
out.push(...entry.map((f) => fieldMd(f, " ")));
});
for (const c of q.comments) {
out.push("");
out.push(`> **${escapeMd(c.author)}** (${escapeMd(c.date)}):`);
out.push(...c.text.split(`
`).map((line) => `> ${escapeMd(line)}`));
}
out.push("");
return out;
};
var toMarkdown = (doc) => {
const out = [`# ${escapeMd(doc.title)}`, "", `_Exported ${doc.exportedAt} from ${doc.url}_`, ""];
for (const section of doc.sections) {
out.push(`## ${escapeMd(section.title)}`, "");
for (const b of section.blocks) {
if (b.kind === "heading")
out.push(`### ${escapeMd(b.text)}`, "");
else
out.push(...questionMd(b));
}
}
return out.join(`
`).replace(/\n{3,}/g, `
`).trimEnd() + `
`;
};
var PRINT_CSS = `
body { font: 11pt/1.45 -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif; color: #111; margin: 0; }
h1 { font-size: 18pt; margin: 0 0 4px; }
.meta { color: #666; font-size: 9pt; margin-bottom: 18px; }
h2 { font-size: 14pt; border-bottom: 1px solid #ccc; padding-bottom: 3px; margin: 22px 0 10px; break-after: avoid; }
h3 { font-size: 12pt; margin: 16px 0 8px; break-after: avoid; }
.q { margin: 0 0 12px; break-inside: avoid; }
.q-text { font-weight: 600; margin-bottom: 3px; }
ul { margin: 2px 0; padding-left: 20px; }
.label { color: #555; }
.empty { color: #999; font-style: italic; }
.entry { margin-top: 3px; }
.comment { border-left: 3px solid #0099b2; background: #f3f9fa; padding: 4px 8px; margin: 6px 0 0 4px; font-size: 10pt; white-space: pre-wrap; }
.comment b { font-weight: 600; }
.comment .date { color: #666; }
@page { margin: 16mm 14mm; }
`;
var fieldHtml = (f) => `
${f.label ? `${escapeHtml(f.label)}: ` : ""}${f.value ? escapeHtml(f.value) : '(blank)'}`;
var questionHtml = (q) => {
const parts = [`${escapeHtml(q.text)}
`];
if (!q.fields.length && !q.entries.length)
parts.push('
No answer
');
if (q.fields.length)
parts.push(`
${q.fields.map(fieldHtml).join("")}
`);
if (q.entries.length) {
const items = q.entries.map((entry, i) => `
Entry ${i + 1}${entry.map(fieldHtml).join("")}
`);
parts.push(`
`);
}
for (const c of q.comments) {
parts.push(``);
}
parts.push("
");
return parts.join("");
};
var toHtml = (doc) => {
const body = doc.sections.map((s) => {
const blocks = s.blocks.map((b) => b.kind === "heading" ? `${escapeHtml(b.text)}
` : questionHtml(b));
return `${escapeHtml(s.title)}
${blocks.join("")}`;
}).join("");
return `${escapeHtml(doc.title)}${escapeHtml(doc.title)}
Exported ${escapeHtml(doc.exportedAt)} from ${escapeHtml(doc.url)}
${body}`;
};
var MAX_PART_LEN = 80;
var fileSafe = (s) => s.replace(/[\\/:*?"<>|\x00-\x1f.\s]+/g, "-").replace(/-{2,}/g, "-").slice(0, MAX_PART_LEN).replace(/^-+|-+$/g, "");
var localDate = (d) => [d.getFullYear(), d.getMonth() + 1, d.getDate()].map((n) => String(n).padStart(2, "0")).join("-");
var exportFileBase = (parts, date) => [
fileSafe(parts.title) || "Smart-Form",
parts.section && fileSafe(parts.section),
parts.ssn && "with-SSN",
localDate(date)
].filter(Boolean).join("_").toLowerCase();
// src/docketwise.com/smart-form-extract.ts
var sels = {
pane: "#form-builder .tab-pane",
question: ".question-container",
questionWrap: ".question-and-comment-container",
comment: ".dw-comment-container",
entryRemove: "a.remove_nested_fields",
fieldGroup: ".floating-label-input-group",
fieldLabel: "label.floating-label"
};
var BUTTON_ROW = ".button-row-container";
var ACTIVE_BUTTON = "button.easyguide-button.active";
var getVm = () => window.smartFormBuilder?.form ?? null;
var ownText = (el) => normalizeWhitespace([...el.childNodes].filter((n) => n.nodeType === Node.TEXT_NODE).map((n) => n.textContent).join(" "));
var promptText = (p) => {
const clone = p.cloneNode(true);
clone.querySelectorAll(".question-icons-div").forEach((el) => el.remove());
return normalizeWhitespace(clone.textContent);
};
var isHiddenIn = (el, pane) => {
if (el.closest(".modal"))
return true;
for (let e = el;e && e !== pane; e = e.parentElement) {
if (getComputedStyle(e).display === "none")
return true;
}
return false;
};
var fieldLabel = (el) => {
const label = el.closest(sels.fieldGroup)?.querySelector(sels.fieldLabel);
return label ? normalizeWhitespace(label.textContent) : "";
};
var selectLabel = (el) => {
const label = fieldLabel(el);
if (label)
return label;
if (el.classList.contains("addon-select"))
return "Unit type";
return el.options.length > 100 ? "Country" : "";
};
var DIGIT_PART_RE = /^(.*)_(hundreds|tens|ones)\]$/;
var readDigitGroup = (el) => {
const m = DIGIT_PART_RE.exec(el.name);
if (!m || m[2] !== "hundreds")
return null;
const scope = el.closest(sels.question) ?? document;
const part = (p) => scope.querySelector(`input[name="${m[1]}_${p}]"]`)?.value.trim() ?? "";
const digits = `${el.value.trim()}${part("tens")}${part("ones")}`.replace(/^0+(?=\d)/, "");
const unit = normalizeWhitespace(scope.querySelector(".input-group-addon")?.textContent);
return { label: "", value: digits && unit ? `${digits} ${unit}` : digits };
};
var isSsnInput = (input) => /\[ssn\]$/.test(input.name) || /social security/i.test(fieldLabel(input));
var buttonPrompt = (el, mainP) => {
const prev = el.closest(BUTTON_ROW)?.previousElementSibling;
if (!prev || prev === mainP || !prev.matches("p, label"))
return "";
return ownText(prev);
};
var isUnansweredRadioGroup = (el) => {
if (!el.name)
return false;
const group = [
...document.querySelectorAll(`input[type=radio][name="${CSS.escape(el.name)}"]`)
];
return group[0] === el && !group.some((r) => r.checked);
};
var readField = (el, mainP) => {
if (el.matches(BUTTON_ROW)) {
if (!el.querySelector("button.easyguide-button") || el.querySelector(ACTIVE_BUTTON))
return null;
return { label: buttonPrompt(el, mainP), value: "" };
}
if (el instanceof HTMLButtonElement) {
return { label: buttonPrompt(el, mainP), value: normalizeWhitespace(el.textContent) };
}
if (el instanceof HTMLSelectElement) {
const opt = el.selectedOptions[0];
const value = opt && opt.value !== "" ? normalizeWhitespace(opt.textContent) : "";
return { label: selectLabel(el), value };
}
if (el instanceof HTMLTextAreaElement) {
return { label: fieldLabel(el), value: el.value.trim() };
}
if (el instanceof HTMLInputElement) {
if (el.type === "checkbox") {
if (!el.checked)
return null;
const value = normalizeWhitespace(el.closest("label")?.textContent);
return value ? { label: "", value } : null;
}
if (el.type === "radio") {
if (!el.checked)
return isUnansweredRadioGroup(el) ? { label: "", value: "" } : null;
const value = normalizeWhitespace(el.closest("label")?.textContent);
return { label: "", value: value === "Blank" ? "" : value };
}
if (["hidden", "button", "submit", "file", "search"].includes(el.type))
return null;
if (DIGIT_PART_RE.test(el.name))
return readDigitGroup(el);
const raw = el.parentElement?.querySelector('input[type=hidden][name$="[ssn]"]');
const ssn = !!raw || isSsnInput(el);
const value = (raw?.value || el.value).trim();
return { label: fieldLabel(el), value, ...ssn ? { ssn } : {} };
}
const value = ownText(el);
return value ? { label: fieldLabel(el), value } : null;
};
var FIELD_SEL = `${BUTTON_ROW}, ${ACTIVE_BUTTON}, input, select, textarea, .populator-tag`;
var readFields = (scope, pane, mainP, skip) => [...scope.querySelectorAll(FIELD_SEL)].filter((el) => !isHiddenIn(el, pane) && !skip.some((s) => s !== scope && s.contains(el))).map((el) => readField(el, mainP)).filter((f) => !!f);
var readComments = (qc) => {
const wrap = qc.closest(sels.questionWrap);
if (!wrap)
return [];
return [...wrap.querySelectorAll(sels.comment)].filter((c) => !qc.contains(c)).map((c) => ({
author: normalizeWhitespace(c.querySelector(".dw-comment__commenter")?.textContent),
date: normalizeWhitespace(c.querySelector(".dw-comment__date")?.textContent),
text: (c.querySelector(".dw-comment_content")?.textContent ?? "").trim()
})).filter((c) => c.text);
};
var readQuestion = (qc, pane) => {
if (qc.querySelector("input[name^=yes_to_all]"))
return null;
const mainP = qc.querySelector("p");
const text = mainP ? promptText(mainP) : "";
const entries = [...qc.querySelectorAll(sels.entryRemove)].map((a) => a.parentElement).filter((e) => !!e && !isHiddenIn(e, pane));
return {
kind: "question",
text: text || "(untitled question)",
fields: readFields(qc, pane, mainP, entries),
entries: entries.map((e) => readFields(e, pane, mainP, [])).filter((f) => f.length),
comments: readComments(qc)
};
};
var readSection = (pane) => {
const title = normalizeWhitespace(pane.querySelector("h4")?.textContent) || pane.id;
const blocks = [...pane.querySelectorAll(`h3, ${sels.question}`)].filter((el) => !isHiddenIn(el, pane) && !el.parentElement?.closest(sels.question)).map((el) => el.matches(sels.question) ? readQuestion(el, pane) : { kind: "heading", text: normalizeWhitespace(el.textContent) }).filter((b) => !!b && (b.kind === "question" || !!b.text));
return { title, blocks };
};
var readDocument = (scope) => {
const panes = [...document.querySelectorAll(sels.pane)].filter((p) => scope === "all" || p.classList.contains("active"));
const ssnValues = [...document.querySelectorAll('input[name$="[ssn]"]')].map((i) => i.value).filter(Boolean);
return {
title: getVm()?.name || document.title,
url: location.href,
exportedAt: new Date().toLocaleString(),
sections: panes.map(readSection).filter((s) => s.blocks.length),
ssnValues
};
};
// src/docketwise.com/smart-form-export.user.ts
var STYLE_ID = "dw-sfe-styles";
var OPTS_KEY = "dw-smart-form-export:opts";
var DIRTY_PROMPT_MS = 30000;
var COUNTDOWN_S = 10;
var TYPING_IDLE_MS = 1500;
var sels2 = {
titleBar: ".easyguide-page-title",
btnGroup: ".easyguide-page-title .btn-group",
loader: ".loader-wrapper"
};
var DEFAULT_OPTS = { comments: true, ssn: false, scope: "all" };
var buildExport = (opts) => {
const doc = prepareDocument(readDocument(opts.scope), opts);
const section = opts.scope === "current" ? doc.sections[0]?.title : undefined;
return { doc, base: exportFileBase({ title: doc.title, section, ssn: opts.ssn }, new Date) };
};
var exportMarkdown = (opts) => {
const { doc, base } = buildExport(opts);
download(`${base}.md`, toMarkdown(doc));
};
var exportPdf = (opts) => {
const { doc, base } = buildExport(opts);
const frame = createElement("iframe", { className: "dw-sfe-print-frame" });
frame.srcdoc = toHtml({ ...doc, title: base });
const prevTitle = document.title;
const cleanup = () => {
document.title = prevTitle;
frame.remove();
};
frame.addEventListener("load", () => {
const win = frame.contentWindow;
if (!win)
return cleanup();
document.title = base;
win.addEventListener("afterprint", () => setTimeout(cleanup, 500));
win.focus();
win.print();
});
document.body.appendChild(frame);
};
var checkbox = (label, checked, onChange) => {
const input = createElement("input", { type: "checkbox", checked });
input.addEventListener("change", () => onChange(input.checked));
return createElement("label", { className: "dw-sfe-opt" }, [input, ` ${label}`]);
};
var buildPanel = () => {
const opts = readJsonStore(OPTS_KEY, DEFAULT_OPTS);
const save = () => writeJsonStore(OPTS_KEY, opts);
const scopeSel = createElement("select", { className: "dw-sfe-scope" }, [
createElement("option", { value: "all", textContent: "All sections" }),
createElement("option", { value: "current", textContent: "Current section" })
]);
scopeSel.value = opts.scope;
scopeSel.addEventListener("change", () => {
opts.scope = scopeSel.value === "current" ? "current" : "all";
save();
});
const action = (label, fn) => {
const btn = createElement("button", {
type: "button",
className: "btn btn-default btn-sm",
textContent: label
});
btn.addEventListener("click", () => {
try {
fn();
} catch (e) {
console.error("[dw-sfe] export failed", e);
}
panel.classList.remove("open");
});
return btn;
};
const panel = createElement("div", { className: "dw-sfe-panel" }, [
checkbox("Include comments", opts.comments, (v) => {
opts.comments = v;
save();
}),
checkbox("Include SSN", opts.ssn, (v) => {
opts.ssn = v;
save();
}),
scopeSel,
createElement("div", { className: "dw-sfe-actions" }, [
action("Markdown", () => exportMarkdown(opts)),
action("PDF", () => exportPdf(opts))
])
]);
const toggle = createElement("a", { className: "btn btn-default btn-sm", textContent: "EXPORT" });
toggle.addEventListener("click", (e) => {
e.preventDefault();
panel.classList.toggle("open");
});
document.addEventListener("click", (e) => {
if (!wrap.contains(e.target))
panel.classList.remove("open");
});
const wrap = createElement("div", { className: "dw-sfe-wrap" }, [toggle, panel]);
return wrap;
};
var ensurePanel = (panel) => {
if (panel.isConnected)
return;
const group = document.querySelector(sels2.btnGroup);
group?.parentElement?.insertBefore(panel, group);
};
var findSaveBtn = () => [...document.querySelectorAll(`${sels2.titleBar} a`)].find((a) => normalizeWhitespace(a.textContent).toUpperCase() === "SAVE FORM") ?? null;
var isBusy = () => {
const loader = document.querySelector(sels2.loader);
return !!loader && getComputedStyle(loader).display !== "none";
};
var startAutosave = () => {
let dirtySince = null;
let countdown = null;
let lastTypedAt = 0;
let loop = null;
const msg = createElement("span");
const saveNow = createElement("button", {
type: "button",
className: "btn btn-primary btn-sm",
textContent: "Save now"
});
const cancel = createElement("button", {
type: "button",
className: "btn btn-default btn-sm",
textContent: "Cancel"
});
const prompt = createElement("div", { className: "dw-sfe-prompt" }, [
msg,
createElement("div", { className: "dw-sfe-actions" }, [saveNow, cancel])
]);
const hide = () => {
countdown = null;
prompt.remove();
};
const trySave = () => {
const btn = findSaveBtn();
if (!btn || isBusy() || Date.now() - lastTypedAt < TYPING_IDLE_MS) {
msg.textContent = "Unsaved changes. Saving when you pause typing...";
return;
}
hide();
dirtySince = Date.now();
btn.click();
};
saveNow.addEventListener("click", () => {
lastTypedAt = 0;
trySave();
});
cancel.addEventListener("click", () => {
hide();
dirtySince = Date.now();
});
const tick = () => {
const vm = getVm();
const dirty = vm?.submit === true && vm.tabs?.active !== "settings";
if (!dirty) {
dirtySince = null;
hide();
if (loop !== null)
window.clearInterval(loop);
loop = null;
return;
}
dirtySince ??= Date.now();
if (countdown !== null) {
countdown = Math.max(0, countdown - 1);
if (countdown === 0)
trySave();
else
msg.textContent = `Unsaved changes. Auto-saving in ${countdown}s`;
return;
}
if (Date.now() - dirtySince >= DIRTY_PROMPT_MS && !isBusy()) {
countdown = COUNTDOWN_S;
msg.textContent = `Unsaved changes. Auto-saving in ${countdown}s`;
document.body.appendChild(prompt);
}
};
const wake = () => {
loop ??= window.setInterval(tick, 1000);
};
document.addEventListener("keydown", () => {
lastTypedAt = Date.now();
wake();
}, true);
["input", "change", "click"].forEach((type) => document.addEventListener(type, wake, true));
wake();
};
var main = async () => {
injectCSS(STYLE_ID, smart_form_export_default);
await waitForEl(sels2.btnGroup, 20000);
const panel = buildPanel();
ensurePanel(panel);
observeDomChanges(() => ensurePanel(panel), { shouldRun: () => !panel.isConnected }).observe(document.body);
startAutosave();
};
main().catch((e) => console.warn("[dw-sfe]", e));
})();