feat: base html sheet

This commit is contained in:
Matthieu CAILLEAUX
2022-04-26 21:11:35 +02:00
parent 986d50a5eb
commit b16967c351
16 changed files with 309 additions and 45 deletions

11
package-lock.json generated
View File

@@ -519,6 +519,12 @@
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
"dev": true "dev": true
}, },
"@types/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
"dev": true
},
"@types/jquery": { "@types/jquery": {
"version": "3.5.11", "version": "3.5.11",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.11.tgz", "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.11.tgz",
@@ -1224,6 +1230,11 @@
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
}, },
"file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
"fill-range": { "fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",

View File

@@ -14,7 +14,8 @@
"typescript": "4.5.4", "typescript": "4.5.4",
"ts-loader": "9.2.6", "ts-loader": "9.2.6",
"webpack": "5.65.0", "webpack": "5.65.0",
"webpack-cli": "4.9.1" "webpack-cli": "4.9.1",
"@types/file-saver": "2.0.5"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@@ -22,6 +23,7 @@
} }
}, },
"dependencies": { "dependencies": {
"jspdf": "2.4.0" "jspdf": "2.4.0",
"file-saver": "2.0.5"
} }
} }

9
src/abstract-builder.ts Normal file
View File

@@ -0,0 +1,9 @@
import { AbstractElement } from './elements/abstract-element';
export abstract class AbstractBuilder {
public abstract build(elements: AbstractElement[]);
public abstract getLabelledRowHeight(): number;
public abstract save(name: string);
}

View File

@@ -6,4 +6,6 @@ export const isGM = () => user()?.isGM ?? false;
export const TEXT_SIZE = 8; export const TEXT_SIZE = 8;
export const LABEL_SIZE = 6; export const LABEL_SIZE = 6;
export const HTML_TEXT_SIZE = 1;
export const HTML_LABEL_SIZE = 0.75;
export const MARGINS = { top: 10, left: 10, bottom: 10, right: 10 }; export const MARGINS = { top: 10, left: 10, bottom: 10, right: 10 };

View File

@@ -28,6 +28,13 @@ export abstract class AbstractElement {
public abstract render(doc: jsPDF, maxWidth?: number): jsPDF; public abstract render(doc: jsPDF, maxWidth?: number): jsPDF;
public abstract renderHtml(
doc: Document,
parent: HTMLElement,
cssRules: string[],
sheet: HTMLStyleElement
): Document;
public abstract getHeight(doc?: jsPDF): number; public abstract getHeight(doc?: jsPDF): number;
public abstract getCheckNewPageHeight(doc?: jsPDF): number; public abstract getCheckNewPageHeight(doc?: jsPDF): number;

View File

@@ -20,6 +20,33 @@ export class Box extends AbstractElement {
return doc; return doc;
} }
public renderHtml(
doc: Document,
parent: HTMLElement,
cssRules: string[],
sheet: HTMLStyleElement
): Document {
const div = doc.createElement('div');
const css = `box-${this.w ?? 0}-${this.h ?? 0}`;
div.classList.add(`box`);
div.classList.add(css);
if (!cssRules.includes(css)) {
cssRules.push(css);
let rule = 'width: 100%;';
if (this.w > 0) {
rule += `max-width: ${this.w}px;`;
rule += `min-width: ${this.w}px;`;
}
if (this.h > 0) {
rule += `max-height: ${this.h}px;`;
rule += `min-height: ${this.h}px;`;
}
sheet.innerHTML += ` .${css} { ${rule} }`;
}
parent.append(div);
return doc;
}
public getHeight(_doc): number { public getHeight(_doc): number {
return this.h; return this.h;
} }

View File

@@ -29,6 +29,23 @@ export class Column extends AbstractElement {
return doc; return doc;
} }
public renderHtml(
doc: Document,
parent: HTMLElement,
cssRules: string[],
sheet: HTMLStyleElement
): Document {
const div = doc.createElement('div');
div.classList.add(`column`);
const elements = this.elements ?? [];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
element.renderHtml(doc, div, cssRules, sheet);
}
parent.append(div);
return doc;
}
public getHeight(doc): number { public getHeight(doc): number {
return this.elements.length > 0 return this.elements.length > 0
? this.elements ? this.elements

View File

@@ -21,6 +21,34 @@ export class Image extends Box {
return doc; return doc;
} }
public renderHtml(
doc: Document,
parent: HTMLElement,
cssRules: string[],
sheet: HTMLStyleElement
): Document {
const img = doc.createElement('img');
img.src = this.imageData;
const css = `img-${this.w ?? 0}-${this.h ?? 0}`;
img.classList.add(`img`);
img.classList.add(css);
if (!cssRules.includes(css)) {
cssRules.push(css);
let rule = '';
if (this.w > 0) {
rule += `width: ${this.w}px;`;
}
if (this.h > 0) {
rule += `height: ${this.h}px;`;
}
if (rule.length > 0) {
sheet.innerHTML += ` .${css} { ${rule} }`;
}
}
parent.append(img);
return doc;
}
public getElements(): AbstractElement[] { public getElements(): AbstractElement[] {
return [this]; return [this];
} }

View File

@@ -1,6 +1,12 @@
import jsPDF, { TextOptionsLight } from 'jspdf'; import jsPDF, { TextOptionsLight } from 'jspdf';
import { Text } from './text'; import { Text } from './text';
import { i18nLocalize, LABEL_SIZE, TEXT_SIZE } from '../constants'; import {
HTML_LABEL_SIZE,
HTML_TEXT_SIZE,
i18nLocalize,
LABEL_SIZE,
TEXT_SIZE,
} from '../constants';
import { AbstractElement } from './abstract-element'; import { AbstractElement } from './abstract-element';
export class LabelledText extends Text { export class LabelledText extends Text {
@@ -40,6 +46,36 @@ export class LabelledText extends Text {
return doc; return doc;
} }
public renderHtml(
doc: Document,
parent: HTMLElement,
cssRules: string[],
sheet: HTMLStyleElement
): Document {
const div = doc.createElement('div');
div.classList.add(`column`);
const label = doc.createElement('p');
const text = doc.createElement('p');
const labelCss = `label-${LABEL_SIZE}`;
const textCss = `text-${TEXT_SIZE}`;
label.classList.add(labelCss);
text.classList.add(textCss);
if (!cssRules.includes(labelCss)) {
cssRules.push(labelCss);
sheet.innerHTML += ` .${labelCss} { font-size: ${HTML_LABEL_SIZE}rem }`;
}
if (!cssRules.includes(textCss)) {
cssRules.push(textCss);
sheet.innerHTML += ` .${textCss} { font-size: ${HTML_TEXT_SIZE}rem }`;
}
label.innerHTML = i18nLocalize(this.label);
text.innerHTML = i18nLocalize(this.text);
div.append(label);
div.append(text);
parent.append(div);
return doc;
}
protected updateMaxWidth(maxWidth?: number) { protected updateMaxWidth(maxWidth?: number) {
if (maxWidth != null && maxWidth > 0) { if (maxWidth != null && maxWidth > 0) {
this.maxWidth = maxWidth; this.maxWidth = maxWidth;

View File

@@ -1,6 +1,6 @@
import { AbstractElement } from './abstract-element'; import { AbstractElement } from './abstract-element';
import jsPDF, { TextOptionsLight } from 'jspdf'; import jsPDF, { TextOptionsLight } from 'jspdf';
import { i18nLocalize, TEXT_SIZE } from '../constants'; import { HTML_TEXT_SIZE, i18nLocalize, TEXT_SIZE } from '../constants';
import { Text } from './text'; import { Text } from './text';
export class MultilineText extends Text { export class MultilineText extends Text {
@@ -32,6 +32,25 @@ export class MultilineText extends Text {
return doc; return doc;
} }
public renderHtml(
doc: Document,
parent: HTMLElement,
cssRules: string[],
sheet: HTMLStyleElement
): Document {
const text = doc.createElement('p');
const css = `text-${TEXT_SIZE}`;
text.classList.add(`multiline-text`);
text.classList.add(css);
if (!cssRules.includes(css)) {
cssRules.push(css);
sheet.innerHTML += ` .${css} { font-size: ${HTML_TEXT_SIZE}rem }`;
}
text.innerHTML = i18nLocalize(this.text);
parent.append(text);
return doc;
}
public getHeight(doc): number { public getHeight(doc): number {
return this.getHeightFromPx(doc, TEXT_SIZE) * this.nbrLine; return this.getHeightFromPx(doc, TEXT_SIZE) * this.nbrLine;
} }

View File

@@ -60,6 +60,23 @@ export class Row extends AbstractElement {
return doc; return doc;
} }
public renderHtml(
doc: Document,
parent: HTMLElement,
cssRules: string[],
sheet: HTMLStyleElement
): Document {
const div = doc.createElement('div');
div.classList.add(`row`);
const elements = this.elements ?? [];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
element.renderHtml(doc, div, cssRules, sheet);
}
parent.append(div);
return doc;
}
public getHeight(doc?: jsPDF): number { public getHeight(doc?: jsPDF): number {
let maxHeight = 0; let maxHeight = 0;
for (const element of this.elements) { for (const element of this.elements) {

View File

@@ -33,6 +33,18 @@ export class Separator extends AbstractElement {
return doc; return doc;
} }
public renderHtml(
doc: Document,
parent: HTMLElement,
_cssRules: string[],
_sheet: HTMLStyleElement
): Document {
const div = doc.createElement('div');
div.classList.add(`separator`);
parent.append(div);
return doc;
}
public getElements(): AbstractElement[] { public getElements(): AbstractElement[] {
return [this]; return [this];
} }

View File

@@ -1,6 +1,6 @@
import { AbstractElement } from './abstract-element'; import { AbstractElement } from './abstract-element';
import jsPDF, { TextOptionsLight } from 'jspdf'; import jsPDF, { TextOptionsLight } from 'jspdf';
import { i18nLocalize, TEXT_SIZE } from '../constants'; import { HTML_TEXT_SIZE, i18nLocalize, TEXT_SIZE } from '../constants';
export class Text extends AbstractElement { export class Text extends AbstractElement {
public text: string; public text: string;
@@ -36,6 +36,25 @@ export class Text extends AbstractElement {
return doc; return doc;
} }
public renderHtml(
doc: Document,
parent: HTMLElement,
cssRules: string[],
sheet: HTMLStyleElement
): Document {
const text = doc.createElement('p');
const css = `text-${TEXT_SIZE}`;
text.classList.add(`ellipsis`);
text.classList.add(css);
text.innerHTML = i18nLocalize(i18nLocalize(this.text));
if (!cssRules.includes(css)) {
cssRules.push(css);
sheet.innerHTML += ` .${css} { font-size: ${HTML_TEXT_SIZE}rem }`;
}
parent.append(text);
return doc;
}
protected updateMaxWidth(maxWidth?: number) { protected updateMaxWidth(maxWidth?: number) {
if (maxWidth != null && maxWidth > 0) { if (maxWidth != null && maxWidth > 0) {
this.maxWidth = maxWidth; this.maxWidth = maxWidth;

50
src/html-builder.ts Normal file
View File

@@ -0,0 +1,50 @@
import { AbstractElement } from './elements/abstract-element';
import { AbstractBuilder } from './abstract-builder';
import { saveAs } from 'file-saver';
import { i18n } from './constants';
export class HtmlBuilder extends AbstractBuilder {
public doc: Document;
public styleSheet: HTMLStyleElement;
constructor() {
super();
this.doc = document.implementation.createHTMLDocument();
const style = document.createElement('style');
style.innerHTML = '.column { display: flex; flex-direction: column; }';
style.innerHTML +=
' .row { display: flex; flex-direction: row; gap: 10px; }';
style.innerHTML +=
' .separator { border: 1px solid; width: 100%; height: 0px }';
style.appendChild(document.createTextNode(''));
this.doc.head.appendChild(style);
this.styleSheet = style;
const meta = document.createElement('meta');
meta.setAttribute('charset', 'UTF-8');
this.doc.head.appendChild(meta);
}
public getLabelledRowHeight(): number {
return 0;
}
public save(name: string) {
this.doc.title = name;
const blob = new Blob(
[
`<html lang="${i18n().lang}">
${this.doc.documentElement.innerHTML}
</html>`,
],
{ type: 'text/html;charset=utf-8' }
);
saveAs(blob, `${name}.html`);
}
public build(elements: AbstractElement[]) {
const cssList: string[] = [];
for (const element of elements) {
element.renderHtml(this.doc, this.doc.body, cssList, this.styleSheet);
}
}
}

View File

@@ -4,13 +4,7 @@ import { Row } from './elements/row';
import { Image } from './elements/image'; import { Image } from './elements/image';
import { Box } from './elements/box'; import { Box } from './elements/box';
import { Util } from './util'; import { Util } from './util';
import { import { i18nLocalize, isGM, MARGINS } from './constants';
i18nLocalize,
isGM,
LABEL_SIZE,
MARGINS,
TEXT_SIZE,
} from './constants';
import { ItemData } from '@league-of-foundry-developers/foundry-vtt-types/src/foundry/common/data/data.mjs'; import { ItemData } from '@league-of-foundry-developers/foundry-vtt-types/src/foundry/common/data/data.mjs';
import { LabelledValues } from './elements/labelled-values'; import { LabelledValues } from './elements/labelled-values';
import { Text } from './elements/text'; import { Text } from './elements/text';
@@ -18,9 +12,12 @@ import { Texts } from './elements/texts';
import { Column } from './elements/column'; import { Column } from './elements/column';
import { Separator } from './elements/separator'; import { Separator } from './elements/separator';
import { Blank } from './elements/blank'; import { Blank } from './elements/blank';
import { AbstractBuilder } from './abstract-builder';
import { HtmlBuilder } from './html-builder';
Hooks.on('getActorDirectoryEntryContext', async (_, options) => { Hooks.on('getActorDirectoryEntryContext', async (_, options) => {
options.push({ options.push(
{
name: i18nLocalize('WFRP4SHEETPRINT.export.pdf'), name: i18nLocalize('WFRP4SHEETPRINT.export.pdf'),
condition: isGM(), condition: isGM(),
icon: '<i class="fas fa-file-pdf"></i>', icon: '<i class="fas fa-file-pdf"></i>',
@@ -28,7 +25,11 @@ Hooks.on('getActorDirectoryEntryContext', async (_, options) => {
const actor: Actor & any = (<any>game).actors.get( const actor: Actor & any = (<any>game).actors.get(
target.attr('data-document-id') target.attr('data-document-id')
); );
await generatePdf(actor); const docBuilder = new PdfBuilder({
orientation: 'p',
unit: 'mm',
});
await generate(actor, docBuilder);
}, },
}, },
{ {
@@ -39,9 +40,11 @@ Hooks.on('getActorDirectoryEntryContext', async (_, options) => {
const actor: Actor & any = (<any>game).actors.get( const actor: Actor & any = (<any>game).actors.get(
target.attr('data-document-id') target.attr('data-document-id')
); );
await generateHtml(actor); const docBuilder = new HtmlBuilder();
await generate(actor, docBuilder);
}, },
}); }
);
}); });
Hooks.on( Hooks.on(
@@ -50,7 +53,11 @@ Hooks.on(
const actor: Actor & any = app.actor; const actor: Actor & any = app.actor;
addActorSheetActionButton(html, 'print', async () => { addActorSheetActionButton(html, 'print', async () => {
await generatePdf(actor); const docBuilder = new PdfBuilder({
orientation: 'p',
unit: 'mm',
});
await generate(actor, docBuilder);
}); });
} }
); );
@@ -71,11 +78,7 @@ function addActorSheetActionButton(
title.after(button); title.after(button);
} }
async function generateHtml(actor: Actor & any) { async function generate(actor: Actor & any, docBuilder: AbstractBuilder) {
console.dir(actor);
}
async function generatePdf(actor: Actor & any) {
const actorData = actor.data; const actorData = actor.data;
// @ts-ignore // @ts-ignore
const actorDetails = actorData.data.details; const actorDetails = actorData.data.details;
@@ -91,14 +94,6 @@ async function generatePdf(actor: Actor & any) {
const careerData: ItemData = currentCareer?.data; const careerData: ItemData = currentCareer?.data;
const careerDetail: any = careerData?.data; const careerDetail: any = careerData?.data;
const docBuilder = new PdfBuilder({
orientation: 'p',
unit: 'mm',
});
const labelledRowHeight =
Util.getHeightFromPx(docBuilder.doc, TEXT_SIZE + LABEL_SIZE) + 1;
const skills = new LabelledValues( const skills = new LabelledValues(
0, 0,
0, 0,
@@ -379,6 +374,8 @@ async function generatePdf(actor: Actor & any) {
3 3
); );
const labelledRowHeight = docBuilder.getLabelledRowHeight();
const imageWidth = 25; const imageWidth = 25;
const imageY = labelledRowHeight + MARGINS.top + 2; const imageY = labelledRowHeight + MARGINS.top + 2;
const actorImageElement = const actorImageElement =
@@ -624,5 +621,5 @@ async function generatePdf(actor: Actor & any) {
mutationM, mutationM,
]), ]),
]); ]);
docBuilder.doc.save(`${actor.name}.pdf`); docBuilder.save(`${actor.name}`);
} }

View File

@@ -1,14 +1,25 @@
import jsPDF, { jsPDFOptions } from 'jspdf'; import jsPDF, { jsPDFOptions } from 'jspdf';
import { AbstractElement } from './elements/abstract-element'; import { AbstractElement } from './elements/abstract-element';
import { MARGINS } from './constants'; import { LABEL_SIZE, MARGINS, TEXT_SIZE } from './constants';
import { AbstractBuilder } from './abstract-builder';
import { Util } from './util';
export class PdfBuilder { export class PdfBuilder extends AbstractBuilder {
public doc: jsPDF; public doc: jsPDF;
constructor(options: jsPDFOptions) { constructor(options: jsPDFOptions) {
super();
this.doc = new jsPDF(options); this.doc = new jsPDF(options);
} }
public getLabelledRowHeight(): number {
return Util.getHeightFromPx(this.doc, TEXT_SIZE + LABEL_SIZE) + 1;
}
public save(name: string) {
this.doc.save(`${name}.pdf`);
}
public build(elements: AbstractElement[]) { public build(elements: AbstractElement[]) {
const finalElements: AbstractElement[] = []; const finalElements: AbstractElement[] = [];
for (const element of elements) { for (const element of elements) {