Implement all logic on the client side in csvxmlV2.js
For completing the rewrite, translations and some UI work are still needed. See #14
This commit is contained in:
682
public/assets/js/csvxmlV2.js
Normal file
682
public/assets/js/csvxmlV2.js
Normal file
@ -0,0 +1,682 @@
|
||||
"use strict";
|
||||
|
||||
class CsvxmlValidator {
|
||||
|
||||
fieldList; // {}{}
|
||||
toValidate; // []{}
|
||||
errors; // {}array
|
||||
|
||||
constructor(fieldList, csvRaw) {
|
||||
|
||||
this.errors = {
|
||||
parsing: [],
|
||||
mandatoryTags: [],
|
||||
duplicateInvNos: [],
|
||||
dependentColumns: [],
|
||||
controlledLists: [],
|
||||
mainImageResource: [],
|
||||
};
|
||||
|
||||
let list = {};
|
||||
for (let sectionName in fieldList) {
|
||||
list = Object.assign(list, fieldList[sectionName]);
|
||||
}
|
||||
this.fieldList = Object.freeze(list);
|
||||
console.log(this.fieldList);
|
||||
|
||||
const lines = csvRaw.trim().replace("\r\n", "\n").split("\n");
|
||||
|
||||
const SEPARATOR = ';';
|
||||
|
||||
// Gets first line
|
||||
let headers = lines.shift().split(SEPARATOR);
|
||||
let expectedFieldCount = headers.length;
|
||||
|
||||
let toValidate = [];
|
||||
let lineCounter = 1;
|
||||
for (let line of lines) {
|
||||
|
||||
// Remove fully empty lines (both without content and those with fields set up,
|
||||
// but without content there.
|
||||
if (line.length <= headers.length) continue;
|
||||
|
||||
let lineContents = {};
|
||||
let fields = line.split(SEPARATOR);
|
||||
|
||||
if (fields.length !== headers.length) {
|
||||
this.errors.parsing.push("Number of columns in line " + lineCounter + " does not match number of headers");
|
||||
}
|
||||
|
||||
for (let i = 0, max = fields.length; i < max; i++) {
|
||||
if (headers[i] === undefined || headers[i] === null) {
|
||||
this.errors.parsing.push("ERROR parsing line " + lineCounter + "; column " + i);
|
||||
continue;
|
||||
}
|
||||
lineContents[headers[i]] = fields[i];
|
||||
}
|
||||
|
||||
toValidate.push(lineContents);
|
||||
lineCounter++;
|
||||
}
|
||||
|
||||
console.log(toValidate);
|
||||
this.toValidate = toValidate;
|
||||
|
||||
if (toValidate.length === 0) {
|
||||
alert("Error: No lines of content identified");
|
||||
}
|
||||
|
||||
this.validate();
|
||||
}
|
||||
|
||||
validate() {
|
||||
|
||||
this.validateMandatoryTagsPresent();
|
||||
this.checkDuplicateInvNos();
|
||||
this.checkDependentColumns();
|
||||
this.checkControlledLists();
|
||||
// this.checkMainImageResource();
|
||||
|
||||
}
|
||||
|
||||
validateMandatoryTagsPresent() {
|
||||
|
||||
let mandatoryFields = [];
|
||||
for (let fieldName in this.fieldList) {
|
||||
if (this.fieldList[fieldName].required === true) {
|
||||
mandatoryFields.push(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(this.toValidate);
|
||||
let lineCounter = 1;
|
||||
for (let line of this.toValidate) {
|
||||
for (let mandatoryField of mandatoryFields) {
|
||||
if (line[mandatoryField] === undefined || line[mandatoryField] === null || line[mandatoryField] === '') {
|
||||
this.errors.mandatoryTags.push("Missing or empty mandatory tag " + mandatoryField + " on line " + lineCounter);
|
||||
}
|
||||
}
|
||||
lineCounter++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
checkDuplicateInvNos() {
|
||||
|
||||
let invNoEncountered = [];
|
||||
let lineCounter = 1;
|
||||
for (let line of this.toValidate) {
|
||||
if (invNoEncountered.includes(line.inventory_number)) {
|
||||
this.errors.duplicateInvNos.push("Duplicate inventory number " + line.inventory_number + " on line " + lineCounter);
|
||||
}
|
||||
invNoEncountered.push(line.inventory_number);
|
||||
lineCounter++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
checkDependentColumns() {
|
||||
|
||||
const headers = Object.keys(this.toValidate[0]);
|
||||
|
||||
for (let header of headers) {
|
||||
if (this.fieldList[header].dependsOn === undefined || this.fieldList[header].dependsOn === null) continue;
|
||||
|
||||
let dependencies = this.fieldList[header].dependsOn;
|
||||
for (let dep of dependencies) {
|
||||
if (headers.includes(dep) === false) {
|
||||
this.errors.dependentColumns.push("Dependency issue at column " + header + ": Corresponding column " + dep + " is missing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
checkControlledLists() {
|
||||
|
||||
let lineCounter = 1;
|
||||
for (let line of this.toValidate) {
|
||||
for (let fieldName in line) {
|
||||
|
||||
let allowedValues = this.fieldList[fieldName].allowedValues;
|
||||
|
||||
// No error if the field doesn't have a controlled list
|
||||
if (allowedValues === undefined || allowedValues === null) continue;
|
||||
// No error if the line's content is in the list
|
||||
if (Object.values(allowedValues).length === 0 || Object.values(allowedValues).includes(line.fieldName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.errors.controlledLists.push("Disallowed value used for column " + fieldName + " at line " + lineCounter + " (Allowed values are: " + Object.values(allowedValues).join(", ") + ")");
|
||||
}
|
||||
lineCounter++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
checkMainImageResource() {
|
||||
|
||||
}
|
||||
|
||||
isValid() {
|
||||
|
||||
for (let errorClass in this.errors) {
|
||||
if (errorClass.length !== 0) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates XML for the parsed lines
|
||||
*/
|
||||
generateXml() {
|
||||
|
||||
let output = [];
|
||||
|
||||
let xmlDoc = document.implementation.createDocument(null, "record");
|
||||
for (let line of this.toValidate) {
|
||||
let root = xmlDoc.createElement("record");
|
||||
for (let fieldName in line) {
|
||||
const elem = xmlDoc.createElement(fieldName);
|
||||
elem.textContent = line[fieldName];
|
||||
root.appendChild(elem);
|
||||
}
|
||||
output.push(root)
|
||||
}
|
||||
|
||||
return output;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CsvxmlTooltip {
|
||||
|
||||
/**
|
||||
* Function for setting the alignment of an element.
|
||||
*
|
||||
* @param {Event} e Event triggering the execution of this function.
|
||||
* @param {DOMElement} elem Dom element to position.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static getDirection(e, elem) {
|
||||
|
||||
if (window.innerHeight < e.clientY + elem.clientHeight) {
|
||||
elem.style.top = "";
|
||||
elem.style.bottom = (window.innerHeight - e.clientY) + "px";
|
||||
}
|
||||
else {
|
||||
elem.style.bottom = "";
|
||||
elem.style.top = (e.clientY + 4) + "px";
|
||||
}
|
||||
if (window.innerWidth < e.clientX + elem.clientWidth) {
|
||||
elem.style.left = "";
|
||||
elem.style.right = (window.innerWidth - e.clientX) + "px";
|
||||
} else {
|
||||
elem.style.right = "";
|
||||
elem.style.left = (e.clientX + 3) + "px";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static positionMobile(newMain) {
|
||||
|
||||
if (window.matchMedia && window.matchMedia('(max-width:75em)').matches) {
|
||||
|
||||
newMain.style.left = "";
|
||||
newMain.style.right = "";
|
||||
newMain.style.top = "";
|
||||
newMain.style.bottom = "";
|
||||
|
||||
if (newMain.classList.contains("atBottom") === false) {
|
||||
newMain.classList.add("atBottom");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static triggerMouseMove(e) {
|
||||
const newMain = document.getElementById("newToolTipMain");
|
||||
if (newMain === undefined || newMain === null) return;
|
||||
CsvxmlTooltip.getDirection(e, newMain);
|
||||
CsvxmlTooltip.positionMobile(newMain);
|
||||
}
|
||||
|
||||
static triggerMouseOut(e) {
|
||||
|
||||
const newMain = document.getElementById("newToolTipMain");
|
||||
if (newMain !== undefined && newMain !== null) {
|
||||
newMain.classList.remove("visible");
|
||||
document.body.removeChild(newMain);
|
||||
}
|
||||
e.target.removeEventListener('mouseout', CsvxmlTooltip.triggerMouseOut);
|
||||
|
||||
}
|
||||
|
||||
static bindTooltipToElement(elem, tooltipTitle, tooltipContent) {
|
||||
|
||||
elem.addEventListener('mouseover', function(e) {
|
||||
|
||||
let newMain = document.getElementById("newToolTipMain");
|
||||
if (newMain !== null) return;
|
||||
newMain = document.createElement("div");
|
||||
newMain.classList.add("newToolTip");
|
||||
newMain.id = "newToolTipMain";
|
||||
|
||||
// Insert contents loaded.
|
||||
|
||||
newMain.setAttribute("data-title", tooltipTitle);
|
||||
|
||||
const tooltipDesc = document.createElement("p");
|
||||
tooltipDesc.textContent = tooltipContent;
|
||||
newMain.appendChild(tooltipDesc);
|
||||
|
||||
document.body.appendChild(newMain);
|
||||
newMain.classList.add("visible");
|
||||
CsvxmlTooltip.getDirection(e, newMain);
|
||||
CsvxmlTooltip.positionMobile(newMain);
|
||||
|
||||
newMain.addEventListener("mouseout", function(e) {
|
||||
const newMain = document.getElementById("newToolTipMain");
|
||||
if (newMain !== undefined && newMain !== null) {
|
||||
newMain.classList.remove("visible");
|
||||
document.body.removeChild(newMain);
|
||||
}
|
||||
});
|
||||
|
||||
elem.addEventListener("mouseout", CsvxmlTooltip.triggerMouseOut);
|
||||
|
||||
}, {passive: true});
|
||||
|
||||
elem.addEventListener("mousemove", CsvxmlTooltip.triggerMouseMove, {passive: true});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CsvxmlPage {
|
||||
|
||||
fieldList;
|
||||
domUploaderWrapper;
|
||||
domMainWrapper;
|
||||
selectedFields;
|
||||
|
||||
constructor(fieldList) {
|
||||
this.fieldList = Object.freeze(fieldList);
|
||||
|
||||
let domUploaderWrapper = document.createElement("div");
|
||||
domUploaderWrapper.id = "uploader";
|
||||
domUploaderWrapper.classList.add("uploader");
|
||||
this.domUploaderWrapper = domUploaderWrapper;
|
||||
|
||||
let domMainWrapper = document.createElement("main");
|
||||
this.domMainWrapper = domMainWrapper;
|
||||
|
||||
this.selectedFields = [];
|
||||
}
|
||||
|
||||
generateCsv(selectedFields = []) {
|
||||
|
||||
let line1 = [];
|
||||
let line2 = [];
|
||||
let line3 = [];
|
||||
|
||||
for (let sectionName in this.fieldList) {
|
||||
const sectionFields = this.fieldList[sectionName];
|
||||
for (let fieldName in sectionFields) {
|
||||
console.log(fieldName);
|
||||
console.log(selectedFields);
|
||||
|
||||
if (selectedFields.length !== 0 && selectedFields.includes(fieldName) === false) continue;
|
||||
const field = sectionFields[fieldName];
|
||||
|
||||
line1.push(fieldName);
|
||||
line2.push(field.name_human_readable);
|
||||
|
||||
if (field.allowedValues !== undefined) {
|
||||
// Join for object
|
||||
let values = [];
|
||||
for (let key in field.allowedValues) values.push(field.allowedValues[key]);
|
||||
line3.push(values.join(","));
|
||||
}
|
||||
else line3.push("");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const csvLine1 = '"' + line1.join('";"') + '"';
|
||||
const csvLine2 = '"' + line2.join('";"') + '"';
|
||||
const csvLine3 = '"' + line3.join('";"') + '"';
|
||||
|
||||
const toStore = csvLine1 + "\n" + csvLine2 + "\n" + csvLine3;
|
||||
|
||||
// Download
|
||||
const triggerLink = document.createElement('a');
|
||||
triggerLink.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(toStore));
|
||||
triggerLink.setAttribute('download', "csvxml_museum-digital_template.csv");
|
||||
|
||||
triggerLink.style.display = 'none';
|
||||
|
||||
document.body.appendChild(triggerLink);
|
||||
triggerLink.click();
|
||||
document.body.removeChild(triggerLink);
|
||||
|
||||
|
||||
}
|
||||
|
||||
zipUploadToXml(validator) {
|
||||
|
||||
let zip = new JSZip();
|
||||
|
||||
let xmlFiles = validator.generateXml();
|
||||
const serializer = new XMLSerializer();
|
||||
let lineCounter = 0;
|
||||
for (let xml of xmlFiles) {
|
||||
zip.file(lineCounter + ".xml", serializer.serializeToString(xml));
|
||||
lineCounter++;
|
||||
}
|
||||
|
||||
zip.generateAsync({type:"blob"})
|
||||
.then(function(content) {
|
||||
const triggerLink = document.createElement('a');
|
||||
triggerLink.href = window.URL.createObjectURL(content);
|
||||
triggerLink.setAttribute('download', "csvxml.zip");
|
||||
|
||||
triggerLink.style.display = 'none';
|
||||
|
||||
document.body.appendChild(triggerLink);
|
||||
triggerLink.click();
|
||||
document.body.removeChild(triggerLink);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
uploadFileForValidation(file) {
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file);
|
||||
|
||||
let app = this;
|
||||
|
||||
reader.onload = function() {
|
||||
|
||||
// On loading success, check if the upload is valid JSON
|
||||
console.log("Read file");
|
||||
// Validate the file
|
||||
let validator = new CsvxmlValidator(app.fieldList, reader.result);
|
||||
if (validator.isValid() === true) {
|
||||
alert("Document is valid");
|
||||
}
|
||||
else {
|
||||
console.log(validator.errors);
|
||||
alert("Document is not valid. Errors are " + validator.errors);
|
||||
}
|
||||
|
||||
app.zipUploadToXml(validator);
|
||||
|
||||
};
|
||||
reader.onerror = function() {
|
||||
alert(reader.error);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
renderUploader() {
|
||||
|
||||
let app = this;
|
||||
(async function() {
|
||||
|
||||
const form = document.createElement("form");
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.textContent = "Please select a CSV file to create XML files"; // TODO
|
||||
label.setAttribute("for", "fileToUpload");
|
||||
form.appendChild(label);
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.id = "fileToUpload";
|
||||
input.accept = ".csv";
|
||||
input.required = "required";
|
||||
input.addEventListener('change', async function() {
|
||||
app.uploadFileForValidation(input.files[0]);
|
||||
});
|
||||
form.appendChild(input);
|
||||
|
||||
const button = document.createElement("button");
|
||||
button.textContent = "Upload"; // TODO
|
||||
button.type = "submit";
|
||||
form.appendChild(button);
|
||||
|
||||
app.domUploaderWrapper.appendChild(form);
|
||||
|
||||
})();
|
||||
|
||||
document.body.appendChild(this.domUploaderWrapper);
|
||||
|
||||
}
|
||||
|
||||
getOptionsSection() {
|
||||
|
||||
function genButton(id, text, link = "") {
|
||||
|
||||
const output = document.createElement("a");
|
||||
output.id = id;
|
||||
output.textContent = text;
|
||||
output.classList.add("buttonLike");
|
||||
if (link !== "") output.href = link;
|
||||
return output;
|
||||
|
||||
}
|
||||
|
||||
const options = document.createElement("div");
|
||||
options.classList.add("options");
|
||||
|
||||
const app = this;
|
||||
|
||||
const dlAllButton = genButton("dlAll", "Download all");
|
||||
dlAllButton.cursor = "pointer";
|
||||
dlAllButton.addEventListener('click', function() {
|
||||
app.generateCsv();
|
||||
});
|
||||
options.appendChild(dlAllButton); // TODO
|
||||
|
||||
const csvBySelectionButton = genButton("csvBySelection", "Csv by selection");
|
||||
csvBySelectionButton.classList.add("invisible");
|
||||
// TODO: Add toggle via event listener
|
||||
options.appendChild(csvBySelectionButton);
|
||||
|
||||
const optionSelectRequired = genButton("selectRequired", "Select required");
|
||||
options.appendChild(optionSelectRequired);
|
||||
|
||||
const optionSelectAll = genButton("selectAll", "Select all");
|
||||
options.appendChild(optionSelectAll);
|
||||
|
||||
const unsetSelectionButton = genButton("unsetSelection", "Unset selection");
|
||||
unsetSelectionButton.classList.add("invisible");
|
||||
// TODO: Add toggle via event listener
|
||||
options.appendChild(unsetSelectionButton);
|
||||
|
||||
function checkCSVBySelectionAccessibility() {
|
||||
|
||||
let selected = document.getElementsByClassName("humanTLToggled");
|
||||
if (selected.length === 0) {
|
||||
csvBySelectionButton.classList.add("invisible");
|
||||
unsetSelectionButton.classList.add("invisible");
|
||||
}
|
||||
else {
|
||||
csvBySelectionButton.classList.remove("invisible");
|
||||
unsetSelectionButton.classList.remove("invisible");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Takes a callback function
|
||||
function doForFieldList(callback) {
|
||||
let fieldLists = document.getElementsByClassName("fieldList");
|
||||
for (let i = 0, max = fieldLists.length; i < max; i++) {
|
||||
|
||||
let fields = fieldLists[i].getElementsByTagName("li");
|
||||
for (let j = 0, maxj = fields.length; j < maxj; j++) {
|
||||
|
||||
callback(fields[j]);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function toggleListFieldSelectionState(field) {
|
||||
|
||||
let newValue = field.getAttribute("data-alt");
|
||||
field.setAttribute("data-alt", field.textContent);
|
||||
field.textContent = newValue;
|
||||
field.classList.toggle("humanTLToggled");
|
||||
|
||||
if (field.classList.contains("humanTLToggled") === false) return;
|
||||
|
||||
let dependencies = field.getAttribute("data-dependencies");
|
||||
if (dependencies !== undefined && dependencies !== null) {
|
||||
let linkedFields = dependencies.split(";");
|
||||
for (let i = 0, max = linkedFields.length; i < max; i++) {
|
||||
let linkedField = document.getElementById(linkedFields[i]);
|
||||
if (linkedField.classList.contains("humanTLToggled") === true) continue;
|
||||
toggleListFieldSelectionState(linkedField);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
doForFieldList(function(field) {
|
||||
|
||||
// Each field should switch its visible content and human-readable
|
||||
// translation on a click.
|
||||
field.addEventListener('click', function(e) {
|
||||
|
||||
toggleListFieldSelectionState(field);
|
||||
checkCSVBySelectionAccessibility();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
csvBySelectionButton.addEventListener('click', function(e) {
|
||||
|
||||
let selected = document.getElementsByClassName("humanTLToggled");
|
||||
let selectedFields = [];
|
||||
for (let i = 0, max = selected.length; i < max; i++) {
|
||||
selectedFields += selected[i].getAttribute("data-value");
|
||||
}
|
||||
app.generateCsv(selectedFields);
|
||||
|
||||
});
|
||||
|
||||
optionSelectRequired.addEventListener('click', function(e) {
|
||||
|
||||
doForFieldList(function(field) {
|
||||
if (field.classList.contains("requiredField") === false) return;
|
||||
if (field.classList.contains("humanTLToggled") === true) return;
|
||||
|
||||
toggleListFieldSelectionState(field);
|
||||
checkCSVBySelectionAccessibility();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
optionSelectAll.addEventListener('click', function(e) {
|
||||
|
||||
doForFieldList(function(field) {
|
||||
if (field.classList.contains("humanTLToggled") === true) return;
|
||||
|
||||
toggleListFieldSelectionState(field);
|
||||
checkCSVBySelectionAccessibility();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
unsetSelectionButton.addEventListener('click', function(e) {
|
||||
|
||||
doForFieldList(function(field) {
|
||||
if (field.classList.contains("humanTLToggled") === false) return;
|
||||
|
||||
toggleListFieldSelectionState(field);
|
||||
checkCSVBySelectionAccessibility();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return options;
|
||||
|
||||
}
|
||||
|
||||
renderMain() {
|
||||
|
||||
const domH2 = document.createElement("h2");
|
||||
domH2.textContent = "Currently approved tags (column names) for md:import";
|
||||
this.domMainWrapper.appendChild(domH2);
|
||||
|
||||
this.domMainWrapper.appendChild(this.getOptionsSection());
|
||||
|
||||
for (let sectionName in this.fieldList) {
|
||||
|
||||
const domDiv = document.createElement("div");
|
||||
|
||||
const domH3 = document.createElement("h3");
|
||||
domH3.textContent = sectionName;
|
||||
domDiv.appendChild(domH3);
|
||||
|
||||
const domUl = document.createElement("ul");
|
||||
domUl.classList.add("fieldList");
|
||||
|
||||
console.log(sectionName);
|
||||
|
||||
const sectionFields = this.fieldList[sectionName];
|
||||
for (let fieldName in sectionFields) {
|
||||
const field = sectionFields[fieldName];
|
||||
|
||||
const domLi = document.createElement("li");
|
||||
domLi.textContent = fieldName;
|
||||
domLi.id = fieldName;
|
||||
domLi.setAttribute("data-alt", field.name_human_readable)
|
||||
domLi.setAttribute("data-value", fieldName)
|
||||
if (field.required === true) domLi.classList.add("requiredField");
|
||||
domUl.appendChild(domLi);
|
||||
|
||||
CsvxmlTooltip.bindTooltipToElement(domLi, field.name_human_readable, field.explica);
|
||||
|
||||
}
|
||||
|
||||
domDiv.appendChild(domUl);
|
||||
|
||||
this.domMainWrapper.appendChild(domDiv);
|
||||
|
||||
}
|
||||
|
||||
document.body.appendChild(this.domMainWrapper);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
document.body.classList.add("loading");
|
||||
|
||||
window.fetch('/?output=json', {
|
||||
method: 'GET', cache: 'no-cache',
|
||||
credentials: 'same-origin',
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(elements) {
|
||||
|
||||
document.body.classList.remove("loading");
|
||||
|
||||
const page = new CsvxmlPage(elements);
|
||||
page.renderUploader();
|
||||
page.renderMain();
|
||||
|
||||
});
|
Reference in New Issue
Block a user