@gov-cy/govcy-express-services
Version:
An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.
320 lines (282 loc) • 12.1 kB
JavaScript
// 🔍 Select all file inputs that have the .govcy-file-upload class
var fileInputs = document.querySelectorAll('input[type="file"].govcy-file-upload');
// select overlay and app root elements
var _govcyOverlay = document.getElementById("govcy--loadingOverlay");
var _govcyAppRoot = document.getElementById("govcy--body");
// Accessibility: Keep track of previously focused element and disabled elements
var _govcyPrevFocus = null;
var _govcyDisabledEls = [];
// 🔁 Loop over each file input and attach a change event listener
fileInputs.forEach(function (input) {
input.addEventListener('change', _uploadFileEventHandler);
});
/**
* Disables all focusable elements within a given root element
* @param {*} root The root element whose focusable children will be disabled
*/
function disableFocusables(root) {
var sel = 'a[href],area[href],button,input,select,textarea,iframe,summary,[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
var nodes = root.querySelectorAll(sel);
_govcyDisabledEls = [];
for (var i = 0; i < nodes.length; i++) {
var el = nodes[i];
if (_govcyOverlay.contains(el)) continue; // don’t disable overlay itself
var prev = el.getAttribute('tabindex');
el.setAttribute('data-prev-tabindex', prev === null ? '' : prev);
el.setAttribute('tabindex', '-1');
_govcyDisabledEls.push(el);
}
root.setAttribute('aria-hidden', 'true'); // hide from AT on fallback
root.setAttribute('aria-busy', 'true');
}
/**
* Restores all focusable elements within a given root element
* @param {*} root The root element whose focusable children will be restored
*/
function restoreFocusables(root) {
for (var i = 0; i < _govcyDisabledEls.length; i++) {
var el = _govcyDisabledEls[i];
var prev = el.getAttribute('data-prev-tabindex');
if (prev === '') el.removeAttribute('tabindex'); else el.setAttribute('tabindex', prev);
el.removeAttribute('data-prev-tabindex');
}
_govcyDisabledEls = [];
root.removeAttribute('aria-hidden');
root.removeAttribute('aria-busy');
}
/**
* Traps tab key navigation within the overlay
* @param {*} e The event
* @returns
*/
function trapTab(e) {
if (e.key !== 'Tab') return;
var focusables = _govcyOverlay.querySelectorAll('a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])');
if (focusables.length === 0) { e.preventDefault(); _govcyOverlay.focus(); return; }
var first = focusables[0], last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
/**
* Shows the loading spinner overlay and traps focus within it
*/
function showLoadingSpinner() {
_govcyPrevFocus = document.activeElement;
_govcyOverlay.setAttribute('aria-hidden', 'false');
_govcyOverlay.setAttribute('tabindex', '-1');
_govcyOverlay.style.display = 'flex';
document.documentElement.style.overflow = 'hidden';
if ('inert' in HTMLElement.prototype) { // progressive enhancement
_govcyAppRoot.inert = true;
} else {
disableFocusables(_govcyAppRoot);
document.addEventListener('keydown', trapTab, true);
}
_govcyOverlay.focus();
}
/**
* Hides the loading spinner overlay and restores focus to the previously focused element
*/
function hideLoadingSpinner() {
_govcyOverlay.style.display = 'none';
_govcyOverlay.setAttribute('aria-hidden', 'true');
document.documentElement.style.overflow = '';
if ('inert' in HTMLElement.prototype) {
_govcyAppRoot.inert = false;
} else {
restoreFocusables(_govcyAppRoot);
document.removeEventListener('keydown', trapTab, true);
}
if (_govcyPrevFocus && _govcyPrevFocus.focus) _govcyPrevFocus.focus();
}
/**
* Handles the upload of a file event
*
* @param {object} event The event
*/
function _uploadFileEventHandler(event) {
var input = event.target;
var messages = {
"uploadSuccesful": {
"el": "Το αρχείο ανέβηκε με επιτυχία",
"en": "File uploaded successfully",
"tr": "File uploaded successfully"
},
"uploadFailed": {
"el": "Το αρχείο δεν ανέβηκε",
"en": "File upload failed",
"tr": "File upload failed"
},
"uploadFailed406": {
"el": "Το επιλεγμένο αρχείο είναι κενό",
"en": "The selected file is empty",
"tr": "The selected file is empty"
},
"uploadFailed407": {
"el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
"en": "The selected file must be a JPG, JPEG, PNG or PDF",
"tr": "The selected file must be a JPG, JPEG, PNG or PDF"
},
"uploadFailed408": {
"el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
"en": "The selected file must be a JPG, JPEG, PNG or PDF",
"tr": "The selected file must be a JPG, JPEG, PNG or PDF"
},
"uploadFailed409": {
"el": "Το επιλεγμένο αρχείο πρέπει να είναι μικρότερο από 4MB",
"en": "The selected file must be smaller than 4MB",
"tr": "The selected file must be smaller than 4MB"
}
};
// 🔐 Get the CSRF token from a hidden input field (generated by your backend)
var csrfEl = document.querySelector('input[type="hidden"][name="_csrf"]');
var csrfToken = csrfEl ? csrfEl.value : '';
// 🔧 Define siteId and pageUrl (you can dynamically extract these later)
var siteId = window._govcySiteId || "";
var pageUrl = window._govcyPageUrl || "";
var lang = window._govcyLang || "el";
// 📦 Grab the selected file
var file = event.target.files[0];
var elementName = input.name; // Form field's `name` attribute
var elementId = input.id; // Form field's `id` attribute
if (!file) return; // Exit if no file was selected
// Show loading spinner
showLoadingSpinner();
// 🧵 Prepare form-data payload for the API
var formData = new FormData();
formData.append('file', file); // Attach the actual file
formData.append('elementName', elementName); // Attach the field name for backend lookup
// 🚀 Build upload URL depending on mode (single / multiple/add / multiple/edit/:index)
var pathname = window.location.pathname;
var uploadUrl;
if (/\/multiple\/add\/?$/.test(pathname)) {
uploadUrl = `/apis/${siteId}/${pageUrl}/multiple/add/upload`;
} else {
var editMatch = pathname.match(/\/multiple\/edit\/(\d+)(?:\/|$)/);
if (editMatch) {
var idx = editMatch[1];
uploadUrl = `/apis/${siteId}/${pageUrl}/multiple/edit/${idx}/upload`;
} else {
uploadUrl = `/apis/${siteId}/${pageUrl}/upload`;
}
}
fetch(uploadUrl, {
method: "POST",
headers: {
"X-CSRF-Token": csrfToken // 🔐 Pass CSRF token in custom header
},
body: formData
})
.then(function (response) {
// 🚀 CHANGED: fetch does not auto-throw on error codes → check manually
if (!response.ok) {
return response.json().then(function (errData) {
throw { response: { data: errData } };
});
}
return response.json();
})
.then(function (data) {
// ✅ Success response
var sha256 = data.Data.sha256;
var fileId = data.Data.fileId;
// 📝 Store returned metadata in hidden fields if needed
// document.querySelector('[name="' + elementName + 'Attachment[fileId]"]').value = fileId;
// document.querySelector('[name="' + elementName + 'Attachment[sha256]"]').value = sha256;
// Hide loading spinner
hideLoadingSpinner();
// Render the file view
_renderFileElement("fileView", elementId, elementName, fileId, sha256, null);
// Accessibility: Update ARIA live region with success message
var statusRegion = document.getElementById('_govcy-upload-status');
if (statusRegion) {
setTimeout(function () {
statusRegion.textContent = messages.uploadSuccesful[lang];
}, 200);
setTimeout(function () {
statusRegion.textContent = '';
}, 5000);
}
})
.catch(function (err) {
// ⚠️ Show an error message if upload fails
var errorMessage = messages.uploadFailed;
var errorCode = err && err.response && err.response.data && err.response.data.ErrorCode;
if (errorCode === 406 || errorCode === 407 || errorCode === 408 || errorCode === 409) {
errorMessage = messages["uploadFailed" + errorCode];
}
// Hide loading spinner
hideLoadingSpinner();
// Render the file input with error
_renderFileElement("fileInput", elementId, elementName, "", "", errorMessage);
// Re-bind the file input's change handler
var newInput = document.getElementById(elementId);
if (newInput) {
newInput.addEventListener('change', _uploadFileEventHandler);
}
// Accessibility: Focus on the form field
document.getElementById(elementId)?.focus();
});
}
/**
* Renders a file element in the DOM
*
* @param {string} elementState The element state. Can be "fileInput" or "fileView"
* @param {string} elementId The element id
* @param {string} elementName The element name
* @param {string} fileId The file id
* @param {string} sha256 The sha256
* @param {object} errorMessage The error message in all supported languages
*/
function _renderFileElement(elementState, elementId, elementName, fileId, sha256, errorMessage) {
// Grab the query string part (?foo=bar&route=something)
var queryString = window.location.search;
// Parse it
var params = new URLSearchParams(queryString);
// Get the "route" value (null if not present)
var route = params.get("route");
// Create an instance of GovcyFrontendRendererBrowser
var renderer = new GovcyFrontendRendererBrowser();
var lang = window._govcyLang || "el";
// Define the input data
var inputData =
{
"site": {
"lang": lang
}
};
var fileInputMap = JSON.parse(JSON.stringify(window._govcyFileInputs));
var fileElement = fileInputMap[elementName];
fileElement.element = elementState;
if (errorMessage != null) fileElement.params.error = errorMessage;
if (fileId != null) fileElement.params.fileId = fileId;
if (sha256 != null) fileElement.params.sha256 = sha256;
if (elementState == "fileView") {
fileElement.params.visuallyHiddenText = fileElement.params.label;
// Use the actual current path (works for single, draft, edit)
var basePath = window.location.pathname.replace(/\/$/, "");
// View link
fileElement.params.viewHref = basePath + "/view-file/" + elementName;
fileElement.params.viewTarget = "_blank";
// Delete link (preserve ?route=review if present)
fileElement.params.deleteHref = basePath + "/delete-file/" + elementName
+ (route !== null ? "?route=" + encodeURIComponent(route) : "");
}
// Construct the JSONTemplate
var JSONTemplate = {
"elements": [fileElement]
};
//render HTML into string
var renderedHtml = renderer.renderFromJSON(JSONTemplate, inputData);
var outerElement = document.getElementById(`${elementId}-outer-control`)
|| document.getElementById(`${elementId}-input-control`)
|| document.getElementById(`${elementId}-view-control`);
if (outerElement) {
//remove all classes from outerElement
outerElement.className = "";
//set the id of the outerElement to `${elementId}-outer-control`
outerElement.id = `${elementId}-outer-control`;
//update DOM and initialize the JS components
renderer.updateDOMAndInitialize(`${elementId}-outer-control`, renderedHtml);
}
}