UNPKG

@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
'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