Implement local save and load buttons #28

Merged
mslw merged 15 commits from local-save into master 2022-06-29 09:20:26 +00:00
2 changed files with 301 additions and 6 deletions

4
.prettierrc.yaml Normal file
View file

@ -0,0 +1,4 @@
trailingComma: "es5"
tabWidth: 4
semi: true
singleQuote: false

View file

@ -2266,6 +2266,18 @@
<div class="row pt-5"> <div class="row pt-5">
<button class="btn btn-primary btn-lg btn-block" type="submit" id="submit-button" formaction="https://sfb1451.inm7.de/store-data" formmethod="post" disabled>Daten speichern</button> <button class="btn btn-primary btn-lg btn-block" type="submit" id="submit-button" formaction="https://sfb1451.inm7.de/store-data" formmethod="post" disabled>Daten speichern</button>
</div> </div>
<div class="row pt-5">
<!-- Eine lokale Kopie der Formulardaten speichern -->
<button class="btn btn-primary btn-lg btn-block" type="button" id="local-save-button">Daten lokal speichern</button>
</div>
<div class="row pt-5">
<!-- Eine lokale Kopie der Formulardaten laden -->
<input type="file" id="local-load-input" accept=".json" style="display:none">
<button class="btn btn-primary btn-lg btn-block" type="button" id="local-load-button">Lokale Daten laden</button>
</div>
</div> </div>
@ -2681,7 +2693,7 @@
return ""; return "";
} }
function getContentString() { function getContentArray() {
let nameArray = [ let nameArray = [
["form-data-version", getStringContent], ["form-data-version", getStringContent],
["data-entry-domain", getStringContent], ["data-entry-domain", getStringContent],
@ -2777,18 +2789,28 @@
["additional-blood-sampling-url", getStringContent], ["additional-blood-sampling-url", getStringContent],
["additional-remarks", getStringContent] ["additional-remarks", getStringContent]
]; ];
let valueArray = nameArray.map(spec => { let contentArray = nameArray.map(spec => {
if (spec.length === 2) { if (spec.length === 2) {
return spec[0] + ":" + spec[1].apply(null, [spec[0]]); return [spec[0], spec[1].apply(null, [spec[0]])];
} }
return spec[0] + ":" + spec[1].apply(null, [spec[2]]); return [spec[0], spec[1].apply(null, [spec[2]])];
}); });
return contentArray;
}
let contentString = valueArray.join(";"); function getContentString() {
console.log("contentString: " + contentString); let keyValueArray = getContentArray();
let contentString = Array.from(keyValueArray, kvpair => kvpair.join(":")).join(";");
return contentString; return contentString;
} }
function getContentObj() {
let keyValueArray = getContentArray();
let contentObj = Object.fromEntries(keyValueArray);
contentObj['subject-group'] = getStringContent('subject-group'); // not present in getContentString
return contentObj;
}
async function digestMessage(message) { async function digestMessage(message) {
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message
@ -3070,9 +3092,278 @@
updatedLines(); updatedLines();
christian-monch commented 2022-06-27 12:15:26 +00:00 (Migrated from github.com)

If subject-pseudonym is not set, fname will be .json (there is also a comment in the PR discussion). Maybe decline to save without subject-pseudonym or ask for a value?

If `subject-pseudonym` is not set, `fname` will be `.json` (there is also a comment in the PR discussion). Maybe decline to save without `subject-pseudonym` or ask for a value?
christian-monch commented 2022-06-27 12:17:03 +00:00 (Migrated from github.com)

Maybe remove element a again?

Maybe remove element `a` again?
christian-monch commented 2022-06-27 14:13:02 +00:00 (Migrated from github.com)

Generally it would be nice if you could unify tab and space, more precisly, replace tabs with 4 spaces

Generally it would be nice if you could unify tab and space, more precisly, replace tabs with 4 spaces
mslw commented 2022-06-28 13:17:05 +00:00 (Migrated from github.com)

I'm going for "decline to save" - it makes little sense to save records not attributed to any subject.

I also don't know an easy way to ask for value. I found showSaveFilePicker in JS, but it's new and currently supported only by Chromium-based browsers.

I'm going for "decline to save" - it makes little sense to save records not attributed to any subject. I also don't know an easy way to ask for value. I found [showSaveFilePicker](https://developer.mozilla.org/en-US/docs/Web/API/Window/showSaveFilePicker) in JS, but it's new and currently supported only by Chromium-based browsers.
mslw commented 2022-06-28 13:32:25 +00:00 (Migrated from github.com)

I thought I knew my editor, but apparently not... I'll clean it up (probably I'll run prettier on the script blocks).

I thought I knew my editor, but apparently not... I'll clean it up (probably I'll run prettier on the script blocks).
</script>
<script>
// Local save
const lsb = document.getElementById("local-save-button");
lsb.onclick = function () {
let savedObject = getContentObj();
// refuse to save without subject pseudonym
if (savedObject["subject-pseudonym"] === "") {
window.alert(
"Vor dem Speichern bitte Probanden-Pseudonym eingeben"
);
return false;
}
// save using pseudonym as filename
let fname = savedObject["subject-pseudonym"] + ".json";
var bb = new Blob([JSON.stringify(savedObject)], {
type: "text/json;charset=utf-8",
});
var a = document.createElement("a");
a.setAttribute("download", fname);
a.setAttribute("href", URL.createObjectURL(bb));
a.click();
a.remove();
return false;
};
</script>
<script>
// Local load
function resetTheForm(fieldsToWrite) {
// call reset() to clear the form
document.getElementById("entry-form").reset();
// while reset affects the sliders used to enable/disable fields,
// it does not change the field's disabled status
// looks like we need to do this explicitly
let shouldBeDisabled = [
"patient-main-disease",
"patient-stronger-impacted-hand-left",
"patient-stronger-impacted-hand-right",
"patient-stronger-impacted-hand-none",
"go-nogo-recognized-error-time",
"kas-sum",
"kopss-sum",
"acl-k-sum",
"demtect-sum",
"additional-mrt-url",
"additional-mrt-resting-state",
"additional-mrt-resting-state-valid",
"additional-mrt-tapping-task",
"additional-mrt-tapping-task-valid",
"additional-mrt-anatomical-representation",
"additional-mrt-dti",
"additional-mrt-dti-valid",
"additional-eeg-url",
"additional-blood-sampling-url",
"submit-button",
];
// disable all fields which should be disabled
shouldBeDisabled.forEach((fieldId) => {
let elem = document.getElementById(fieldId);
if (!elem.disabled) {
elem.disabled = true;
}
});
// enable all fields which should be enabled
fieldsToWrite.forEach((fieldId) => {
let elem = document.getElementById(fieldId);
// note: this won't catch radio buttons, but presently they cannot get disabled
if (
elem !== null &&
elem.disabled &&
!shouldBeDisabled.includes(fieldId)
) {
elem.disabled = false;
}
});
}
function insertSubjectGroup(groupName) {
document.getElementById("subject-group").value = groupName;
patientUpdateElement(); // required to display or hide patient-specific fields
}
function insertGoNogo(loadedData) {
function updateAndDispatch(obj, key, evt, parseFun) {
// helper to update a field and trigger an event if obj[key] is nonempty
let elem = document.getElementById(key);
if (obj[key] !== "") {
if (parseFun === undefined) {
elem.value = obj[key];
} else {
elem.value = parseFun(obj[key]);
}
elem.dispatchEvent(evt);
}
}
let syntheticChangeEvent = new UIEvent("change");
// update block count, reaction times, and error counts
updateAndDispatch(
loadedData,
"go-nogo-block-count",
syntheticChangeEvent
);
updateAndDispatch(
loadedData,
"go-nogo-correct-answer-time",
syntheticChangeEvent,
parseFloat
);
updateAndDispatch(
loadedData,
"go-nogo-total-errors",
syntheticChangeEvent,
parseInt
);
updateAndDispatch(
loadedData,
"go-nogo-wrong-errors",
syntheticChangeEvent,
parseInt
);
updateAndDispatch(
loadedData,
"go-nogo-recognized-errors",
syntheticChangeEvent,
parseInt
);
updateAndDispatch(
loadedData,
"go-nogo-recognized-error-time",
syntheticChangeEvent,
parseFloat
);
// note *-time fields have no onChange event, but that shouldn't be a problem
// update checkbox
if (loadedData["go-nogo-incorrectly-executed"] === "True") {
document.getElementById(
"go-nogo-incorrectly-executed"
).checked = true;
} else if (loadedData["go-nogo-incorrectly-executed"] === "False") {
document.getElementById(
"go-nogo-incorrectly-executed"
).checked = false;
}
}
function loadData() {
let obj = JSON.parse(this.result); // when used as event handler, this = element on which is placed
// set the state to a clean sheet
resetTheForm(Object.keys(obj));
// some fields need to trigger their events and need more than just value update
let handledSeparately = [/subject-group/, /go-nogo-*/];
insertSubjectGroup(obj["subject-group"]);
insertGoNogo(obj);
// iterate through all the loaded values and put remaining values into the form
for (let [key, value] of Object.entries(obj)) {
if (handledSeparately.some((x) => key.match(x))) continue;
let element = document.getElementById(key); // will be null if key not present
if (element !== null) {
let elType = element.getAttribute("type");
if (
(elType === "text") |
(elType === "date") |
(elType === "url")
) {
element.value = value;
} else if (elType === "number") {
if (value === "") {
// disabled or NaN - don't change (relies on the form being reset by this function)
} else {
element.value = parseFloat(value);
}
} else if (elType === "checkbox") {
if (value === "True") {
element.checked = true;
} else if (value === "False") {
element.checked = false;
}
// note: (value === "") <-> checkbox was disabled; do nothing
} else if (element.type === "textarea") {
// for textarea, getAttribute() doesn't work
element.value = value;
} else if (element.getAttribute("class") == "form-select") {
element.value = value;
} else {
// catch those that didn't match - shouldn't be any
console.log(
"unmatched",
element.getAttribute("class"),
elType,
element
);
}
} else {
// element with given id not found, probably radiobox
// assuming the coding is done with name & value proprerties
document.getElementsByName(key).forEach((elem) => {
// assuming type === radio, might check
if (elem.getAttribute("value") === value) {
elem.checked = true;
} else {
elem.checked = false;
}
});
}
}
// Weitere Diagnostik starts disabled, enable if needed
if (
obj["additional-mrt-url"] !== "" ||
obj["additional-eeg-url"] !== "" ||
obj["additional-blood-sampling-url"] != ""
) {
document.getElementById("enable-ad").click();
}
// update sum fields
kasSum();
kopssSum();
aclKSum();
demtectSum();
}
function readSavedJSON() {
const [dataFile] = this.files; // let dataFile = this.files[0];
const reader = new FileReader();
reader.addEventListener("load", loadData, false);
if (dataFile) {
reader.readAsText(dataFile);
}
}
const fileSelect = document.getElementById("local-load-button");
const fileElem = document.getElementById("local-load-input");
fileSelect.addEventListener(
"click",
function (e) {
if (fileElem) {
fileElem.click();
}
},
false
);
fileElem.addEventListener("change", readSavedJSON, false);
</script> </script>
</body> </body>
</html> </html>
<!-- Local Variables: -->
<!-- sgml-basic-offset: 4 -->
<!-- web-mode-script-padding: 4 -->
<!-- indent-tabs-mode: nil -->
<!-- End: -->