Skip to content

Commit 559083b

Browse files
committed
Make Sisyphus even smaller and call it AutoFormSaver
1 parent 8604d46 commit 559083b

File tree

4 files changed

+181
-341
lines changed

4 files changed

+181
-341
lines changed

evap/static/ts/src/auto-form-saver.ts

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* Plugin for saving form data to local storage to restore them later.
3+
*
4+
* Based on Sisyphus by Alexander Kaupanin <[email protected]>
5+
*/
6+
7+
interface AutoFormSaverOptions {
8+
name?: string;
9+
excludeFields: string[];
10+
customKeySuffix: string;
11+
onSave: (arg0: AutoFormSaver) => void;
12+
onRestore: (arg0: AutoFormSaver) => void;
13+
}
14+
15+
export class AutoFormSaver {
16+
private options: AutoFormSaverOptions;
17+
private readonly href: string;
18+
private readonly target: HTMLFormElement;
19+
20+
constructor(target: HTMLFormElement, options: AutoFormSaverOptions) {
21+
const defaults = {
22+
excludeFields: [],
23+
customKeySuffix: "",
24+
timeout: 0,
25+
onSave: function () {},
26+
onRestore: function () {},
27+
};
28+
29+
this.options = { ...defaults, ...options };
30+
31+
this.target = target;
32+
if (this.options.name) {
33+
this.href = this.options.name;
34+
} else {
35+
this.href = location.hostname + location.pathname;
36+
}
37+
38+
this.restoreAllData();
39+
40+
this.bindSaveData();
41+
}
42+
43+
findFieldsToProtect(): Element[] {
44+
return Array.of(...this.target.elements).filter((el: Element) => {
45+
if (
46+
el instanceof HTMLInputElement &&
47+
["submit", "reset", "button", "file", "password", "hidden"].includes(el.type.toLowerCase())
48+
) {
49+
return false;
50+
}
51+
if (["BUTTON", "FIELDSET", "OBJECT", "OUTPUT"].includes(el.tagName)) {
52+
return false;
53+
}
54+
return true;
55+
});
56+
}
57+
58+
getExcludeFields(): Element[] {
59+
return this.options.excludeFields.flatMap(selector => Array.from(document.querySelectorAll(selector)));
60+
}
61+
62+
getPrefix(field: Element) {
63+
return (
64+
this.href +
65+
this.target.id +
66+
this.target.name +
67+
(field.getAttribute("name") ?? "") +
68+
this.options.customKeySuffix
69+
);
70+
}
71+
72+
bindSaveData() {
73+
for (const field of this.findFieldsToProtect()) {
74+
if (this.getExcludeFields().includes(field)) {
75+
continue;
76+
}
77+
const prefix = this.getPrefix(field);
78+
if ((field instanceof HTMLInputElement && field.type === "text") || field instanceof HTMLTextAreaElement) {
79+
this.bindSaveDataImmediately(field, prefix);
80+
}
81+
this.bindSaveDataOnChange(field);
82+
}
83+
}
84+
85+
saveAllData() {
86+
for (const field of this.findFieldsToProtect()) {
87+
if (this.getExcludeFields().includes(field) || !field.getAttribute("name")) {
88+
continue;
89+
}
90+
const prefix = this.getPrefix(field);
91+
const fieldType = field.getAttribute("type");
92+
// @ts-expect-error All field objects are some kind of input field with value.
93+
let value: string | string[] | boolean = field.value;
94+
95+
if (field instanceof HTMLInputElement && fieldType === "checkbox") {
96+
value = field.checked;
97+
this.saveToBrowserStorage(prefix, value, false);
98+
} else if (field instanceof HTMLInputElement && fieldType === "radio") {
99+
if (field.checked) {
100+
value = field.value;
101+
this.saveToBrowserStorage(prefix, value, false);
102+
}
103+
} else {
104+
this.saveToBrowserStorage(prefix, value, false);
105+
}
106+
}
107+
this.options.onSave(this);
108+
}
109+
110+
restoreAllData() {
111+
let restored = false;
112+
113+
for (const field of this.findFieldsToProtect()) {
114+
if (this.getExcludeFields().includes(field)) {
115+
continue;
116+
}
117+
const storedValue = localStorage.getItem(this.getPrefix(field));
118+
if (storedValue !== null) {
119+
this.restoreFieldsData(field, storedValue);
120+
restored = true;
121+
}
122+
}
123+
124+
if (restored) {
125+
this.options.onRestore(this);
126+
}
127+
}
128+
129+
restoreFieldsData(field: Element, storedValue: string) {
130+
if (field.getAttribute("name") === undefined) {
131+
return false;
132+
}
133+
if (field instanceof HTMLInputElement && field.type === "checkbox") {
134+
field.checked = storedValue !== "false";
135+
} else if (field instanceof HTMLInputElement && field.type === "radio") {
136+
if (field.value === storedValue) {
137+
field.checked = true;
138+
}
139+
} else {
140+
// @ts-expect-error Definitely an input field with a value, but not known by type
141+
field.value = storedValue;
142+
}
143+
}
144+
145+
bindSaveDataImmediately(field: HTMLInputElement | HTMLTextAreaElement, prefix: string) {
146+
field.addEventListener("input", () => {
147+
this.saveToBrowserStorage(prefix, field.value);
148+
});
149+
}
150+
151+
saveToBrowserStorage(key: string, value: any, fireCallback?: boolean) {
152+
// if fireCallback is undefined it should be true
153+
fireCallback = fireCallback ?? true;
154+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
155+
localStorage.setItem(key, value + "");
156+
if (fireCallback && value !== "") {
157+
this.options.onSave(this);
158+
}
159+
}
160+
161+
bindSaveDataOnChange(field: Element) {
162+
field.addEventListener("change", () => {
163+
this.saveAllData();
164+
});
165+
}
166+
167+
releaseData() {
168+
for (const field of this.findFieldsToProtect()) {
169+
if (this.getExcludeFields().includes(field)) {
170+
continue;
171+
}
172+
localStorage.removeItem(this.getPrefix(field));
173+
}
174+
}
175+
}

evap/static/ts/src/sisyphus.LICENSE

-21
This file was deleted.

0 commit comments

Comments
 (0)