budibase/packages/client/src/components/app/pdf/PDF.svelte

197 lines
5.3 KiB
Svelte

<script lang="ts">
import { getContext, onMount, tick } from "svelte"
import { Heading, Button } from "@budibase/bbui"
import { htmlToPdf, pxToPt, A4HeightPx, type PDFOptions } from "./pdf"
import { GridRowHeight } from "@/constants"
import CustomThemeWrapper from "@/components/CustomThemeWrapper.svelte"
const component = getContext("component")
const { styleable, Block, BlockComponent } = getContext("sdk")
export let fileName: string | undefined
export let buttonText: string | undefined
// Derive dimension calculations
const DesiredRows = 40
const innerPageHeightPx = GridRowHeight * DesiredRows
const doubleMarginPx = A4HeightPx - innerPageHeightPx
const marginPt = pxToPt(doubleMarginPx / 2)
let rendering = false
let pageCount = 1
let ref: HTMLElement
let gridRef: HTMLElement
$: safeName = fileName || "Report"
$: safeButtonText = buttonText || "Download PDF"
$: heightPx = pageCount * innerPageHeightPx + doubleMarginPx
$: pageStyle = `--height:${heightPx}px; --margin:${marginPt}pt;`
$: gridMinHeight = pageCount * DesiredRows * GridRowHeight
const generatePDF = async () => {
rendering = true
await tick()
preprocessCSS()
try {
const opts: PDFOptions = {
fileName: safeName,
marginPt,
footer: true,
}
await htmlToPdf(ref, opts)
} catch (error) {
console.error("Error rendering PDF", error)
}
rendering = false
}
const preprocessCSS = () => {
const els = document.getElementsByClassName(
"grid-child"
) as unknown as HTMLElement[]
for (let el of els) {
const styles = window.getComputedStyle(el)
el.style.setProperty("grid-column-end", styles.gridColumnEnd, "important")
}
}
const getDividerStyle = (idx: number) => {
const top = (idx + 1) * innerPageHeightPx + doubleMarginPx / 2
return `--idx:"${idx + 1}"; --top:${top}px;`
}
const handleGridMutation = () => {
const rows = parseInt(gridRef.dataset.requiredRows || "1")
const nextPageCount = Math.max(1, Math.ceil(rows / DesiredRows))
if (nextPageCount > pageCount || !gridRef.classList.contains("highlight")) {
pageCount = nextPageCount
}
}
onMount(() => {
// Observe required content rows and use this to determine required pages
const gridDOMID = `${$component.id}-grid-dom`
gridRef = document.getElementsByClassName(gridDOMID)[0] as HTMLElement
const mutationObserver = new MutationObserver(handleGridMutation)
mutationObserver.observe(gridRef, {
attributes: true,
attributeFilter: ["data-required-rows", "class"],
})
return () => {
mutationObserver.disconnect()
}
})
</script>
<Block>
<div class="wrapper" style="--margin:{marginPt}pt;">
<div class="container" use:styleable={$component.styles}>
<div class="title">
<Heading size="M">{safeName}</Heading>
<Button disabled={rendering} cta on:click={generatePDF}>
{safeButtonText}
</Button>
</div>
<div class="page" style={pageStyle}>
{#if pageCount > 1}
{#each { length: pageCount } as _, idx}
<div
class="divider"
class:last={idx === pageCount - 1}
style={getDividerStyle(idx)}
/>
{/each}
{/if}
<div
class="spectrum spectrum--medium spectrum--light pageContent"
bind:this={ref}
>
<CustomThemeWrapper popoverRoot={false}>
<BlockComponent
type="container"
props={{ layout: "grid" }}
styles={{
normal: {
height: `${gridMinHeight}px`,
},
}}
context="grid"
>
<slot />
</BlockComponent>
</CustomThemeWrapper>
</div>
</div>
</div>
</div>
</Block>
<style>
.wrapper {
width: 100%;
height: 100%;
padding: 64px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
}
.container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
width: 595.28pt;
gap: var(--spacing-xl);
align-self: center;
}
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.page {
width: 595.28pt;
min-height: var(--height);
padding: var(--margin);
background-color: white;
flex: 0 0 auto;
display: flex;
justify-content: flex-start;
align-items: stretch;
box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.1);
flex-direction: column;
margin: 0 auto;
position: relative;
}
.pageContent {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
background: white;
}
.divider {
width: 100%;
height: 2px;
background: var(--spectrum-global-color-static-gray-400);
position: absolute;
left: 0;
top: var(--top);
transform: translateY(-50%);
}
.divider.last {
top: calc(var(--top) + var(--margin));
background: transparent;
}
/*.divider::after {*/
/* position: absolute;*/
/* top: -32px;*/
/* right: 24px;*/
/* content: var(--idx);*/
/* color: var(--spectrum-global-color-static-gray-400);*/
/* text-align: right;*/
/*}*/
</style>