class AppClient { constructor(currentUserId) { this.socket = io({transports: ['websocket']}); this.users = {}; this.users.self = this.loadUser(currentUserId); } loadUser(userId) { let user = new User(); this.users[userId] = user; this.socket.on(`user_${userId}_init`, msg => user.init(JSON.parse(msg))); this.socket.on(`user_${userId}_patch`, msg => user.patch(JSON.parse(msg))); this.socket.emit('start_user_session', userId); return user; } } class User { constructor() { this.data = undefined; this.eventListeners = { corporaInit: [], corporaPatch: [], jobsInit: [], jobsPatch: [], queryResultsInit: [], queryResultsPatch: [] }; } init(data) { this.data = data; let listener; for (listener of this.eventListeners.corporaInit) { listener(this.data.corpora); } for (listener of this.eventListeners.jobsInit) { listener(this.data.jobs); } for (listener of this.eventListeners.queryResultsInit) { listener(this.data.query_results); } } patch(patch) { this.data = jsonpatch.apply_patch(this.data, patch); let corporaPatch = patch.filter(operation => operation.path.startsWith("/corpora")); let jobsPatch = patch.filter(operation => operation.path.startsWith("/jobs")); let queryResultsPatch = patch.filter(operation => operation.path.startsWith("/query_results")); for (let listener of this.eventListeners.corporaPatch) { if (corporaPatch.length > 0) {listener(corporaPatch);} } for (let listener of this.eventListeners.jobsPatch) { if (jobsPatch.length > 0) {listener(jobsPatch);} } for (let listener of this.eventListeners.queryResultsPatch) { if (queryResultsPatch.length > 0) {listener(queryResultsPatch);} } for (let operation of jobsPatch) { if (operation.op !== 'replace') {continue;} // Matches the only path that should be handled here: /jobs/{jobId}/status if (/^\/jobs\/(\d+)\/status$/.test(operation.path)) { let [match, jobId] = operation.path.match(/^\/jobs\/(\d+)\/status$/); if (this.data.settings.job_status_site_notifications === "end" && !["complete", "failed"].includes(operation.value)) {continue;} nopaque.flash(`[${this.data.jobs[jobId].title}] New status: ${operation.value}`, "job"); } } } addEventListener(type, listener) { switch (type) { case 'corporaInit': this.eventListeners.corporaInit.push(listener); if (this.data !== undefined) {listener(this.data.corpora);} break; case 'corporaPatch': this.eventListeners.corporaPatch.push(listener); break; case 'jobsInit': this.eventListeners.jobsInit.push(listener); if (this.data !== undefined) {listener(this.data.jobs);} break; case 'jobsPatch': this.eventListeners.jobsPatch.push(listener); break; case 'queryResultsInit': this.eventListeners.queryResultsInit.push(listener); if (this.data !== undefined) {listener(this.data.query_results);} break; case 'queryResultsPatch': this.eventListeners.queryResultsPatch.push(listener); break; default: console.error(`Unknown event type: ${type}`); } } } /* * The nopaque object is used as a namespace for nopaque specific functions and * variables. */ var nopaque = {}; nopaque.flash = function(message, category) { let toast; let toastActionElement; switch (category) { case "corpus": message = `book${message}`; break; case "error": message = `error${message}`; break; case "job": message = `work${message}`; break; default: message = `notifications${message}`; } toast = M.toast({html: `${message} `}); toastActionElement = toast.el.querySelector('.toast-action[data-action="close"]'); toastActionElement.addEventListener('click', () => {toast.dismiss();}); }; nopaque.Forms = {}; nopaque.Forms.init = function() { var abortRequestElement, parentElement, progressElement, progressModal, progressModalElement, request, submitElement; for (let form of document.querySelectorAll(".nopaque-submit-form")) { submitElement = form.querySelector('button[type="submit"]'); submitElement.addEventListener("click", function() { for (let selectElement of form.querySelectorAll('select')) { if (selectElement.value === "") { parentElement = selectElement.closest(".input-field"); parentElement.querySelector(".select-dropdown").classList.add("invalid"); for (let helperTextElement of parentElement.querySelectorAll(".helper-text")) { helperTextElement.remove(); } parentElement.insertAdjacentHTML("beforeend", `Please select an option.`); } } }); request = new XMLHttpRequest(); if (form.dataset.hasOwnProperty("progressModal")) { progressModalElement = document.getElementById(form.dataset.progressModal); progressModal = M.Modal.getInstance(progressModalElement); progressModal.options.dismissible = false; abortRequestElement = progressModalElement.querySelector(".abort-request"); abortRequestElement.addEventListener("click", function() {request.abort();}); progressElement = progressModalElement.querySelector(".determinate"); } form.addEventListener("submit", function(event) { event.preventDefault(); var formData; formData = new FormData(form); // Initialize progress modal if (progressModalElement) { progressElement.style.width = "0%"; progressModal.open(); } request.open("POST", window.location.href); request.send(formData); }); request.addEventListener("load", function(event) { var fieldElement; if (request.status === 201) { window.location.href = JSON.parse(this.responseText).redirect_url; } if (request.status === 400) { console.log(request); for (let [field, errors] of Object.entries(JSON.parse(this.responseText))) { fieldElement = form.querySelector(`input[name$="${field}"]`).closest(".input-field"); for (let error of errors) { fieldElement.insertAdjacentHTML("beforeend", `${error}`); } } if (progressModalElement) { progressModal.close(); } } if (request.status === 500) { location.reload(); } }); if (progressModalElement) { request.upload.addEventListener("progress", function(event) { progressElement.style.width = Math.floor(100 * event.loaded / event.total).toString() + "%"; }); } } }