@hmlr/govuk-react-components-library 1.0.2 → 1.0.3
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/bundle-analysis-main.html +1 -3
- package/bundle-analysis-pdfvc.html +1 -1
- package/dist/PDFViewerCanvas.cjs.js +91 -7
- package/dist/PDFViewerCanvas.cjs.js.map +1 -1
- package/dist/PDFViewerCanvas.esm.js +91 -7
- package/dist/PDFViewerCanvas.esm.js.map +1 -1
- package/dist/index.cjs.css +6 -1
- package/dist/index.cjs.js +270 -36
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.css +6 -1
- package/dist/index.esm.js +271 -37
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/PDFViewer/ResolvePDFSource.d.ts +24 -1
- package/package.json +44 -44
package/dist/index.esm.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
|
2
2
|
import { Accordion as Accordion$1, createAll, Button as Button$1, Checkboxes as Checkboxes$1, ErrorSummary as ErrorSummary$1, Header as Header$1, Radios as Radios$1, SkipLink as SkipLink$1, Tabs as Tabs$1 } from 'govuk-frontend';
|
|
3
3
|
import { Link, useLocation } from 'react-router-dom';
|
|
4
4
|
import * as React from 'react';
|
|
5
|
-
import React__default, { useContext, useMemo, createElement, Component, useState, useEffect } from 'react';
|
|
5
|
+
import React__default, { useContext, useMemo, createElement, Component, useState, useRef, useEffect } from 'react';
|
|
6
6
|
|
|
7
7
|
function ConfigureOverallAccordion($scope, config) {
|
|
8
8
|
if (JSON.stringify(config) === JSON.stringify({})) {
|
|
@@ -1011,28 +1011,281 @@ function Slugify(str) {
|
|
|
1011
1011
|
return str;
|
|
1012
1012
|
}
|
|
1013
1013
|
|
|
1014
|
+
/**
|
|
1015
|
+
* Convert a base64 string to an ArrayBuffer
|
|
1016
|
+
*/
|
|
1017
|
+
const _base64ToArrayBuffer = (base64) => {
|
|
1018
|
+
const binaryString = window.atob(base64);
|
|
1019
|
+
const len = binaryString.length;
|
|
1020
|
+
const bytes = new Uint8Array(len);
|
|
1021
|
+
for (let i = 0; i < len; i++) {
|
|
1022
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
1023
|
+
}
|
|
1024
|
+
return bytes.buffer;
|
|
1025
|
+
};
|
|
1026
|
+
/**
|
|
1027
|
+
* Very small heuristic to detect a plain base64 string
|
|
1028
|
+
*/
|
|
1029
|
+
const isBase64 = (value) => /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/.test(value);
|
|
1030
|
+
/**
|
|
1031
|
+
* ResolvePDFSource
|
|
1032
|
+
*
|
|
1033
|
+
* Accepts a source that may be:
|
|
1034
|
+
* - a plain base64 string (PDF binary encoded as base64)
|
|
1035
|
+
* - a data: URL containing base64 (data:application/pdf;base64,...)
|
|
1036
|
+
* - an existing blob: URL (blob:...)
|
|
1037
|
+
* - a Blob/File object
|
|
1038
|
+
* - a regular URL string (http(s) or same-origin path)
|
|
1039
|
+
*
|
|
1040
|
+
* Returns a SourceDeterminer:
|
|
1041
|
+
* - { isBase64Source: true, source: string } when an object URL was created
|
|
1042
|
+
* - { isBase64Source: false, source: string } when source can be used directly
|
|
1043
|
+
*
|
|
1044
|
+
* If an object URL is created, callers should revoke it when no longer needed:
|
|
1045
|
+
* `URL.revokeObjectURL(source)`
|
|
1046
|
+
*/
|
|
1047
|
+
const ResolvePDFSource = (source) => {
|
|
1048
|
+
// Nothing provided
|
|
1049
|
+
if (source === undefined || source === null || source === "") {
|
|
1050
|
+
return { isBase64Source: false, source: "" };
|
|
1051
|
+
}
|
|
1052
|
+
// If source is already a Blob (File), create an object URL
|
|
1053
|
+
if (typeof source.arrayBuffer === "function" ||
|
|
1054
|
+
source instanceof Blob) {
|
|
1055
|
+
const blob = source;
|
|
1056
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
1057
|
+
return { isBase64Source: true, source: objectUrl };
|
|
1058
|
+
}
|
|
1059
|
+
// If source is a string:
|
|
1060
|
+
if (typeof source === "string") {
|
|
1061
|
+
// If it's already a blob URL, return as-is
|
|
1062
|
+
if (source.startsWith("blob:")) {
|
|
1063
|
+
return { isBase64Source: false, source };
|
|
1064
|
+
}
|
|
1065
|
+
// If it's a data URL (data:application/pdf;base64,....)
|
|
1066
|
+
if (source.startsWith("data:")) {
|
|
1067
|
+
// data:[<mediatype>][;base64],<data>
|
|
1068
|
+
const commaIndex = source.indexOf(",");
|
|
1069
|
+
if (commaIndex > -1) {
|
|
1070
|
+
const meta = source.substring(0, commaIndex);
|
|
1071
|
+
const dataPart = source.substring(commaIndex + 1);
|
|
1072
|
+
// If it contains base64 marker, decode accordingly
|
|
1073
|
+
if (meta.indexOf(";base64") !== -1) {
|
|
1074
|
+
try {
|
|
1075
|
+
const arrayBuffer = _base64ToArrayBuffer(dataPart);
|
|
1076
|
+
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
|
1077
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
1078
|
+
return { isBase64Source: true, source: objectUrl };
|
|
1079
|
+
}
|
|
1080
|
+
catch (err) {
|
|
1081
|
+
console.error("ResolvePDFSource: failed to convert data: URL to blob", err);
|
|
1082
|
+
// fallback to returning original data URL
|
|
1083
|
+
return { isBase64Source: false, source };
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
// Not base64-encoded data URL — return as is
|
|
1088
|
+
return { isBase64Source: false, source };
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
// If it's a plain base64 string (no data: prefix), convert
|
|
1093
|
+
if (isBase64(source)) {
|
|
1094
|
+
try {
|
|
1095
|
+
const arrayBuffer = _base64ToArrayBuffer(source);
|
|
1096
|
+
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
|
1097
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
1098
|
+
return { isBase64Source: true, source: objectUrl };
|
|
1099
|
+
}
|
|
1100
|
+
catch (err) {
|
|
1101
|
+
console.error("ResolvePDFSource: failed to convert base64 to blob", err);
|
|
1102
|
+
return { isBase64Source: false, source };
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
// Otherwise treat as a normal URL (http(s) or same-origin path)
|
|
1106
|
+
return { isBase64Source: false, source };
|
|
1107
|
+
}
|
|
1108
|
+
// Fallback: convert to string
|
|
1109
|
+
try {
|
|
1110
|
+
const asString = String(source);
|
|
1111
|
+
return { isBase64Source: false, source: asString };
|
|
1112
|
+
}
|
|
1113
|
+
catch (err) {
|
|
1114
|
+
console.error("ResolvePDFSource: failed to convert source to string", err);
|
|
1115
|
+
return { isBase64Source: false, source: "" };
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1014
1119
|
const PDFViewer = (props) => {
|
|
1015
1120
|
const viewerRef = React__default.useRef(null);
|
|
1016
|
-
const { iframeId, src, viewerLocation, documentName, documentNameColour, backend, toolbar = "minimal", additionalBackendAttributes,
|
|
1121
|
+
const { iframeId, src, viewerLocation, documentName, documentNameColour, backend, toolbar = "minimal", additionalBackendAttributes,
|
|
1122
|
+
// optional: lets callers skip client-side fetch for remote resources
|
|
1123
|
+
disableClientFetch = false, ...attributes } = props;
|
|
1017
1124
|
const [loading, setLoading] = useState(false);
|
|
1125
|
+
// ref for object URL (either created by ResolvePDFSource or by us after fetching)
|
|
1126
|
+
const objectUrlRef = useRef(null);
|
|
1127
|
+
// did ResolvePDFSource create the object URL? (helps understanding where to revoke)
|
|
1128
|
+
const createdByResolveRef = useRef(false);
|
|
1129
|
+
// fetch abort controller for cross-origin remote fetches
|
|
1130
|
+
const fetchControllerRef = useRef(null);
|
|
1131
|
+
const isRemoteUrl = (url) => {
|
|
1132
|
+
if (!url)
|
|
1133
|
+
return false;
|
|
1134
|
+
// Check if it's an HTTP(S) URL
|
|
1135
|
+
return /^https?:\/\//i.test(url);
|
|
1136
|
+
};
|
|
1018
1137
|
useEffect(() => {
|
|
1138
|
+
let mounted = true;
|
|
1019
1139
|
setLoading(true);
|
|
1020
1140
|
const element = viewerRef.current;
|
|
1021
|
-
if (!element)
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1141
|
+
if (!element) {
|
|
1142
|
+
setLoading(false);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
// cleanup previous URL/fetch if any
|
|
1146
|
+
if (fetchControllerRef.current) {
|
|
1147
|
+
try {
|
|
1148
|
+
fetchControllerRef.current.abort();
|
|
1149
|
+
}
|
|
1150
|
+
catch (err) {
|
|
1151
|
+
console.error("PDFViewer: fetch abort error:", err);
|
|
1152
|
+
/* ignore */
|
|
1153
|
+
}
|
|
1154
|
+
fetchControllerRef.current = null;
|
|
1155
|
+
}
|
|
1156
|
+
if (objectUrlRef.current) {
|
|
1157
|
+
try {
|
|
1158
|
+
URL.revokeObjectURL(objectUrlRef.current);
|
|
1159
|
+
}
|
|
1160
|
+
catch (err) {
|
|
1161
|
+
console.error("PDFViewer: revokeObjectURL error:", err);
|
|
1162
|
+
}
|
|
1163
|
+
objectUrlRef.current = null;
|
|
1164
|
+
createdByResolveRef.current = false;
|
|
1165
|
+
}
|
|
1166
|
+
const init = async () => {
|
|
1167
|
+
let sourceToUse = src;
|
|
1168
|
+
// 1) Let ResolvePDFSource handle base64/data/blob inputs synchronously.
|
|
1169
|
+
try {
|
|
1170
|
+
const resolved = ResolvePDFSource(src);
|
|
1171
|
+
if (resolved && resolved.source) {
|
|
1172
|
+
sourceToUse = resolved.source;
|
|
1173
|
+
if (resolved.isBase64Source) {
|
|
1174
|
+
// ResolvePDFSource created an object URL for us (data/blob/base64)
|
|
1175
|
+
objectUrlRef.current = resolved.source;
|
|
1176
|
+
createdByResolveRef.current = true;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
catch (err) {
|
|
1181
|
+
// If ResolvePDFSource throws, just ignore and fall back to src
|
|
1182
|
+
console.error("PDFViewer: ResolvePDFSource error:", err);
|
|
1183
|
+
}
|
|
1184
|
+
// 2) If ResolvePDFSource didn't create an object URL and the src is cross-origin HTTP(S),
|
|
1185
|
+
// attempt a client-side fetch to create an object URL (so viewer can load it safely).
|
|
1186
|
+
if (!objectUrlRef.current &&
|
|
1187
|
+
!disableClientFetch &&
|
|
1188
|
+
typeof src === "string" &&
|
|
1189
|
+
/^https?:\/\//i.test(src) &&
|
|
1190
|
+
isRemoteUrl(src)) {
|
|
1191
|
+
const controller = new AbortController();
|
|
1192
|
+
fetchControllerRef.current = controller;
|
|
1193
|
+
try {
|
|
1194
|
+
const resp = await fetch(src, {
|
|
1195
|
+
signal: controller.signal,
|
|
1196
|
+
credentials: "omit",
|
|
1197
|
+
});
|
|
1198
|
+
if (!resp.ok) {
|
|
1199
|
+
console.warn(`PDFViewer: fetch returned ${resp.status} ${resp.statusText} for ${src}`);
|
|
1200
|
+
// fallback: keep using original src
|
|
1201
|
+
}
|
|
1202
|
+
else {
|
|
1203
|
+
const blob = await resp.blob();
|
|
1204
|
+
if (blob && blob.size > 0) {
|
|
1205
|
+
const obj = URL.createObjectURL(blob);
|
|
1206
|
+
objectUrlRef.current = obj;
|
|
1207
|
+
createdByResolveRef.current = false; // we created it via fetch
|
|
1208
|
+
sourceToUse = obj;
|
|
1209
|
+
}
|
|
1210
|
+
else {
|
|
1211
|
+
console.warn("PDFViewer: fetched blob is empty - falling back to original src");
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
catch (err) {
|
|
1216
|
+
if (err?.name === "AbortError") {
|
|
1217
|
+
// aborted, nothing to do
|
|
1218
|
+
if (!mounted)
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
console.error("PDFViewer: error fetching cross-origin PDF:", err);
|
|
1223
|
+
}
|
|
1224
|
+
// fallback to source as-is (original remote URL)
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
// 3) Prepare and call backend
|
|
1228
|
+
if (!mounted)
|
|
1229
|
+
return;
|
|
1230
|
+
const jsProps = {
|
|
1231
|
+
iframeId,
|
|
1232
|
+
source: sourceToUse,
|
|
1233
|
+
viewerLocation,
|
|
1234
|
+
documentName,
|
|
1235
|
+
documentNameColour,
|
|
1236
|
+
element,
|
|
1237
|
+
toolbar,
|
|
1238
|
+
...additionalBackendAttributes,
|
|
1239
|
+
};
|
|
1240
|
+
try {
|
|
1241
|
+
backend(jsProps);
|
|
1242
|
+
}
|
|
1243
|
+
catch (err) {
|
|
1244
|
+
console.error("PDFViewer: backend threw an error:", err);
|
|
1245
|
+
}
|
|
1246
|
+
finally {
|
|
1247
|
+
if (mounted)
|
|
1248
|
+
setLoading(false);
|
|
1249
|
+
}
|
|
1250
|
+
}; // end init
|
|
1251
|
+
init();
|
|
1252
|
+
return () => {
|
|
1253
|
+
mounted = false;
|
|
1254
|
+
// cancel any ongoing fetch
|
|
1255
|
+
if (fetchControllerRef.current) {
|
|
1256
|
+
try {
|
|
1257
|
+
fetchControllerRef.current.abort();
|
|
1258
|
+
}
|
|
1259
|
+
catch (err) {
|
|
1260
|
+
console.error("PDFViewer: fetch abort error during cleanup:", err);
|
|
1261
|
+
}
|
|
1262
|
+
fetchControllerRef.current = null;
|
|
1263
|
+
}
|
|
1264
|
+
// revoke any object URL we created
|
|
1265
|
+
if (objectUrlRef.current) {
|
|
1266
|
+
try {
|
|
1267
|
+
URL.revokeObjectURL(objectUrlRef.current);
|
|
1268
|
+
}
|
|
1269
|
+
catch (err) {
|
|
1270
|
+
console.error("PDFViewer: fetch abort error during cleanup:", err);
|
|
1271
|
+
}
|
|
1272
|
+
objectUrlRef.current = null;
|
|
1273
|
+
createdByResolveRef.current = false;
|
|
1274
|
+
}
|
|
1275
|
+
// Note: PDFViewerBackend also attaches an iframe to the element. If backend does not
|
|
1276
|
+
// remove previous iframes, you might want to clear element.children here.
|
|
1032
1277
|
};
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1278
|
+
// re-run when these inputs change
|
|
1279
|
+
}, [
|
|
1280
|
+
src,
|
|
1281
|
+
iframeId,
|
|
1282
|
+
viewerLocation,
|
|
1283
|
+
documentName,
|
|
1284
|
+
documentNameColour,
|
|
1285
|
+
toolbar,
|
|
1286
|
+
backend,
|
|
1287
|
+
disableClientFetch,
|
|
1288
|
+
]);
|
|
1036
1289
|
return (jsxs(Fragment, { children: [loading && jsx(Loading, { message: "Loading PDF Document" }), jsx("div", { ref: viewerRef, id: `viewer ${documentName ? Slugify(documentName) : ""}`, "data-testid": `viewer ${documentName ? Slugify(documentName) : ""}`, style: { width: "100%", height: "100%" }, ...attributes })] }));
|
|
1037
1290
|
};
|
|
1038
1291
|
|
|
@@ -1046,27 +1299,6 @@ const ObjectToQueryString = (queryParameters) => {
|
|
|
1046
1299
|
: "";
|
|
1047
1300
|
};
|
|
1048
1301
|
|
|
1049
|
-
const _base64ToArrayBuffer = (base64) => {
|
|
1050
|
-
const binaryString = window.atob(base64);
|
|
1051
|
-
const len = binaryString.length;
|
|
1052
|
-
const bytes = new Uint8Array(len);
|
|
1053
|
-
for (let i = 0; i < len; i++) {
|
|
1054
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
1055
|
-
}
|
|
1056
|
-
return bytes.buffer;
|
|
1057
|
-
};
|
|
1058
|
-
const isBase64 = (value) => /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/.test(value);
|
|
1059
|
-
const ResolvePDFSource = (source) => {
|
|
1060
|
-
const isBase64Source = isBase64(source);
|
|
1061
|
-
if (!isBase64Source) {
|
|
1062
|
-
return { isBase64Source, source };
|
|
1063
|
-
}
|
|
1064
|
-
const blob = new Blob([_base64ToArrayBuffer(source)], {
|
|
1065
|
-
type: "application/pdf",
|
|
1066
|
-
});
|
|
1067
|
-
return { isBase64Source, source: URL.createObjectURL(blob) };
|
|
1068
|
-
};
|
|
1069
|
-
|
|
1070
1302
|
const PDFViewerBackend = ({ iframeId, source, viewerLocation = "/pdfjs-4.4.168-dist/web/viewer.html", documentName, documentNameColour = "black", toolbar = "minimal", element, ...remaining }) => {
|
|
1071
1303
|
const sourceDeterminer = ResolvePDFSource(source);
|
|
1072
1304
|
const iframe = document.createElement("iframe");
|
|
@@ -1083,6 +1315,8 @@ const PDFViewerBackend = ({ iframeId, source, viewerLocation = "/pdfjs-4.4.168-d
|
|
|
1083
1315
|
iframe.src = `${viewerLocation}${queryString}`;
|
|
1084
1316
|
iframe.style.width = "100%";
|
|
1085
1317
|
iframe.style.height = "94%";
|
|
1318
|
+
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-downloads");
|
|
1319
|
+
iframe.setAttribute("allow", "cross-origin-isolated");
|
|
1086
1320
|
if (sourceDeterminer.isBase64Source) {
|
|
1087
1321
|
iframe.addEventListener("beforeunload", (event) => {
|
|
1088
1322
|
event.preventDefault();
|