"use strict"; if ('serviceWorker' in navigator) { console.log("Registering service worker"); navigator.serviceWorker.register('/sw.js'); } class CsvxmlValidator { fieldList; // {}{} toValidate; // []{} errors; // {}array constructor(fieldList, csvRaw) { this.errors = { parsing: [], mandatoryTags: [], duplicateInvNos: [], dependentColumns: [], controlledLists: [], mainImageResource: [], }; this.fieldList = Object.freeze(fieldList); const data = Papa.parse(csvRaw.trim(), {header: true}); if (data.errors.length !== 0) { window.alert(data.errors); } let toValidate = data.data; 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] === undefined || 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"); } } } let lineCounter = 1; for (let line of this.toValidate) { for (let fieldName in line) { if (line[fieldName] === '') continue; const dependencies = this.fieldList[fieldName].dependsOn; if (dependencies === undefined) continue; for (let dependency of dependencies) { if (line[dependency] === '') { this.errors.dependentColumns.push("Dependency issue at column " + fieldName + ": Corresponding column " + dependency + " is empty"); } } } lineCounter++; } } checkControlledLists() { let lineCounter = 1; for (let line of this.toValidate) { for (let fieldName in line) { if (this.fieldList[fieldName] === undefined) { console.log("Undefined but requested field " + fieldName); continue; } const 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; } if (line[fieldName] === '') continue; this.errors.controlledLists.push("Disallowed value used for column " + fieldName + " at line " + lineCounter + " (Allowed values are: " + Object.values(allowedValues).join(", ") + "; current value is " + line[fieldName] + ")"); } lineCounter++; } } checkMainImageResource() { } isValid() { for (let errorClass in this.errors) { if (this.errors[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); newMain.appendChild(tooltipContent); 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 CsvxmlDialogue { static closeDialogue(e) { if (e !== undefined) { e.preventDefault(); e.stopPropagation(); } let dialogueArea = document.getElementById("dialogueArea"); if (dialogueArea !== null && dialogueArea !== false) { while (dialogueArea.firstChild) { dialogueArea.removeChild(dialogueArea.firstChild); } dialogueArea.parentElement.removeChild(dialogueArea); document.removeEventListener('keydown', CsvxmlDialogue.closeDialogueByEscape, false); } } static closeDialogueByEscape(e) { if (e.keyCode === 27) { // 27 = Esc CsvxmlDialogue.closeDialogue(e); } } /** * Function for drawing a dialogue and attaching it to the body elem. * * @param {DOMElement} contents Contents. */ static drawDialogue(contents) { let dialogueArea = document.createElement("div"); dialogueArea.id = "dialogueArea"; let dialogue = document.createElement("div"); dialogue.id = "dialogue"; dialogue.appendChild(contents); dialogueArea.appendChild(dialogue); document.body.appendChild(dialogueArea); document.addEventListener('keydown', CsvxmlDialogue.closeDialogueByEscape); return dialogue; } } class CsvxmlPage { fieldList; fieldListFlat; tls; domHelpWrapper; domUploaderWrapper; domMainWrapper; selectedFields; csvBySelectionButton; unsetSelectionButton; constructor(fieldList, tls) { this.fieldList = Object.freeze(fieldList); let list = {}; for (let sectionName in fieldList) { list = Object.assign(list, fieldList[sectionName]); } this.fieldListFlat = Object.freeze(list); this.tls = Object.freeze(tls); let domHelpWrapper = document.createElement("div"); domHelpWrapper.id = "helpSection"; this.domHelpWrapper = domHelpWrapper; 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 fieldName in this.fieldListFlat) { console.log(fieldName); console.log(selectedFields); if (selectedFields.length !== 0 && selectedFields.includes(fieldName) === false) continue; const field = this.fieldListFlat[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) { // Wrap zipping in function to postload it function runZipping() { 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); }); } if (typeof JSZip === "undefined") { const loadScript = document.createElement("script"); loadScript.setAttribute("src", "assets/js/jszip/dist/jszip.min.js"); loadScript.addEventListener('load', function() { // console.log("Post-loaded OpenLayers"); runZipping(); }, {passive: true, once: true}); document.body.appendChild(loadScript); } else { runZipping(); } } generateDialogueCloseButton() { const cancelB = document.createElement("a"); cancelB.classList.add("icons"); cancelB.classList.add("iconsClose"); cancelB.classList.add("dialogueCloseX"); cancelB.id = "dialogueClose"; cancelB.textContent = "X"; cancelB.title = "Close"; cancelB.href = "#" + location.href; cancelB.addEventListener('click', CsvxmlDialogue.closeDialogue); return cancelB; } listValidationErrors(validator) { console.log("Listing validation errors"); const dialogueContent = document.createElement("div"); const headline = document.createElement("h3"); headline.textContent = this.tls.validation_errors; headline.appendChild(this.generateDialogueCloseButton()); dialogueContent.appendChild(headline); const domErrorsSection = document.createElement("div"); for (let errorType in validator.errors) { if (validator.errors[errorType].length === 0) continue; const ulHl = document.createElement("h4"); ulHl.textContent = this.tls['errors_' + errorType] + " (" + validator.errors[errorType].length + ")"; ulHl.style.cursor = "pointer"; domErrorsSection.appendChild(ulHl); const ul = document.createElement("ul"); for (let error of validator.errors[errorType]) { const li = document.createElement("li"); li.textContent = error; ul.appendChild(li); } ulHl.addEventListener('click', function() { ul.classList.toggle("minimized"); }); domErrorsSection.appendChild(ul); } dialogueContent.appendChild(domErrorsSection); const domDlSection = document.createElement("div"); const domDlA = document.createElement("span"); domDlA.textContent = this.tls.download; domDlA.classList.add("buttonLike"); let app = this; domDlA.addEventListener('click', function() { app.zipUploadToXml(validator); }); domDlSection.appendChild(domDlA); dialogueContent.appendChild(domDlSection); dialogue = CsvxmlDialogue.drawDialogue(dialogueContent); } uploadFileForValidation(file) { const reader = new FileReader(); reader.readAsText(file); let app = this; document.body.classList.add("loading"); reader.onload = function() { function handleValidation() { // On loading success, check if the upload is valid JSON console.log("Read file"); // Validate the file let validator = new CsvxmlValidator(app.fieldListFlat, reader.result); document.body.classList.remove("loading"); if (validator.isValid() === true) { alert("Document is valid. Press ok to download."); app.zipUploadToXml(validator); } else { console.log("Identified invalid upload document"); app.listValidationErrors(validator); } } console.log("Postload papaparse"); if (typeof Papa === "undefined") { const loadScript = document.createElement("script"); loadScript.setAttribute("src", "assets/js/papaparse/papaparse.min.js"); loadScript.addEventListener('load', function() { // console.log("Post-loaded OpenLayers"); handleValidation(); }, {passive: true, once: true}); document.body.appendChild(loadScript); } else { handleValidation(); } }; reader.onerror = function() { alert(reader.error); }; } renderGenHeader() { const header = document.createElement("header"); header.id = "mainHeader"; const logoArea = document.createElement("a"); logoArea.id = "logoArea"; logoArea.href = "https://www.museum-digital.org/"; const logoImg = document.createElement("img"); logoImg.src = "assets/img/mdlogo-code-128px.png"; logoImg.alt = "Logo of museum-digital"; logoArea.appendChild(logoImg); const h2 = document.createElement("h2"); h2.textContent = "museum-digital"; logoArea.appendChild(h2); header.appendChild(logoArea); // Right side of the header const nav = document.createElement("nav"); const lAbout = document.createElement("a"); lAbout.href = "https://en.about.museum-digital.org/about"; lAbout.textContent = this.tls.about; nav.appendChild(lAbout); const lContactList = document.createElement("div"); const lContact = document.createElement("a"); lContact.textContent = this.tls.contact; lContact.href = "https://en.about.museum-digital.org/contact/"; lContactList.appendChild(lContact); const lContactDiv = document.createElement("div"); const lImprint = document.createElement("a"); lImprint.textContent = this.tls.imprint; lImprint.href = "https://en.about.museum-digital.org/impressum"; lContactDiv.appendChild(lImprint); const lPrivacy = document.createElement("a"); lPrivacy.textContent = this.tls.privacy_policy; lPrivacy.href = "https://en.about.museum-digital.org/privacy/"; lContactDiv.appendChild(lPrivacy); lContactList.appendChild(lContactDiv); nav.appendChild(lContactList); const lNews = document.createElement("a") lNews.textContent = this.tls.news; lNews.href = "https://blog.museum-digital.org/"; nav.appendChild(lNews); header.appendChild(nav); document.body.appendChild(header); } renderHeader() { const appHeader = document.createElement("header"); const h1 = document.createElement("h1"); const img = document.createElement("img"); img.width = "70"; img.height = "70"; img.src = "assets/img/mdlogo-csvxml.svg"; img.alt = ""; h1.appendChild(img); const h1Span = document.createElement("span"); h1Span.textContent = "museum-digital:csvxml"; h1.appendChild(h1Span); appHeader.appendChild(h1); document.body.appendChild(appHeader); } renderHelpTexts() { let app = this; (async function() { function appendQA(question, answer) { const div = document.createElement("div"); div.classList.add("qaDiv"); const qElem = document.createElement("h3"); qElem.textContent = question; qElem.style.cursor = "pointer"; div.appendChild(qElem); qElem.addEventListener('click', function() { console.log("Listing validation errors"); const dialogueContent = document.createElement("div"); const headline = document.createElement("h3"); headline.textContent = question; headline.appendChild(app.generateDialogueCloseButton()); dialogueContent.appendChild(headline); const answerDiv = document.createElement("div"); answerDiv.textContent = answer; dialogueContent.appendChild(answerDiv); CsvxmlDialogue.drawDialogue(dialogueContent); }); return div; } const div = document.createElement("div"); div.appendChild(appendQA(app.tls.help_where_am_i, app.tls.help_where_am_i_content)); div.appendChild(appendQA(app.tls.help_what_is_csv, app.tls.help_what_is_csv_content)); div.appendChild(appendQA(app.tls.help_how_to_format_csv, app.tls.help_how_to_format_csv_content)); app.domHelpWrapper.appendChild(div); })(); document.body.appendChild(this.domHelpWrapper); } renderUploader() { let app = this; (async function() { const h2 = document.createElement("h2"); h2.textContent = app.tls.upload; app.domUploaderWrapper.appendChild(h2); const form = document.createElement("form"); const label = document.createElement("label"); label.textContent = app.tls.select_csv_file_for_upload; label.setAttribute("for", "fileToUpload"); form.appendChild(label); const input = document.createElement("input"); input.type = "file"; input.id = "fileToUpload"; input.setAttribute("tabindex", "1"); 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); } // Takes a callback 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]); } } } toggleListFieldSelectionState(field) { let app = this; 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 = this.fieldListFlat[field.id].dependsOn; if (dependencies !== undefined && dependencies !== null) { let linkedFields = this.fieldListFlat[field.id].dependsOn; for (let i = 0, max = linkedFields.length; i < max; i++) { let linkedField = document.getElementById(linkedFields[i]); if (linkedField.classList.contains("humanTLToggled") === true) continue; this.toggleListFieldSelectionState(linkedField); } } } checkCSVBySelectionAccessibility() { let selected = document.getElementsByClassName("humanTLToggled"); if (selected.length === 0) { this.csvBySelectionButton.classList.add("invisible"); this.unsetSelectionButton.classList.add("invisible"); } else { this.csvBySelectionButton.classList.remove("invisible"); this.unsetSelectionButton.classList.remove("invisible"); } } getOptionsSection() { function genButton(id, text, link = "") { const output = document.createElement("span"); output.id = id; output.setAttribute("tabindex", "1"); 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", this.tls.download_csv_all); dlAllButton.cursor = "pointer"; dlAllButton.addEventListener('click', function(e) { e.preventDefault(); app.generateCsv(); }); options.appendChild(dlAllButton); this.csvBySelectionButton = genButton("csvBySelection", this.tls.download_csv_by_selection); this.csvBySelectionButton.classList.add("invisible"); options.appendChild(this.csvBySelectionButton); const optionSelectRequired = genButton("selectRequired", this.tls.select_required_fields); options.appendChild(optionSelectRequired); const optionSelectAll = genButton("selectAll", this.tls.select_all_fields); options.appendChild(optionSelectAll); this.unsetSelectionButton = genButton("unsetSelection", this.tls.unset_selection); this.unsetSelectionButton.classList.add("invisible"); options.appendChild(this.unsetSelectionButton); this.csvBySelectionButton.addEventListener('click', function(e) { e.preventDefault(); 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) { e.preventDefault(); app.doForFieldList(function(field) { if (field.classList.contains("requiredField") === false) return; if (field.classList.contains("humanTLToggled") === true) return; app.toggleListFieldSelectionState(field); app.checkCSVBySelectionAccessibility(); }); }); optionSelectAll.addEventListener('click', function(e) { e.preventDefault(); app.doForFieldList(function(field) { if (field.classList.contains("humanTLToggled") === true) return; app.toggleListFieldSelectionState(field); app.checkCSVBySelectionAccessibility(); }); }); this.unsetSelectionButton.addEventListener('click', function(e) { e.preventDefault(); app.doForFieldList(function(field) { if (field.classList.contains("humanTLToggled") === false) return; app.toggleListFieldSelectionState(field); app.checkCSVBySelectionAccessibility(); }); }); return options; } renderMain() { const domH2 = document.createElement("h2"); domH2.textContent = this.tls.currently_approved_tags; 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"); 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); const tooltipContent = document.createElement("div"); const explicaP = document.createElement("p"); explicaP.textContent = field.explica; tooltipContent.appendChild(explicaP); if (field.remarks !== undefined && field.remarks !== '') { const remarkHl = document.createElement("h4"); remarkHl.textContent = this.tls.remarks; tooltipContent.appendChild(remarkHl) const remarkCont = document.createElement("p"); remarkCont = field.remarks; tooltipContent.appendChild(remarkCont); } if (field.allowedValues !== undefined && Object.values(field.allowedValues).length !== 0) { const allowedHl = document.createElement("h4"); allowedHl.textContent = this.tls.allowed_values; tooltipContent.appendChild(allowedHl); const allowedList = document.createElement("p"); allowedList.textContent = Object.values(field.allowedValues).join(', '); tooltipContent.appendChild(allowedList); } CsvxmlTooltip.bindTooltipToElement(domLi, field.name_human_readable, tooltipContent); } domDiv.appendChild(domUl); this.domMainWrapper.appendChild(domDiv); } document.body.appendChild(this.domMainWrapper); let app = this; this.doForFieldList(function(field) { // Each field should switch its visible content and human-readable // translation on a click. field.addEventListener('click', function(e) { app.toggleListFieldSelectionState(field); app.checkCSVBySelectionAccessibility(); }); }); } } (async function() { function getLang() { const allowedLangs = document.documentElement.getAttribute("data-allowed-langs").split(','); if (navigator.language === undefined) return 'en'; const browserLang = navigator.language.toLowerCase().substr(0, 2); console.log(browserLang); if (allowedLangs.includes(browserLang)) return browserLang; else return 'en'; } const lang = getLang(); document.documentElement.setAttribute("lang", lang); document.body.classList.add("loading"); let loaded = 0; let fieldList; let tls; function loadPage() { document.body.classList.remove("loading"); const page = new CsvxmlPage(fieldList, tls); page.renderGenHeader(); page.renderHeader(); page.renderHelpTexts(); page.renderUploader(); page.renderMain(); } window.fetch('/json/fields.' + lang + '.json', { method: 'GET', cache: 'no-cache', credentials: 'same-origin', }).then(function(response) { return response.json(); }).then(function(elements) { fieldList = elements; loaded++; if (loaded === 2) loadPage(); }); window.fetch('/json/tls.' + lang + '.json', { method: 'GET', cache: 'no-cache', credentials: 'same-origin', }).then(function(response) { return response.json(); }).then(function(elements) { tls = elements; loaded++; if (loaded === 2) loadPage(); }); })();