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==",
"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": {
"version": "3.5.11",
"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",
"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": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",

View File

@@ -14,7 +14,8 @@
"typescript": "4.5.4",
"ts-loader": "9.2.6",
"webpack": "5.65.0",
"webpack-cli": "4.9.1"
"webpack-cli": "4.9.1",
"@types/file-saver": "2.0.5"
},
"husky": {
"hooks": {
@@ -22,6 +23,7 @@
}
},
"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 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 };

View File

@@ -28,6 +28,13 @@ export abstract class AbstractElement {
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 getCheckNewPageHeight(doc?: jsPDF): number;

View File

@@ -20,6 +20,33 @@ export class Box extends AbstractElement {
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 {
return this.h;
}

View File

@@ -29,6 +29,23 @@ export class Column extends AbstractElement {
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 {
return this.elements.length > 0
? this.elements

View File

@@ -21,6 +21,34 @@ export class Image extends Box {
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[] {
return [this];
}

View File

@@ -1,6 +1,12 @@
import jsPDF, { TextOptionsLight } from 'jspdf';
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';
export class LabelledText extends Text {
@@ -40,6 +46,36 @@ export class LabelledText extends Text {
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) {
if (maxWidth != null && maxWidth > 0) {
this.maxWidth = maxWidth;

View File

@@ -1,6 +1,6 @@
import { AbstractElement } from './abstract-element';
import jsPDF, { TextOptionsLight } from 'jspdf';
import { i18nLocalize, TEXT_SIZE } from '../constants';
import { HTML_TEXT_SIZE, i18nLocalize, TEXT_SIZE } from '../constants';
import { Text } from './text';
export class MultilineText extends Text {
@@ -32,6 +32,25 @@ export class MultilineText extends Text {
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 {
return this.getHeightFromPx(doc, TEXT_SIZE) * this.nbrLine;
}

View File

@@ -60,6 +60,23 @@ export class Row extends AbstractElement {
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 {
let maxHeight = 0;
for (const element of this.elements) {

View File

@@ -33,6 +33,18 @@ export class Separator extends AbstractElement {
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[] {
return [this];
}

View File

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

View File

@@ -1,14 +1,25 @@
import jsPDF, { jsPDFOptions } from 'jspdf';
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;
constructor(options: jsPDFOptions) {
super();
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[]) {
const finalElements: AbstractElement[] = [];
for (const element of elements) {