PDF Designer Variablen Plugin
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
"esbuild": "^0.18.17",
|
||||
"express": "^4.18.2",
|
||||
"flag-icons": "^6.9.2",
|
||||
"fontkit": "^2.0.2",
|
||||
"i18next": "^23.4.1",
|
||||
"i18next-fs-backend": "^2.1.5",
|
||||
"i18next-http-backend": "^2.2.1",
|
||||
@@ -68,6 +69,7 @@
|
||||
"@faker-js/faker": "^8.3.1",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/body-scroll-lock": "^3.1.2",
|
||||
"@types/fontkit": "^2.0.6",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
||||
|
||||
@@ -78,7 +78,12 @@
|
||||
</li>
|
||||
<li>
|
||||
<a use:ripple={rippleOptions} class="button-tab" href="/dashboard/admin/pdf-designer">
|
||||
PDF Erstellen
|
||||
PDF Designer
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a use:ripple={rippleOptions} class="button-tab" href="/dashboard/admin/pdf-viewer">
|
||||
PDF Viewer
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
26
src/lib/pdf/plugins/variables/constants.ts
Normal file
26
src/lib/pdf/plugins/variables/constants.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ALIGNMENT, VERTICAL_ALIGNMENT, DYNAMIC_FONT_SIZE_FIT } from './types';
|
||||
|
||||
export const DEFAULT_FONT_SIZE = 13;
|
||||
|
||||
export const ALIGN_LEFT = 'left' as ALIGNMENT;
|
||||
export const ALIGN_CENTER = 'center' as ALIGNMENT;
|
||||
export const ALIGN_RIGHT = 'right' as ALIGNMENT;
|
||||
export const DEFAULT_ALIGNMENT = ALIGN_LEFT;
|
||||
export const VERTICAL_ALIGN_TOP = 'top' as VERTICAL_ALIGNMENT;
|
||||
export const VERTICAL_ALIGN_MIDDLE = 'middle' as VERTICAL_ALIGNMENT;
|
||||
export const VERTICAL_ALIGN_BOTTOM = 'bottom' as VERTICAL_ALIGNMENT;
|
||||
export const DEFAULT_VERTICAL_ALIGNMENT = VERTICAL_ALIGN_TOP;
|
||||
export const DEFAULT_LINE_HEIGHT = 1;
|
||||
export const DEFAULT_CHARACTER_SPACING = 0;
|
||||
export const DEFAULT_FONT_COLOR = '#000000';
|
||||
export const PLACEHOLDER_FONT_COLOR = '#A0A0A0';
|
||||
export const DYNAMIC_FIT_VERTICAL = 'vertical' as DYNAMIC_FONT_SIZE_FIT;
|
||||
export const DYNAMIC_FIT_HORIZONTAL = 'horizontal' as DYNAMIC_FONT_SIZE_FIT;
|
||||
export const DEFAULT_DYNAMIC_FIT = DYNAMIC_FIT_VERTICAL;
|
||||
export const DEFAULT_DYNAMIC_MIN_FONT_SIZE = 4;
|
||||
|
||||
export const DEFAULT_DYNAMIC_MAX_FONT_SIZE = 72;
|
||||
export const FONT_SIZE_ADJUSTMENT = 0.25;
|
||||
|
||||
export const DEFAULT_OPACITY = 1;
|
||||
export const HEX_COLOR_PATTERN = '^#(?:[A-Fa-f0-9]{3,4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$';
|
||||
311
src/lib/pdf/plugins/variables/index.ts
Normal file
311
src/lib/pdf/plugins/variables/index.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import {
|
||||
ZOOM,
|
||||
Plugin,
|
||||
Schema,
|
||||
PropPanel,
|
||||
DEFAULT_FONT_NAME,
|
||||
getFallbackFontName,
|
||||
PropPanelSchema,
|
||||
PropPanelWidgetProps,
|
||||
} from "@pdfme/common";
|
||||
import { image, text } from "@pdfme/schemas";
|
||||
import type { TextSchema } from "@pdfme/schemas/dist/types/src/text/types";
|
||||
|
||||
import {
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_ALIGNMENT,
|
||||
DEFAULT_VERTICAL_ALIGNMENT,
|
||||
DEFAULT_CHARACTER_SPACING,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
VERTICAL_ALIGN_TOP,
|
||||
VERTICAL_ALIGN_MIDDLE,
|
||||
VERTICAL_ALIGN_BOTTOM,
|
||||
DEFAULT_FONT_COLOR,
|
||||
DYNAMIC_FIT_VERTICAL,
|
||||
DYNAMIC_FIT_HORIZONTAL,
|
||||
DEFAULT_DYNAMIC_FIT,
|
||||
DEFAULT_DYNAMIC_MIN_FONT_SIZE,
|
||||
DEFAULT_DYNAMIC_MAX_FONT_SIZE,
|
||||
ALIGN_RIGHT,
|
||||
ALIGN_CENTER,
|
||||
DEFAULT_OPACITY,
|
||||
HEX_COLOR_PATTERN,
|
||||
} from "./constants";
|
||||
import {
|
||||
GebaeudeStammdaten,
|
||||
Rechnungen,
|
||||
VerbrauchsausweisWohnen,
|
||||
} from "@ibcornelsen/database/client";
|
||||
|
||||
const UseDynamicFontSize = (props: PropPanelWidgetProps) => {
|
||||
const { rootElement, changeSchemas, activeSchema, i18n } = props;
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = Boolean((activeSchema as any)?.dynamicFontSize);
|
||||
checkbox.onchange = (e: any) => {
|
||||
const val = e.target.checked
|
||||
? {
|
||||
min: DEFAULT_DYNAMIC_MIN_FONT_SIZE,
|
||||
max: DEFAULT_DYNAMIC_MAX_FONT_SIZE,
|
||||
fit: DEFAULT_DYNAMIC_FIT,
|
||||
}
|
||||
: undefined;
|
||||
changeSchemas([
|
||||
{ key: "dynamicFontSize", value: val, schemaId: activeSchema.id },
|
||||
]);
|
||||
};
|
||||
const label = document.createElement("label");
|
||||
label.innerText = i18n("schemas.text.dynamicFontSize") || "";
|
||||
label.style.cssText = "display: flex; width: 100%;";
|
||||
label.appendChild(checkbox);
|
||||
rootElement.appendChild(label);
|
||||
};
|
||||
|
||||
type AusweisIndex = keyof VerbrauchsausweisWohnen
|
||||
| `gebaeude_stammdaten.${keyof GebaeudeStammdaten}`
|
||||
| `rechnung.${keyof Rechnungen}`;
|
||||
|
||||
const sampleData: Partial<Record<AusweisIndex , string>> = {
|
||||
"gebaeude_stammdaten.adresse": "Musterstraße 123",
|
||||
"gebaeude_stammdaten.plz": "12345",
|
||||
"gebaeude_stammdaten.ort": "Musterstadt",
|
||||
"gebaeude_stammdaten.baujahr_gebaeude": "1990",
|
||||
"gebaeude_stammdaten.baujahr_heizung": "2000",
|
||||
}
|
||||
|
||||
const variableOptions: {
|
||||
label: string;
|
||||
value: AusweisIndex;
|
||||
}[] = [
|
||||
{
|
||||
label: "Gebäude -> Adresse",
|
||||
value: "gebaeude_stammdaten.adresse",
|
||||
},
|
||||
{
|
||||
label: "Gebäude -> PLZ",
|
||||
value: "gebaeude_stammdaten.plz",
|
||||
},
|
||||
{
|
||||
label: "Gebaeude -> Ort",
|
||||
value: "gebaeude_stammdaten.ort",
|
||||
},
|
||||
{
|
||||
label: "Gebäude -> Baujahr Gebäude",
|
||||
value: "gebaeude_stammdaten.baujahr_gebaeude",
|
||||
},
|
||||
{
|
||||
label: "Gebäude -> Baujahr Heizung",
|
||||
value: "gebaeude_stammdaten.baujahr_heizung",
|
||||
},
|
||||
{
|
||||
label: "Gebäude -> Fläche",
|
||||
value: "gebaeude_stammdaten.flaeche"
|
||||
},
|
||||
{
|
||||
label: "Gebäude -> Einheiten",
|
||||
value: "gebaeude_stammdaten.einheiten"
|
||||
}
|
||||
];
|
||||
|
||||
interface VariableSchema extends TextSchema {
|
||||
variable: AusweisIndex | undefined
|
||||
}
|
||||
|
||||
const propPanel: PropPanel<VariableSchema> = {
|
||||
schema: ({ options, activeSchema, i18n }) => {
|
||||
const font = options.font || {
|
||||
[DEFAULT_FONT_NAME]: { data: "", fallback: true },
|
||||
};
|
||||
const fontNames = Object.keys(font);
|
||||
const fallbackFontName = getFallbackFontName(font);
|
||||
|
||||
const enableDynamicFont = Boolean(
|
||||
(activeSchema as any)?.dynamicFontSize
|
||||
);
|
||||
|
||||
const textSchema: Record<string, PropPanelSchema> = {
|
||||
variable: {
|
||||
title: "Variable",
|
||||
type: "string",
|
||||
widget: "select",
|
||||
props: { options: variableOptions },
|
||||
span: 24,
|
||||
},
|
||||
fontName: {
|
||||
title: i18n("schemas.text.fontName"),
|
||||
type: "string",
|
||||
widget: "select",
|
||||
default: fallbackFontName,
|
||||
props: {
|
||||
options: fontNames.map((name) => ({
|
||||
label: name,
|
||||
value: name,
|
||||
})),
|
||||
},
|
||||
span: 12,
|
||||
},
|
||||
fontSize: {
|
||||
title: i18n("schemas.text.size"),
|
||||
type: "number",
|
||||
widget: "inputNumber",
|
||||
span: 6,
|
||||
disabled: enableDynamicFont,
|
||||
},
|
||||
characterSpacing: {
|
||||
title: i18n("schemas.text.spacing"),
|
||||
type: "number",
|
||||
widget: "inputNumber",
|
||||
span: 6,
|
||||
},
|
||||
alignment: {
|
||||
title: i18n("schemas.text.textAlign"),
|
||||
type: "string",
|
||||
widget: "select",
|
||||
props: {
|
||||
options: [
|
||||
{
|
||||
label: i18n("schemas.left"),
|
||||
value: DEFAULT_ALIGNMENT,
|
||||
},
|
||||
{ label: i18n("schemas.center"), value: ALIGN_CENTER },
|
||||
{ label: i18n("schemas.right"), value: ALIGN_RIGHT },
|
||||
],
|
||||
},
|
||||
span: 8,
|
||||
},
|
||||
verticalAlignment: {
|
||||
title: i18n("schemas.text.verticalAlign"),
|
||||
type: "string",
|
||||
widget: "select",
|
||||
props: {
|
||||
options: [
|
||||
{
|
||||
label: i18n("schemas.top"),
|
||||
value: VERTICAL_ALIGN_TOP,
|
||||
},
|
||||
{
|
||||
label: i18n("schemas.middle"),
|
||||
value: VERTICAL_ALIGN_MIDDLE,
|
||||
},
|
||||
{
|
||||
label: i18n("schemas.bottom"),
|
||||
value: VERTICAL_ALIGN_BOTTOM,
|
||||
},
|
||||
],
|
||||
},
|
||||
span: 8,
|
||||
},
|
||||
lineHeight: {
|
||||
title: i18n("schemas.text.lineHeight"),
|
||||
type: "number",
|
||||
widget: "inputNumber",
|
||||
props: {
|
||||
step: 0.1,
|
||||
},
|
||||
span: 8,
|
||||
},
|
||||
useDynamicFontSize: {
|
||||
type: "boolean",
|
||||
widget: "UseDynamicFontSize",
|
||||
bind: false,
|
||||
span: 16,
|
||||
},
|
||||
dynamicFontSize: {
|
||||
type: "object",
|
||||
widget: "card",
|
||||
column: 3,
|
||||
properties: {
|
||||
min: {
|
||||
title: i18n("schemas.text.min"),
|
||||
type: "number",
|
||||
widget: "inputNumber",
|
||||
hidden: !enableDynamicFont,
|
||||
},
|
||||
max: {
|
||||
title: i18n("schemas.text.max"),
|
||||
type: "number",
|
||||
widget: "inputNumber",
|
||||
hidden: !enableDynamicFont,
|
||||
},
|
||||
fit: {
|
||||
title: i18n("schemas.text.fit"),
|
||||
type: "string",
|
||||
widget: "select",
|
||||
hidden: !enableDynamicFont,
|
||||
props: {
|
||||
options: [
|
||||
{
|
||||
label: i18n("schemas.horizontal"),
|
||||
value: DYNAMIC_FIT_HORIZONTAL,
|
||||
},
|
||||
{
|
||||
label: i18n("schemas.vertical"),
|
||||
value: DYNAMIC_FIT_VERTICAL,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fontColor: {
|
||||
title: i18n("schemas.textColor"),
|
||||
type: "string",
|
||||
widget: "color",
|
||||
rules: [
|
||||
{
|
||||
pattern: HEX_COLOR_PATTERN,
|
||||
message: i18n("hexColorPrompt"),
|
||||
},
|
||||
],
|
||||
},
|
||||
backgroundColor: {
|
||||
title: i18n("schemas.bgColor"),
|
||||
type: "string",
|
||||
widget: "color",
|
||||
rules: [
|
||||
{
|
||||
pattern: HEX_COLOR_PATTERN,
|
||||
message: i18n("hexColorPrompt"),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return textSchema;
|
||||
},
|
||||
widgets: { UseDynamicFontSize },
|
||||
defaultValue: "Type Something...",
|
||||
defaultSchema: {
|
||||
type: "variable",
|
||||
position: { x: 0, y: 0 },
|
||||
width: 45,
|
||||
height: 10,
|
||||
rotate: 0,
|
||||
alignment: DEFAULT_ALIGNMENT,
|
||||
verticalAlignment: DEFAULT_VERTICAL_ALIGNMENT,
|
||||
fontSize: DEFAULT_FONT_SIZE,
|
||||
lineHeight: DEFAULT_LINE_HEIGHT,
|
||||
characterSpacing: DEFAULT_CHARACTER_SPACING,
|
||||
dynamicFontSize: undefined,
|
||||
fontColor: DEFAULT_FONT_COLOR,
|
||||
fontName: undefined,
|
||||
backgroundColor: "",
|
||||
opacity: DEFAULT_OPACITY,
|
||||
variable: undefined
|
||||
},
|
||||
};
|
||||
|
||||
export const variable: Plugin<VariableSchema> = {
|
||||
ui: (props) => {
|
||||
if (props.schema.variable) {
|
||||
props.value = sampleData[props.schema.variable] as string;
|
||||
}
|
||||
return text.ui(props);
|
||||
},
|
||||
pdf: (props) => {
|
||||
props.value = props.schema.variable as string;
|
||||
text.pdf(props);
|
||||
},
|
||||
propPanel,
|
||||
};
|
||||
29
src/lib/pdf/plugins/variables/types.ts
Normal file
29
src/lib/pdf/plugins/variables/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Schema } from '@pdfme/common';
|
||||
import type { Font as FontKitFont } from 'fontkit';
|
||||
|
||||
export type ALIGNMENT = 'left' | 'center' | 'right';
|
||||
export type VERTICAL_ALIGNMENT = 'top' | 'middle' | 'bottom';
|
||||
export type DYNAMIC_FONT_SIZE_FIT = 'horizontal' | 'vertical';
|
||||
|
||||
export type FontWidthCalcValues = {
|
||||
font: FontKitFont;
|
||||
fontSize: number;
|
||||
characterSpacing: number;
|
||||
boxWidthInPt: number;
|
||||
};
|
||||
export interface TextSchema extends Schema {
|
||||
fontName?: string;
|
||||
alignment: ALIGNMENT;
|
||||
verticalAlignment: VERTICAL_ALIGNMENT;
|
||||
fontSize: number;
|
||||
lineHeight: number;
|
||||
characterSpacing: number;
|
||||
dynamicFontSize?: {
|
||||
min: number;
|
||||
max: number;
|
||||
fit: DYNAMIC_FONT_SIZE_FIT;
|
||||
};
|
||||
|
||||
fontColor: string;
|
||||
backgroundColor: string;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Designer } from "@pdfme/ui";
|
||||
import { image, text } from "@pdfme/schemas";
|
||||
import { variable } from "../../lib/pdf/plugins/variables/index"
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import { Template, BLANK_PDF } from "@pdfme/common";
|
||||
@@ -12,12 +14,14 @@
|
||||
],
|
||||
};
|
||||
|
||||
let plugins = { Bild: image, Text: text, Variablen: variable }
|
||||
|
||||
let container: HTMLDivElement;
|
||||
|
||||
let designer: Designer;
|
||||
|
||||
onMount(() => {
|
||||
designer = new Designer({ domContainer: container, template });
|
||||
designer = new Designer({ domContainer: container, template, plugins });
|
||||
});
|
||||
|
||||
function loadBasePDF() {
|
||||
@@ -31,7 +35,7 @@
|
||||
|
||||
const basePdf = e.target.result as string;
|
||||
const newTemplate = { ...template, basePdf };
|
||||
designer = new Designer({ domContainer: container, template: newTemplate });
|
||||
designer = new Designer({ domContainer: container, template: newTemplate, plugins });
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
@@ -74,7 +78,7 @@
|
||||
if (!e.target) return;
|
||||
|
||||
const template = JSON.parse(e.target.result as string) as Template;
|
||||
designer = new Designer({ domContainer: container, template });
|
||||
designer = new Designer({ domContainer: container, template, plugins });
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
@@ -86,7 +90,7 @@
|
||||
let loadTemplateInput: HTMLInputElement;
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<header class="mb-4">
|
||||
<button class="btn btn-secondary" on:click={() => loadTemplateInput.click()}>Change base PDF</button>
|
||||
<button class="btn btn-secondary" on:click={addNewField}>Add new Field</button>
|
||||
<button class="btn btn-secondary" on:click={exportTemplate}>Export</button>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { variable } from "#lib/pdf/plugins/variables";
|
||||
import { Benutzer, GebaeudeStammdaten, Rechnungen, VerbrauchsausweisWohnen } from "@ibcornelsen/database/client";
|
||||
import { Template } from "@pdfme/common";
|
||||
import { Viewer } from "@pdfme/ui";
|
||||
import { Check } from "radix-svelte-icons";
|
||||
import { image, text } from "@pdfme/schemas";
|
||||
|
||||
type AusweisData = VerbrauchsausweisWohnen & { benutzer: Benutzer, gebaeude_stammdaten: GebaeudeStammdaten, rechnungen: Rechnungen }
|
||||
|
||||
@@ -47,7 +50,8 @@
|
||||
viewer = new Viewer({
|
||||
domContainer: pdfViewerContainer,
|
||||
template,
|
||||
inputs: [convertAusweisData(pdfInputs)]
|
||||
inputs: [convertAusweisData(pdfInputs)],
|
||||
plugins: { text, image, variable}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -64,10 +68,13 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (viewer) viewer.destroy();
|
||||
|
||||
viewer = new Viewer({
|
||||
domContainer: pdfViewerContainer,
|
||||
template,
|
||||
inputs: [convertAusweisData(pdfInputs)]
|
||||
inputs: [convertAusweisData(pdfInputs)],
|
||||
plugins: { text, image, variable}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,20 +83,21 @@
|
||||
|
||||
<header>
|
||||
<div>
|
||||
<button class="btn btn-secondary" on:click={loadTemplate}>Load Template</button>
|
||||
<button class="btn btn-secondary mb-4" on:click={loadTemplate}>Load Template</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="grid grid-cols-[1fr_4fr]">
|
||||
<main class="grid grid-cols-[1fr_4fr] gap-4">
|
||||
<div>
|
||||
<h1>Ausweise</h1>
|
||||
{#each ausweise as ausweis}
|
||||
<div>
|
||||
<h2 class="text-black">{ausweis.gebaeude_stammdaten.adresse}</h2>
|
||||
<button class="btn btn-secondary" on:click={() => {
|
||||
changeInputs(ausweis)
|
||||
}}>Diesen Ausweis als Basis nehmen</button>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#each ausweise as ausweis}
|
||||
<div class="rounded-lg border p-2 flex flex-row items-center justify-between">
|
||||
<h2 class="text-black">{ausweis.gebaeude_stammdaten.adresse}</h2>
|
||||
<button class="btn btn-square btn-ghost p-1.5" on:click={() => {
|
||||
changeInputs(ausweis)
|
||||
}}><Check size={20}/></button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div bind:this={pdfViewerContainer}></div>
|
||||
</main>
|
||||
Reference in New Issue
Block a user