Implement local save and load buttons #28
2 changed files with 301 additions and 6 deletions
4
.prettierrc.yaml
Normal file
4
.prettierrc.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
trailingComma: "es5"
|
||||||
|
tabWidth: 4
|
||||||
|
semi: true
|
||||||
|
singleQuote: false
|
||||||
303
entry.html
303
entry.html
|
|
@ -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();
|
||||||
|
|
||||||
|
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.
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: -->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue
If
subject-pseudonymis not set,fnamewill be.json(there is also a comment in the PR discussion). Maybe decline to save withoutsubject-pseudonymor ask for a value?Maybe remove element
aagain?Generally it would be nice if you could unify tab and space, more precisly, replace tabs with 4 spaces