@gov-cy/govcy-express-services 1.0.0-alpha.2 → 1.0.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1051 -83
- package/package.json +10 -3
- package/src/auth/cyLoginAuth.mjs +2 -1
- package/src/index.mjs +20 -1
- package/src/middleware/cyLoginAuth.mjs +11 -1
- package/src/middleware/govcyCsrf.mjs +15 -1
- package/src/middleware/govcyFileDeleteHandler.mjs +320 -0
- package/src/middleware/govcyFileUpload.mjs +36 -0
- package/src/middleware/govcyFileViewHandler.mjs +161 -0
- package/src/middleware/govcyFormsPostHandler.mjs +1 -1
- package/src/middleware/govcyHttpErrorHandler.mjs +4 -3
- package/src/middleware/govcyPDFRender.mjs +3 -1
- package/src/middleware/govcyPageHandler.mjs +3 -3
- package/src/middleware/govcyPageRender.mjs +10 -0
- package/src/middleware/govcyReviewPageHandler.mjs +4 -1
- package/src/middleware/govcyReviewPostHandler.mjs +1 -1
- package/src/middleware/govcySuccessPageHandler.mjs +2 -3
- package/src/public/js/govcyFiles.js +299 -0
- package/src/public/js/govcyForms.js +19 -8
- package/src/resources/govcyResources.mjs +85 -4
- package/src/utils/govcyApiDetection.mjs +17 -0
- package/src/utils/govcyApiRequest.mjs +30 -5
- package/src/utils/govcyApiResponse.mjs +31 -0
- package/src/utils/govcyConstants.mjs +5 -1
- package/src/utils/govcyDataLayer.mjs +211 -11
- package/src/utils/govcyExpressions.mjs +1 -1
- package/src/utils/govcyFormHandling.mjs +81 -5
- package/src/utils/govcyHandleFiles.mjs +307 -0
- package/src/utils/govcyLoadSubmissionDataAPIs.mjs +10 -3
- package/src/utils/govcySubmitData.mjs +186 -106
- package/src/utils/govcyTempSave.mjs +2 -1
- package/src/utils/govcyValidator.mjs +7 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
|
|
2
|
+
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
3
|
+
import { getEnvVariable, getEnvVariableBool } from "../utils/govcyEnvVariables.mjs";
|
|
4
|
+
import { ALLOWED_FILE_MIME_TYPES } from "../utils/govcyConstants.mjs";
|
|
5
|
+
import { govcyApiRequest } from "../utils/govcyApiRequest.mjs";
|
|
6
|
+
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
7
|
+
import { logger } from '../utils/govcyLogger.mjs';
|
|
8
|
+
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
9
|
+
import { isMagicByteValid, pageContainsFileInput } from "../utils/govcyHandleFiles.mjs";
|
|
10
|
+
|
|
11
|
+
export function govcyFileViewHandler() {
|
|
12
|
+
return async (req, res, next) => {
|
|
13
|
+
try {
|
|
14
|
+
const { siteId, pageUrl, elementName } = req.params;
|
|
15
|
+
|
|
16
|
+
// Create a deep copy of the service to avoid modifying the original
|
|
17
|
+
let serviceCopy = req.serviceData;
|
|
18
|
+
|
|
19
|
+
// Get the download file configuration
|
|
20
|
+
const downloadCfg = serviceCopy?.site?.fileDownloadAPIEndpoint;
|
|
21
|
+
// Check if download file configuration is available
|
|
22
|
+
if (!downloadCfg?.url || !downloadCfg?.clientKey || !downloadCfg?.serviceId) {
|
|
23
|
+
return handleMiddlewareError(`File download APU configuration not found`, 404, next);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Environment vars
|
|
27
|
+
const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
|
|
28
|
+
let url = getEnvVariable(downloadCfg.url || "", false);
|
|
29
|
+
const clientKey = getEnvVariable(downloadCfg.clientKey || "", false);
|
|
30
|
+
const serviceId = getEnvVariable(downloadCfg.serviceId || "", false);
|
|
31
|
+
const dsfGtwKey = getEnvVariable(downloadCfg?.dsfgtwApiKey || "", "");
|
|
32
|
+
const method = (downloadCfg?.method || "GET").toLowerCase();
|
|
33
|
+
|
|
34
|
+
// Check if the upload API is configured correctly
|
|
35
|
+
if (!url || !clientKey) {
|
|
36
|
+
return handleMiddlewareError(`Missing environment variables for upload`, 404, next);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// ⤵️ Find the current page based on the URL
|
|
41
|
+
const page = getPageConfigData(serviceCopy, pageUrl);
|
|
42
|
+
|
|
43
|
+
// deep copy the page template to avoid modifying the original
|
|
44
|
+
const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
|
|
45
|
+
|
|
46
|
+
// ----- Conditional logic comes here
|
|
47
|
+
// ✅ Skip this POST handler if the page's conditions evaluate to true (redirect away)
|
|
48
|
+
// const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
|
|
49
|
+
// if (conditionResult.result === false) {
|
|
50
|
+
// logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult);
|
|
51
|
+
// return handleMiddlewareError(`Page condition evaluated to true on POST — skipping form save and redirecting`, 404, next);
|
|
52
|
+
// }
|
|
53
|
+
|
|
54
|
+
// Validate the field: Only allow delete if the page contains a fileInput with the given name
|
|
55
|
+
const fileInputElement = pageContainsFileInput(pageTemplateCopy, elementName);
|
|
56
|
+
if (!fileInputElement) {
|
|
57
|
+
return handleMiddlewareError(`File input [${elementName}] not allowed on this page`, 404, next);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//check the reference number
|
|
61
|
+
const referenceNo = dataLayer.getSiteLoadDataReferenceNumber(req.session, siteId);
|
|
62
|
+
if (!referenceNo) {
|
|
63
|
+
return handleMiddlewareError(`Missing submission reference number`, 404, next);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//get element data
|
|
67
|
+
const elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName)
|
|
68
|
+
|
|
69
|
+
// If the element data is not found, return an error response
|
|
70
|
+
if (!elementData || !elementData?.sha256 || !elementData?.fileId) {
|
|
71
|
+
return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Construct the URL with tag being the elementName
|
|
75
|
+
url += `/${encodeURIComponent(referenceNo)}/${encodeURIComponent(elementData.fileId)}/${encodeURIComponent(elementData.sha256)}`;
|
|
76
|
+
|
|
77
|
+
// Get the user
|
|
78
|
+
const user = dataLayer.getUser(req.session);
|
|
79
|
+
// Perform the upload request
|
|
80
|
+
const response = await govcyApiRequest(
|
|
81
|
+
method,
|
|
82
|
+
url,
|
|
83
|
+
{},
|
|
84
|
+
true,
|
|
85
|
+
user,
|
|
86
|
+
{
|
|
87
|
+
accept: "text/plain",
|
|
88
|
+
"client-key": clientKey,
|
|
89
|
+
"service-id": serviceId,
|
|
90
|
+
...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey })
|
|
91
|
+
},
|
|
92
|
+
3,
|
|
93
|
+
allowSelfSignedCerts
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// If not succeeded, handle error
|
|
97
|
+
if (!response?.Succeeded) {
|
|
98
|
+
return handleMiddlewareError(`fileDownloadAPIEndpoint returned succeeded false`, 500, next);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if the response contains the expected data
|
|
102
|
+
if (!response?.Data?.contentType || !response?.Data?.fileName || !response?.Data?.base64) {
|
|
103
|
+
return handleMiddlewareError(`fileDownloadAPIEndpoint - Missing contentType, fileName or base64 in response data`, 500, next);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Get filename
|
|
107
|
+
const filename = response?.Data?.fileName || "filename";
|
|
108
|
+
const fallbackFilename = asciiFallback(filename);
|
|
109
|
+
const utf8Filename = encodeRFC5987(filename);
|
|
110
|
+
|
|
111
|
+
// Decode base64 to binary
|
|
112
|
+
const fileBuffer = Buffer.from(response.Data.base64, 'base64');
|
|
113
|
+
|
|
114
|
+
// file type checks
|
|
115
|
+
// 1. Check declared mimetype
|
|
116
|
+
if (!ALLOWED_FILE_MIME_TYPES.includes(response?.Data?.contentType)) {
|
|
117
|
+
return handleMiddlewareError(`fileDownloadAPIEndpoint - Invalid file type (MIME not allowed)`, 500, next);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. Check actual file content (magic bytes) matches claimed MIME type
|
|
121
|
+
if (!isMagicByteValid(fileBuffer, response?.Data?.contentType)) {
|
|
122
|
+
return handleMiddlewareError(`fileDownloadAPIEndpoint - Invalid file type (magic byte mismatch)`, 500, next);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check if Buffer is empty
|
|
126
|
+
if (!fileBuffer || fileBuffer.length === 0) {
|
|
127
|
+
return handleMiddlewareError(`fileDownloadAPIEndpoint - File is empty or invalid`, 500, next);
|
|
128
|
+
}
|
|
129
|
+
// Send the file to the browser
|
|
130
|
+
res.type(response?.Data?.contentType);
|
|
131
|
+
res.setHeader(
|
|
132
|
+
'Content-Disposition',
|
|
133
|
+
`inline; filename="${fallbackFilename}"; filename*=UTF-8''${utf8Filename}`
|
|
134
|
+
);
|
|
135
|
+
res.send(fileBuffer);
|
|
136
|
+
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger.error("Error in govcyViewFileHandler middleware:", error.message);
|
|
139
|
+
return next(error); // Pass the error to the next middleware
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//------------------------------------------------------------------------------
|
|
145
|
+
// Helper functions
|
|
146
|
+
// rfc5987 encoding for filename*
|
|
147
|
+
function encodeRFC5987(str) {
|
|
148
|
+
return encodeURIComponent(str)
|
|
149
|
+
.replace(/['()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())
|
|
150
|
+
.replace(/%(7C|60|5E)/g, (m, p) => '%' + p); // | ` ^
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ASCII fallback for old browsers
|
|
154
|
+
function asciiFallback(str) {
|
|
155
|
+
return str
|
|
156
|
+
.replace(/[^\x20-\x7E]/g, '') // strip non-ASCII
|
|
157
|
+
.replace(/[/\\?%*:|"<>]/g, '-') // reserved chars
|
|
158
|
+
.replace(/[\r\n]/g, ' ') // drop newlines
|
|
159
|
+
.replace(/"/g, "'") // no quotes
|
|
160
|
+
|| 'download';
|
|
161
|
+
}
|
|
@@ -43,7 +43,7 @@ export function govcyFormsPostHandler() {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// const formData = req.body; // Submitted data
|
|
46
|
-
const formData = getFormData(formElement.params.elements, req.body); // Submitted data
|
|
46
|
+
const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl); // Submitted data
|
|
47
47
|
|
|
48
48
|
// ☑️ Start validation from top-level form elements
|
|
49
49
|
const validationErrors = validateFormElements(formElement.params.elements, formData);
|
|
@@ -3,6 +3,8 @@ import * as govcyResources from "../resources/govcyResources.mjs";
|
|
|
3
3
|
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
4
4
|
import { logger } from "../utils/govcyLogger.mjs";
|
|
5
5
|
import { whatsIsMyEnvironment } from '../utils/govcyEnvVariables.mjs';
|
|
6
|
+
import { errorResponse } from '../utils/govcyApiResponse.mjs';
|
|
7
|
+
import { isApiRequest } from '../utils/govcyApiDetection.mjs';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Middleware function to handle HTTP errors and render appropriate error pages.
|
|
@@ -49,9 +51,8 @@ export function govcyHttpErrorHandler(err, req, res, next) {
|
|
|
49
51
|
|
|
50
52
|
res.status(statusCode);
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return res.json({ error: message });
|
|
54
|
+
if (isApiRequest(req)) {
|
|
55
|
+
return res.status(statusCode).json(errorResponse(statusCode, message));
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// Render an error page for non-JSON requests
|
|
@@ -6,6 +6,7 @@ import { logger } from "../utils/govcyLogger.mjs";
|
|
|
6
6
|
* Middleware function to render PDFs using the GovCy Frontend Renderer.
|
|
7
7
|
* This function takes the processed page data and template, and generates the final PDF response.
|
|
8
8
|
*/
|
|
9
|
+
/* c8 ignore start */
|
|
9
10
|
export function govcyPDFRender() {
|
|
10
11
|
return async (req, res) => {
|
|
11
12
|
try {
|
|
@@ -29,4 +30,5 @@ export function govcyPDFRender() {
|
|
|
29
30
|
res.status(500).send('Unable to generate PDF at this time.');
|
|
30
31
|
}
|
|
31
32
|
};
|
|
32
|
-
}
|
|
33
|
+
}
|
|
34
|
+
/* c8 ignore end */
|
|
@@ -53,8 +53,7 @@ export function govcyPageHandler() {
|
|
|
53
53
|
element.params.method = "POST";
|
|
54
54
|
// ➕ Add CSRF token
|
|
55
55
|
element.params.elements.push(govcyResources.csrfTokenInput(req.csrfToken()));
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
|
|
58
57
|
// 🔍 Find the first button with `prototypeNavigate`
|
|
59
58
|
const button = element.params.elements.find(subElement =>
|
|
60
59
|
// subElement.element === "button" && subElement.params.prototypeNavigate
|
|
@@ -88,7 +87,7 @@ export function govcyPageHandler() {
|
|
|
88
87
|
}
|
|
89
88
|
//--------- End of Handle Validation Errors ---------
|
|
90
89
|
|
|
91
|
-
populateFormData(element.params.elements, theData,validationErrors);
|
|
90
|
+
populateFormData(element.params.elements, theData,validationErrors, req.session, siteId, pageUrl, req.globalLang, null, req.query.route);
|
|
92
91
|
// if there are validation errors, add an error summary
|
|
93
92
|
if (validationErrors?.errorSummary?.length > 0) {
|
|
94
93
|
element.params.elements.unshift(govcyResources.errorSummary(validationErrors.errorSummary));
|
|
@@ -117,3 +116,4 @@ export function govcyPageHandler() {
|
|
|
117
116
|
}
|
|
118
117
|
};
|
|
119
118
|
}
|
|
119
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { govcyFrontendRenderer } from "@gov-cy/govcy-frontend-renderer";
|
|
2
|
+
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Middleware function to render pages using the GovCy Frontend Renderer.
|
|
@@ -6,8 +7,17 @@ import { govcyFrontendRenderer } from "@gov-cy/govcy-frontend-renderer";
|
|
|
6
7
|
*/
|
|
7
8
|
export function renderGovcyPage() {
|
|
8
9
|
return (req, res) => {
|
|
10
|
+
const afterBody = {
|
|
11
|
+
name: "afterBody",
|
|
12
|
+
elements: [
|
|
13
|
+
govcyResources.staticResources.elements["govcyLoadingOverlay"],
|
|
14
|
+
govcyResources.staticResources.elements["govcyFormsJs"]
|
|
15
|
+
]
|
|
16
|
+
};
|
|
17
|
+
// Initialize the renderer
|
|
9
18
|
const renderer = new govcyFrontendRenderer();
|
|
10
19
|
const { processedPage } = req;
|
|
20
|
+
processedPage.pageTemplate.sections.push(afterBody);
|
|
11
21
|
const html = renderer.renderFromJSON(processedPage.pageTemplate, processedPage.pageData);
|
|
12
22
|
res.send(html);
|
|
13
23
|
};
|
|
@@ -79,7 +79,10 @@ export function govcyReviewPageHandler() {
|
|
|
79
79
|
//--------- End Handle Validation Errors ---------
|
|
80
80
|
|
|
81
81
|
// Add elements to the main section, the H1, summary list, the submit button and the JS
|
|
82
|
-
mainElements.push(pageH1,
|
|
82
|
+
mainElements.push(pageH1,
|
|
83
|
+
summaryList,
|
|
84
|
+
submitButton
|
|
85
|
+
);
|
|
83
86
|
// Append generated summary list to the page template
|
|
84
87
|
pageTemplate.sections.push({ name: "main", elements: mainElements });
|
|
85
88
|
|
|
@@ -118,7 +118,7 @@ export function govcyReviewPostHandler() {
|
|
|
118
118
|
|
|
119
119
|
//-- Send email to user
|
|
120
120
|
// Generate the email body
|
|
121
|
-
let emailBody = generateSubmitEmail(service, submissionData.
|
|
121
|
+
let emailBody = generateSubmitEmail(service, submissionData.printFriendlyData, referenceNo, req);
|
|
122
122
|
logger.debug("Email generated:", emailBody);
|
|
123
123
|
// Send the email
|
|
124
124
|
sendEmail(service.site.title[service.site.lang],emailBody,[dataLayer.getUser(req.session).email], "eMail").catch(err => {
|
|
@@ -78,7 +78,7 @@ export function govcySuccessPageHandler(isPDF = false) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
let summaryList = submissionData.
|
|
81
|
+
let summaryList = submissionData.rendererData;
|
|
82
82
|
|
|
83
83
|
let mainElements = [];
|
|
84
84
|
// Add elements to the main section
|
|
@@ -87,8 +87,7 @@ export function govcySuccessPageHandler(isPDF = false) {
|
|
|
87
87
|
weHaveSendYouAnEmail,
|
|
88
88
|
pdfLink,
|
|
89
89
|
theDataFromYourRequest,
|
|
90
|
-
summaryList
|
|
91
|
-
govcyResources.staticResources.elements["govcyFormsJs"]
|
|
90
|
+
summaryList
|
|
92
91
|
);
|
|
93
92
|
// Append generated summary list to the page template
|
|
94
93
|
pageTemplate.sections.push({ name: "main", elements: mainElements });
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// 🔍 Select all file inputs that have the .govcy-file-upload class
|
|
2
|
+
var fileInputs = document.querySelectorAll('input[type="file"].govcy-file-upload');
|
|
3
|
+
|
|
4
|
+
// select overlay and app root elements
|
|
5
|
+
var _govcyOverlay = document.getElementById("govcy--loadingOverlay");
|
|
6
|
+
var _govcyAppRoot = document.getElementById("govcy--body");
|
|
7
|
+
|
|
8
|
+
// Accessibility: Keep track of previously focused element and disabled elements
|
|
9
|
+
var _govcyPrevFocus = null;
|
|
10
|
+
var _govcyDisabledEls = [];
|
|
11
|
+
|
|
12
|
+
// 🔁 Loop over each file input and attach a change event listener
|
|
13
|
+
fileInputs.forEach(function(input) {
|
|
14
|
+
input.addEventListener('change', _uploadFileEventHandler);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Disables all focusable elements within a given root element
|
|
19
|
+
* @param {*} root The root element whose focusable children will be disabled
|
|
20
|
+
*/
|
|
21
|
+
function disableFocusables(root) {
|
|
22
|
+
var sel = 'a[href],area[href],button,input,select,textarea,iframe,summary,[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
|
|
23
|
+
var nodes = root.querySelectorAll(sel);
|
|
24
|
+
_govcyDisabledEls = [];
|
|
25
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
26
|
+
var el = nodes[i];
|
|
27
|
+
if (_govcyOverlay.contains(el)) continue; // don’t disable overlay itself
|
|
28
|
+
var prev = el.getAttribute('tabindex');
|
|
29
|
+
el.setAttribute('data-prev-tabindex', prev === null ? '' : prev);
|
|
30
|
+
el.setAttribute('tabindex', '-1');
|
|
31
|
+
_govcyDisabledEls.push(el);
|
|
32
|
+
}
|
|
33
|
+
root.setAttribute('aria-hidden', 'true'); // hide from AT on fallback
|
|
34
|
+
root.setAttribute('aria-busy', 'true');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Restores all focusable elements within a given root element
|
|
39
|
+
* @param {*} root The root element whose focusable children will be restored
|
|
40
|
+
*/
|
|
41
|
+
function restoreFocusables(root) {
|
|
42
|
+
for (var i = 0; i < _govcyDisabledEls.length; i++) {
|
|
43
|
+
var el = _govcyDisabledEls[i];
|
|
44
|
+
var prev = el.getAttribute('data-prev-tabindex');
|
|
45
|
+
if (prev === '') el.removeAttribute('tabindex'); else el.setAttribute('tabindex', prev);
|
|
46
|
+
el.removeAttribute('data-prev-tabindex');
|
|
47
|
+
}
|
|
48
|
+
_govcyDisabledEls = [];
|
|
49
|
+
root.removeAttribute('aria-hidden');
|
|
50
|
+
root.removeAttribute('aria-busy');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Traps tab key navigation within the overlay
|
|
55
|
+
* @param {*} e The event
|
|
56
|
+
* @returns
|
|
57
|
+
*/
|
|
58
|
+
function trapTab(e) {
|
|
59
|
+
if (e.key !== 'Tab') return;
|
|
60
|
+
var focusables = _govcyOverlay.querySelectorAll('a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])');
|
|
61
|
+
if (focusables.length === 0) { e.preventDefault(); _govcyOverlay.focus(); return; }
|
|
62
|
+
var first = focusables[0], last = focusables[focusables.length - 1];
|
|
63
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
64
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Shows the loading spinner overlay and traps focus within it
|
|
69
|
+
*/
|
|
70
|
+
function showLoadingSpinner() {
|
|
71
|
+
_govcyPrevFocus = document.activeElement;
|
|
72
|
+
_govcyOverlay.setAttribute('aria-hidden', 'false');
|
|
73
|
+
_govcyOverlay.setAttribute('tabindex', '-1');
|
|
74
|
+
_govcyOverlay.style.display = 'flex';
|
|
75
|
+
document.documentElement.style.overflow = 'hidden';
|
|
76
|
+
|
|
77
|
+
if ('inert' in HTMLElement.prototype) { // progressive enhancement
|
|
78
|
+
_govcyAppRoot.inert = true;
|
|
79
|
+
} else {
|
|
80
|
+
disableFocusables(_govcyAppRoot);
|
|
81
|
+
document.addEventListener('keydown', trapTab, true);
|
|
82
|
+
}
|
|
83
|
+
_govcyOverlay.focus();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Hides the loading spinner overlay and restores focus to the previously focused element
|
|
88
|
+
*/
|
|
89
|
+
function hideLoadingSpinner() {
|
|
90
|
+
_govcyOverlay.style.display = 'none';
|
|
91
|
+
_govcyOverlay.setAttribute('aria-hidden', 'true');
|
|
92
|
+
document.documentElement.style.overflow = '';
|
|
93
|
+
|
|
94
|
+
if ('inert' in HTMLElement.prototype) {
|
|
95
|
+
_govcyAppRoot.inert = false;
|
|
96
|
+
} else {
|
|
97
|
+
restoreFocusables(_govcyAppRoot);
|
|
98
|
+
document.removeEventListener('keydown', trapTab, true);
|
|
99
|
+
}
|
|
100
|
+
if (_govcyPrevFocus && _govcyPrevFocus.focus) _govcyPrevFocus.focus();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Handles the upload of a file event
|
|
106
|
+
*
|
|
107
|
+
* @param {object} event The event
|
|
108
|
+
*/
|
|
109
|
+
function _uploadFileEventHandler(event) {
|
|
110
|
+
var input = event.target;
|
|
111
|
+
var messages = {
|
|
112
|
+
"uploadSuccesful": {
|
|
113
|
+
"el": "Το αρχείο ανεβαστηκε",
|
|
114
|
+
"en": "File uploaded successfully",
|
|
115
|
+
"tr": "File uploaded successfully"
|
|
116
|
+
},
|
|
117
|
+
"uploadFailed": {
|
|
118
|
+
"el": "Αποτυχια ανεβασης",
|
|
119
|
+
"en": "File upload failed",
|
|
120
|
+
"tr": "File upload failed"
|
|
121
|
+
},
|
|
122
|
+
"uploadFailed406": {
|
|
123
|
+
"el": "Το επιλεγμένο αρχείο είναι κενό",
|
|
124
|
+
"en": "The selected file is empty",
|
|
125
|
+
"tr": "The selected file is empty"
|
|
126
|
+
},
|
|
127
|
+
"uploadFailed407": {
|
|
128
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
|
|
129
|
+
"en": "The selected file must be a JPG, JPEG, PNG or PDF",
|
|
130
|
+
"tr": "The selected file must be a JPG, JPEG, PNG or PDF"
|
|
131
|
+
},
|
|
132
|
+
"uploadFailed408": {
|
|
133
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
|
|
134
|
+
"en": "The selected file must be a JPG, JPEG, PNG or PDF",
|
|
135
|
+
"tr": "The selected file must be a JPG, JPEG, PNG or PDF"
|
|
136
|
+
},
|
|
137
|
+
"uploadFailed409": {
|
|
138
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι μικρότερο από 4MB",
|
|
139
|
+
"en": "The selected file must be smaller than 4MB",
|
|
140
|
+
"tr": "The selected file must be smaller than 4MB"
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// 🔐 Get the CSRF token from a hidden input field (generated by your backend)
|
|
145
|
+
var csrfEl = document.querySelector('input[type="hidden"][name="_csrf"]');
|
|
146
|
+
var csrfToken = csrfEl ? csrfEl.value : '';
|
|
147
|
+
|
|
148
|
+
// 🔧 Define siteId and pageUrl (you can dynamically extract these later)
|
|
149
|
+
var siteId = window._govcySiteId || "";
|
|
150
|
+
var pageUrl = window._govcyPageUrl || "";
|
|
151
|
+
var lang = window._govcyLang || "el";
|
|
152
|
+
|
|
153
|
+
// 📦 Grab the selected file
|
|
154
|
+
var file = event.target.files[0];
|
|
155
|
+
var elementName = input.name; // Form field's `name` attribute
|
|
156
|
+
var elementId = input.id; // Form field's `id` attribute
|
|
157
|
+
|
|
158
|
+
if (!file) return; // Exit if no file was selected
|
|
159
|
+
|
|
160
|
+
// Show loading spinner
|
|
161
|
+
showLoadingSpinner();
|
|
162
|
+
|
|
163
|
+
// 🧵 Prepare form-data payload for the API
|
|
164
|
+
var formData = new FormData();
|
|
165
|
+
formData.append('file', file); // Attach the actual file
|
|
166
|
+
formData.append('elementName', elementName); // Attach the field name for backend lookup
|
|
167
|
+
|
|
168
|
+
// 🚀 CHANGED: using fetch instead of axios.post
|
|
169
|
+
fetch(`/apis/${siteId}/${pageUrl}/upload`, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: {
|
|
172
|
+
"X-CSRF-Token": csrfToken // 🔐 Pass CSRF token in custom header
|
|
173
|
+
},
|
|
174
|
+
body: formData
|
|
175
|
+
})
|
|
176
|
+
.then(function(response) {
|
|
177
|
+
// 🚀 CHANGED: fetch does not auto-throw on error codes → check manually
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
return response.json().then(function(errData) {
|
|
180
|
+
throw { response: { data: errData } };
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return response.json();
|
|
184
|
+
})
|
|
185
|
+
.then(function(data) {
|
|
186
|
+
// ✅ Success response
|
|
187
|
+
var sha256 = data.Data.sha256;
|
|
188
|
+
var fileId = data.Data.fileId;
|
|
189
|
+
|
|
190
|
+
// 📝 Store returned metadata in hidden fields if needed
|
|
191
|
+
// document.querySelector('[name="' + elementName + 'Attachment[fileId]"]').value = fileId;
|
|
192
|
+
// document.querySelector('[name="' + elementName + 'Attachment[sha256]"]').value = sha256;
|
|
193
|
+
|
|
194
|
+
// Hide loading spinner
|
|
195
|
+
hideLoadingSpinner();
|
|
196
|
+
|
|
197
|
+
// Render the file view
|
|
198
|
+
_renderFileElement("fileView", elementId, elementName, fileId, sha256, null);
|
|
199
|
+
|
|
200
|
+
// Accessibility: Update ARIA live region with success message
|
|
201
|
+
var statusRegion = document.getElementById('_govcy-upload-status');
|
|
202
|
+
if (statusRegion) {
|
|
203
|
+
setTimeout(function() {
|
|
204
|
+
statusRegion.textContent = messages.uploadSuccesful[lang];
|
|
205
|
+
}, 200);
|
|
206
|
+
setTimeout(function() {
|
|
207
|
+
statusRegion.textContent = '';
|
|
208
|
+
}, 5000);
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
.catch(function(err) {
|
|
212
|
+
// ⚠️ Show an error message if upload fails
|
|
213
|
+
var errorMessage = messages.uploadFailed;
|
|
214
|
+
var errorCode = err && err.response && err.response.data && err.response.data.ErrorCode;
|
|
215
|
+
|
|
216
|
+
if (errorCode === 406 || errorCode === 407 || errorCode === 408 || errorCode === 409) {
|
|
217
|
+
errorMessage = messages["uploadFailed" + errorCode];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Hide loading spinner
|
|
221
|
+
hideLoadingSpinner();
|
|
222
|
+
// Render the file input with error
|
|
223
|
+
_renderFileElement("fileInput", elementId, elementName, "", "", errorMessage);
|
|
224
|
+
|
|
225
|
+
// Re-bind the file input's change handler
|
|
226
|
+
var newInput = document.getElementById(elementId);
|
|
227
|
+
if (newInput) {
|
|
228
|
+
newInput.addEventListener('change', _uploadFileEventHandler);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Accessibility: Focus on the form field
|
|
232
|
+
document.getElementById(elementId)?.focus();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Renders a file element in the DOM
|
|
239
|
+
*
|
|
240
|
+
* @param {string} elementState The element state. Can be "fileInput" or "fileView"
|
|
241
|
+
* @param {string} elementId The element id
|
|
242
|
+
* @param {string} elementName The element name
|
|
243
|
+
* @param {string} fileId The file id
|
|
244
|
+
* @param {string} sha256 The sha256
|
|
245
|
+
* @param {object} errorMessage The error message in all supported languages
|
|
246
|
+
*/
|
|
247
|
+
function _renderFileElement(elementState, elementId, elementName, fileId, sha256, errorMessage) {
|
|
248
|
+
|
|
249
|
+
// Grab the query string part (?foo=bar&route=something)
|
|
250
|
+
var queryString = window.location.search;
|
|
251
|
+
// Parse it
|
|
252
|
+
var params = new URLSearchParams(queryString);
|
|
253
|
+
// Get the "route" value (null if not present)
|
|
254
|
+
var route = params.get("route");
|
|
255
|
+
|
|
256
|
+
// Create an instance of GovcyFrontendRendererBrowser
|
|
257
|
+
var renderer = new GovcyFrontendRendererBrowser();
|
|
258
|
+
var lang = window._govcyLang || "el";
|
|
259
|
+
// Define the input data
|
|
260
|
+
var inputData =
|
|
261
|
+
{
|
|
262
|
+
"site": {
|
|
263
|
+
"lang": lang
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
var fileInputMap = JSON.parse(JSON.stringify(window._govcyFileInputs));
|
|
267
|
+
var fileElement = fileInputMap[elementName];
|
|
268
|
+
fileElement.element = elementState;
|
|
269
|
+
if (errorMessage != null) fileElement.params.error = errorMessage;
|
|
270
|
+
if (fileId != null) fileElement.params.fileId = fileId;
|
|
271
|
+
if (sha256 != null) fileElement.params.sha256 = sha256;
|
|
272
|
+
if (elementState == "fileView") {
|
|
273
|
+
fileElement.params.visuallyHiddenText = fileElement.params.label;
|
|
274
|
+
// TODO: Also need to set the `view` and `download` URLs
|
|
275
|
+
fileElement.params.viewHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/view-file/" + elementName;
|
|
276
|
+
fileElement.params.viewTarget = "_blank";
|
|
277
|
+
fileElement.params.deleteHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/delete-file/" + elementName
|
|
278
|
+
+ (route !== null ? "?route=" + encodeURIComponent(route) : "");
|
|
279
|
+
}
|
|
280
|
+
// Construct the JSONTemplate
|
|
281
|
+
var JSONTemplate = {
|
|
282
|
+
"elements": [fileElement]
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
//render HTML into string
|
|
286
|
+
var renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
|
|
287
|
+
var outerElement = document.getElementById(`${elementId}-outer-control`)
|
|
288
|
+
|| document.getElementById(`${elementId}-input-control`)
|
|
289
|
+
|| document.getElementById(`${elementId}-view-control`);
|
|
290
|
+
|
|
291
|
+
if (outerElement) {
|
|
292
|
+
//remove all classes from outerElement
|
|
293
|
+
outerElement.className = "";
|
|
294
|
+
//set the id of the outerElement to `${elementId}-outer-control`
|
|
295
|
+
outerElement.id = `${elementId}-outer-control`;
|
|
296
|
+
//update DOM and initialize the JS components
|
|
297
|
+
renderer.updateDOMAndInitialize(`${elementId}-outer-control`, renderedHtml);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
document.addEventListener("DOMContentLoaded", function () {
|
|
2
2
|
// --- Show conditionals for checked radios ---
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// CHANGED: NodeList.prototype.forEach is not in IE11 → use Array.prototype.forEach.call
|
|
4
|
+
// CHANGED: Arrow function → function
|
|
5
|
+
Array.prototype.forEach.call(
|
|
6
|
+
document.querySelectorAll('.govcy-radio-input[data-aria-controls]:checked'),
|
|
7
|
+
function (radio) { // CHANGED: arrow → function
|
|
8
|
+
// CHANGED: const → var (ES5)
|
|
9
|
+
var targetId = radio.getAttribute('data-aria-controls');
|
|
10
|
+
var targetElement = document.getElementById(targetId);
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
if (targetElement) {
|
|
13
|
+
targetElement.classList.remove('govcy-radio__conditional--hidden');
|
|
14
|
+
}
|
|
9
15
|
}
|
|
10
|
-
|
|
16
|
+
);
|
|
17
|
+
|
|
11
18
|
// --- Disable submit button after form submission ---
|
|
12
|
-
|
|
19
|
+
// CHANGED: NodeList.forEach → Array.prototype.forEach.call
|
|
20
|
+
Array.prototype.forEach.call(document.querySelectorAll('form'), function (form) { // CHANGED
|
|
13
21
|
form.addEventListener('submit', function (e) {
|
|
14
|
-
const
|
|
22
|
+
// CHANGED: const → var (ES5)
|
|
23
|
+
var submitButton = form.querySelector('[type="submit"]');
|
|
15
24
|
if (submitButton) {
|
|
16
25
|
submitButton.disabled = true;
|
|
17
26
|
submitButton.setAttribute('aria-disabled', 'true');
|
|
27
|
+
// (Optional) announce busy state for AT:
|
|
28
|
+
// submitButton.setAttribute('aria-busy', 'true'); // CHANGED: optional a11y improvement
|
|
18
29
|
}
|
|
19
30
|
});
|
|
20
31
|
});
|