@hmlr/govuk-react-components-library
Version:
These are common component use for React applications based on GDS and govuk-frontend
328 lines (315 loc) • 16.1 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
var webpack_mjs = require('pdfjs-dist/webpack.mjs');
var reactRouterDom = require('react-router-dom');
require('govuk-frontend');
const Loading = ({ message = null, html = null }) => {
return (jsxRuntime.jsx("div", { className: "govuk-grid-row", children: jsxRuntime.jsxs("div", { className: "govuk-grid-column-full", children: [message || html ? (jsxRuntime.jsx("div", { className: "govuk-grid-row", children: jsxRuntime.jsx("div", { className: "govuk-grid-column-full head-space", children: html ? (html) : (jsxRuntime.jsx("h2", { className: "govuk-heading-m govuk-!-text-align-centre", children: message })) }) })) : null, jsxRuntime.jsx("div", { className: "govuk-hint govuk-grid-column-full", children: jsxRuntime.jsx("div", { className: "centered-wheel", "data-testid": "centered-wheel-identifier", children: jsxRuntime.jsx("div", { className: "loading-wheel-2", "data-testid": "loading-wheel-2-identifier" }) }) })] }) }));
};
function Slugify(str) {
str = str.replace(/^\s+|\s+$/g, ""); // trim leading/trailing white space
str = str.toLowerCase(); // convert string to lowercase
str = str
.replace(/[^a-z0-9 -]/g, "") // remove any non-alphanumeric characters
.replace(/\s+/g, "-") // replace spaces with hyphens
.replace(/-+/g, "-"); // remove consecutive hyphens
return str;
}
/**
* Convert a base64 string to an ArrayBuffer
*/
const _base64ToArrayBuffer = (base64) => {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
};
/**
* Very small heuristic to detect a plain base64 string
*/
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);
/**
* ResolvePDFSource
*
* Accepts a source that may be:
* - a plain base64 string (PDF binary encoded as base64)
* - a data: URL containing base64 (data:application/pdf;base64,...)
* - an existing blob: URL (blob:...)
* - a Blob/File object
* - a regular URL string (http(s) or same-origin path)
*
* Returns a SourceDeterminer:
* - { isBase64Source: true, source: string } when an object URL was created
* - { isBase64Source: false, source: string } when source can be used directly
*
* If an object URL is created, callers should revoke it when no longer needed:
* `URL.revokeObjectURL(source)`
*/
const ResolvePDFSource = (source) => {
// Nothing provided
if (source === undefined || source === null || source === "") {
return { isBase64Source: false, source: "" };
}
// If source is already a Blob (File), create an object URL
if (typeof source.arrayBuffer === "function" ||
source instanceof Blob) {
const blob = source;
const objectUrl = URL.createObjectURL(blob);
return { isBase64Source: true, source: objectUrl };
}
// If source is a string:
if (typeof source === "string") {
// If it's already a blob URL, return as-is
if (source.startsWith("blob:")) {
return { isBase64Source: false, source };
}
// If it's a data URL (data:application/pdf;base64,....)
if (source.startsWith("data:")) {
// data:[<mediatype>][;base64],<data>
const commaIndex = source.indexOf(",");
if (commaIndex > -1) {
const meta = source.substring(0, commaIndex);
const dataPart = source.substring(commaIndex + 1);
// If it contains base64 marker, decode accordingly
if (meta.indexOf(";base64") !== -1) {
try {
const arrayBuffer = _base64ToArrayBuffer(dataPart);
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
const objectUrl = URL.createObjectURL(blob);
return { isBase64Source: true, source: objectUrl };
}
catch (err) {
console.error("ResolvePDFSource: failed to convert data: URL to blob", err);
// fallback to returning original data URL
return { isBase64Source: false, source };
}
}
else {
// Not base64-encoded data URL — return as is
return { isBase64Source: false, source };
}
}
}
// If it's a plain base64 string (no data: prefix), convert
if (isBase64(source)) {
try {
const arrayBuffer = _base64ToArrayBuffer(source);
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
const objectUrl = URL.createObjectURL(blob);
return { isBase64Source: true, source: objectUrl };
}
catch (err) {
console.error("ResolvePDFSource: failed to convert base64 to blob", err);
return { isBase64Source: false, source };
}
}
// Otherwise treat as a normal URL (http(s) or same-origin path)
return { isBase64Source: false, source };
}
// Fallback: convert to string
try {
const asString = String(source);
return { isBase64Source: false, source: asString };
}
catch (err) {
console.error("ResolvePDFSource: failed to convert source to string", err);
return { isBase64Source: false, source: "" };
}
};
const LinkWithRef = ({ children, to, href, forwardedRef = null, ...attributes }) => {
if (to) {
return (jsxRuntime.jsx(reactRouterDom.Link, { ref: forwardedRef, to: to, ...attributes, children: children }));
}
return (jsxRuntime.jsx("a", { ref: forwardedRef, href: href || "#", ...attributes, children: children }));
};
const Button = (props) => {
const { element, href, to, isStartButton, disabled, className, preventDoubleClick, name, type, children, ...attributes } = props;
let el = "";
let buttonAttributes = {
name,
type,
...attributes,
"data-module": "govuk-button",
};
let buttonElement = null;
if (element) {
el = element;
}
else if (href || to) {
el = "a";
}
else {
el = "button";
}
let iconHtml;
if (isStartButton) {
iconHtml = (jsxRuntime.jsx("svg", { className: "govuk-button__start-icon", xmlns: "http://www.w3.org/2000/svg", width: "17.5", height: "19", viewBox: "0 0 33 40", "aria-hidden": "true", focusable: "false", children: jsxRuntime.jsx("path", { fill: "currentColor", d: "M0 0h13l20 20-20 20H0l20-20z" }) }));
}
const commonAttributes = {
className: `govuk-button ${className || ""}${disabled ? " govuk-button--disabled" : ""} ${isStartButton ? "govuk-button--start" : ""}`,
// ref: buttonRef,
};
if (preventDoubleClick) {
buttonAttributes["data-prevent-double-click"] = preventDoubleClick;
}
if (disabled) {
buttonAttributes = {
...buttonAttributes,
"aria-disabled": "true",
disabled: true,
};
}
if (el === "a") {
const linkAttributes = {
...commonAttributes,
role: "button",
draggable: "false",
...attributes,
"data-module": "govuk-button",
href,
to,
};
buttonElement = (jsxRuntime.jsxs(LinkWithRef, { ...linkAttributes, children: [children, iconHtml] }));
}
else if (el === "button") {
buttonElement = (jsxRuntime.jsxs("button", { ...buttonAttributes, ...commonAttributes, children: [children, iconHtml] }));
}
else if (el === "input") {
if (!type) {
buttonAttributes.type = "submit";
}
buttonElement = (jsxRuntime.jsx("input", { value: children, ...buttonAttributes, ...commonAttributes }));
}
return buttonElement;
};
Button.displayName = "Button";
function titleCase(str) {
return str
.toLowerCase()
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
const Label = ({ className = "", htmlFor, children, isPageHeading, ...attributes }) => {
if (!children)
return null;
const label = (jsxRuntime.jsx("label", { className: `govuk-label ${className}`, htmlFor: htmlFor, ...attributes, children: children }));
return isPageHeading ? (jsxRuntime.jsx("h1", { className: "govuk-label-wrapper", children: label })) : (label);
};
const DifferenceNavigation = ({ differenceId, setDifferenceFocus, totalDifferences, keyword = "variation", plural = "variations", }) => {
if (totalDifferences === 0) {
return (jsxRuntime.jsxs("p", { className: "govuk-body govuk-!-font-size-20 govuk-!-text-align-centre", children: ["No ", `${plural}`, " found"] }));
}
const isPreviousDisabled = differenceId <= 1;
const isNextDisabled = differenceId === totalDifferences;
const renderButton = (id, onClick, disabled, content) => (jsxRuntime.jsx(Button, { id: id, onClick: onClick, "data-testid": id, disabled: disabled, children: content }));
return (jsxRuntime.jsxs("div", { className: "govuk-grid-row", children: [jsxRuntime.jsx("div", { className: "govuk-grid-column-one-third", children: jsxRuntime.jsx("div", { className: "govuk-!-text-align-left", children: renderButton(`previous-${keyword}`, () => setDifferenceFocus(differenceId - 1), isPreviousDisabled, jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [" ", jsxRuntime.jsx("svg", { className: "govuk-button__start-icon back-button", xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", transform: "rotate(180)", children: jsxRuntime.jsx("path", { fill: "currentColor", d: "M8.122 24l-4.122-4 8-8-8-8 4.122-4 11.878 12z" }) }), "\u00A0Previous", " "] })) }) }), jsxRuntime.jsx("div", { className: "govuk-grid-column-one-third", children: jsxRuntime.jsxs(Label, { className: "govuk-!-text-align-centre", children: [titleCase(keyword), " ", differenceId, " of ", totalDifferences] }) }), jsxRuntime.jsx("div", { className: "govuk-grid-column-one-third", children: jsxRuntime.jsx("div", { className: "govuk-!-text-align-right", children: renderButton(`next-${keyword}`, () => setDifferenceFocus(differenceId + 1), isNextDisabled, jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [" ", "Next", jsxRuntime.jsx("svg", { className: "govuk-button__start-icon back-button", xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: jsxRuntime.jsx("path", { fill: "currentColor", d: "M8.122 24l-4.122-4 8-8-8-8 4.122-4 11.878 12z" }) })] })) }) })] }));
};
const ErrorMessage = ({ className, children, visuallyHiddenText = "Error", ...attributes }) => {
let visuallyHiddenTextComponent = null;
if (visuallyHiddenText) {
visuallyHiddenTextComponent = (jsxRuntime.jsxs("span", { className: "govuk-visually-hidden", children: [visuallyHiddenText, ": "] }));
}
return (jsxRuntime.jsxs("p", { className: `govuk-error-message ${className || ""}`, ...attributes, children: [visuallyHiddenTextComponent, children] }));
};
/**
* PDFViewerCanvas component for rendering PDF documents on a canvas.
* @param {PDFViewerCanvasProps} props - The component props
* @returns {JSX.Element} The rendered component
*/
const PDFViewerCanvas = ({ src, className, documentName, pageNumber = 1, showNavigation = true, ...attributes }) => {
const [pdfState, setPdfState] = react.useState({
loading: true,
numberOfPages: 0,
currentPage: pageNumber,
errorMessage: "",
});
const canvasRef = react.useRef(null);
const pdfDocumentRef = react.useRef(null);
const pageRenderingRef = react.useRef(false);
const pageNumberPendingRef = react.useRef(null);
const scale = 1;
const renderPage = react.useCallback((pageNum) => {
const pdfDocument = pdfDocumentRef.current;
const canvas = canvasRef.current;
if (!pdfDocument || !canvas)
return;
pageRenderingRef.current = true;
pdfDocument.getPage(pageNum).then((page) => {
const viewport = page.getViewport({ scale });
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext("2d");
if (context) {
const renderContext = {
canvasContext: context,
viewport,
annotationMode: 3,
};
const renderTask = page.render(renderContext);
renderTask.promise.then(() => {
pageRenderingRef.current = false;
if (pageNumberPendingRef.current !== null) {
renderPage(pageNumberPendingRef.current);
pageNumberPendingRef.current = null;
}
});
}
});
}, [scale]);
const queueRenderPage = react.useCallback((pageNum) => {
if (pageRenderingRef.current) {
pageNumberPendingRef.current = pageNum;
}
else {
renderPage(pageNum);
}
}, [renderPage]);
const setDifferenceFocus = react.useCallback((pageNum) => {
if (pageNum < 1 || pageNum > pdfState.numberOfPages)
return;
setPdfState((prevState) => ({
...prevState,
currentPage: pageNum,
}));
queueRenderPage(pageNum);
}, [pdfState.numberOfPages, queueRenderPage]);
react.useEffect(() => {
const loadPDF = async (file) => {
try {
setPdfState((prevState) => ({ ...prevState, loading: true }));
const source = ResolvePDFSource(file).source;
const pdf = await webpack_mjs.getDocument(source).promise;
pdfDocumentRef.current = pdf;
const initialPageNumber = pdf.numPages >= pageNumber ? pageNumber : 1;
setPdfState({
loading: false,
numberOfPages: pdf.numPages,
currentPage: initialPageNumber,
errorMessage: "",
});
renderPage(initialPageNumber);
}
catch (error) {
console.error("Error loading PDF:", error);
setPdfState((prevState) => ({
...prevState,
loading: false,
errorMessage: documentName
? `There was an error loading the PDF document called "${documentName}".`
: "There was an error loading the PDF document.",
}));
}
};
if (src) {
loadPDF(src);
}
}, [src, pageNumber, renderPage]);
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [pdfState.loading && jsxRuntime.jsx(Loading, { message: "Loading PDF Document on Canvas" }), pdfState.errorMessage && (jsxRuntime.jsx(ErrorMessage, { className: "govuk-!-text-align-centre", children: pdfState.errorMessage })), showNavigation && pdfState.numberOfPages > 1 && (jsxRuntime.jsx("div", { style: { margin: "0 auto" }, children: jsxRuntime.jsx(DifferenceNavigation, { differenceId: pdfState.currentPage, setDifferenceFocus: setDifferenceFocus, totalDifferences: pdfState.numberOfPages, keyword: "page", plural: "Pages" }) })), jsxRuntime.jsx("canvas", { className: className, ref: canvasRef, id: `viewer-${Slugify(documentName || "")}`, "data-testid": "viewer", style: { width: "100%", height: "100%" }, ...attributes })] }));
};
exports.PDFViewerCanvas = PDFViewerCanvas;
//# sourceMappingURL=PDFViewerCanvas.cjs.js.map