diff --git a/package-lock.json b/package-lock.json index 1427173..fde5e27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2e20b57..b2d7fb3 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/abstract-builder.ts b/src/abstract-builder.ts new file mode 100644 index 0000000..66283e5 --- /dev/null +++ b/src/abstract-builder.ts @@ -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); +} diff --git a/src/constants.ts b/src/constants.ts index dd738c1..b93c829 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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 }; diff --git a/src/elements/abstract-element.ts b/src/elements/abstract-element.ts index a443d04..aeb80d5 100644 --- a/src/elements/abstract-element.ts +++ b/src/elements/abstract-element.ts @@ -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; diff --git a/src/elements/box.ts b/src/elements/box.ts index e0534f7..cd059f8 100644 --- a/src/elements/box.ts +++ b/src/elements/box.ts @@ -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; } diff --git a/src/elements/column.ts b/src/elements/column.ts index 1c32baf..4af3854 100644 --- a/src/elements/column.ts +++ b/src/elements/column.ts @@ -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 diff --git a/src/elements/image.ts b/src/elements/image.ts index b7fd76a..86680f6 100644 --- a/src/elements/image.ts +++ b/src/elements/image.ts @@ -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]; } diff --git a/src/elements/labelled-text.ts b/src/elements/labelled-text.ts index 8b23966..92ceb5b 100644 --- a/src/elements/labelled-text.ts +++ b/src/elements/labelled-text.ts @@ -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; diff --git a/src/elements/multiline-text.ts b/src/elements/multiline-text.ts index e748de2..e273f35 100644 --- a/src/elements/multiline-text.ts +++ b/src/elements/multiline-text.ts @@ -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; } diff --git a/src/elements/row.ts b/src/elements/row.ts index 6ca13a3..7c4c836 100644 --- a/src/elements/row.ts +++ b/src/elements/row.ts @@ -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) { diff --git a/src/elements/separator.ts b/src/elements/separator.ts index e0b045b..b2f41dd 100644 --- a/src/elements/separator.ts +++ b/src/elements/separator.ts @@ -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]; } diff --git a/src/elements/text.ts b/src/elements/text.ts index 1267bc7..0e164ec 100644 --- a/src/elements/text.ts +++ b/src/elements/text.ts @@ -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; diff --git a/src/html-builder.ts b/src/html-builder.ts new file mode 100644 index 0000000..a4b4c4a --- /dev/null +++ b/src/html-builder.ts @@ -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( + [ + ` + ${this.doc.documentElement.innerHTML} + `, + ], + { 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); + } + } +} diff --git a/src/main.ts b/src/main.ts index 14e3489..12fe30c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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: '', - callback: async (target) => { - const actor: Actor & any = (game).actors.get( - target.attr('data-document-id') - ); - await generatePdf(actor); + options.push( + { + name: i18nLocalize('WFRP4SHEETPRINT.export.pdf'), + condition: isGM(), + icon: '', + callback: async (target) => { + const actor: Actor & 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: '', callback: async (target) => { - const actor: Actor & any = (game).actors.get( - target.attr('data-document-id') - ); - await generateHtml(actor); + const actor: Actor & 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}`); } diff --git a/src/pdf-builder.ts b/src/pdf-builder.ts index 3055b3c..877747f 100644 --- a/src/pdf-builder.ts +++ b/src/pdf-builder.ts @@ -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) {