"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(); });