ngx-print 22.0.0 → 22.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +13 -0
- package/.eslintrc.json +50 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/.github/dependabot.yml +17 -0
- package/.github/workflows/PullRequest.yml +32 -0
- package/.github/workflows/stale.yml +25 -0
- package/.prettierignore +42 -0
- package/.prettierrc.json +14 -0
- package/CHANGELOG.md +126 -0
- package/README.md +11 -0
- package/_config.yml +1 -0
- package/angular.json +132 -0
- package/eslint.config.js +32 -0
- package/ng-package.json +7 -0
- package/package.json +60 -44
- package/projects/demo/public/favicon.ico +0 -0
- package/projects/demo/src/app/app.config.ts +11 -0
- package/projects/demo/src/app/app.routes.ts +3 -0
- package/projects/demo/src/app/demo.component.html +83 -0
- package/projects/demo/src/app/demo.component.scss +54 -0
- package/projects/demo/src/app/demo.component.ts +73 -0
- package/projects/demo/src/index.html +13 -0
- package/projects/demo/src/main.ts +5 -0
- package/projects/demo/src/styles.scss +1 -0
- package/projects/demo/tsconfig.app.json +19 -0
- package/projects/demo/tsconfig.spec.json +15 -0
- package/src/lib/ngx-print.base.ts +381 -0
- package/src/lib/ngx-print.directive.spec.ts +232 -0
- package/src/lib/ngx-print.directive.ts +72 -0
- package/src/lib/ngx-print.module.ts +8 -0
- package/src/lib/ngx-print.service.spec.ts +276 -0
- package/src/lib/ngx-print.service.ts +50 -0
- package/src/lib/print-options.ts +16 -0
- package/src/public_api.ts +8 -0
- package/tsconfig.json +38 -0
- package/tsconfig.lib.json +19 -0
- package/tsconfig.spec.json +10 -0
- package/tslint.json +17 -0
- package/fesm2022/ngx-print.mjs +0 -543
- package/fesm2022/ngx-print.mjs.map +0 -1
- package/types/ngx-print.d.ts +0 -222
- package/types/ngx-print.d.ts.map +0 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
|
2
|
+
import { provideRouter } from '@angular/router';
|
|
3
|
+
|
|
4
|
+
import { routes } from './app.routes';
|
|
5
|
+
|
|
6
|
+
export const appConfig: ApplicationConfig = {
|
|
7
|
+
providers: [
|
|
8
|
+
provideBrowserGlobalErrorListeners(),
|
|
9
|
+
provideRouter(routes)
|
|
10
|
+
]
|
|
11
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<h1>Print Library Demo</h1>
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
Fill out the form and draw on the canvas below, then click print.
|
|
5
|
+
The library will capture the current state of all fields and the canvas.
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<form class="option-form" [formGroup]="optionForm">
|
|
9
|
+
<fieldset>
|
|
10
|
+
<legend>Print options:</legend>
|
|
11
|
+
<div class="form-group">
|
|
12
|
+
@for (method of printMethods; track method) {
|
|
13
|
+
<div>
|
|
14
|
+
<label>
|
|
15
|
+
<input [formControlName]="'printMethod'"
|
|
16
|
+
[value]="method"
|
|
17
|
+
type="radio"
|
|
18
|
+
id="print-method-{{ $index }}"
|
|
19
|
+
name="printMethod"
|
|
20
|
+
/>
|
|
21
|
+
{{ method }}
|
|
22
|
+
</label>
|
|
23
|
+
</div>
|
|
24
|
+
}
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="form-group">
|
|
28
|
+
<label for="fullName">printTitle</label>
|
|
29
|
+
<input id="fullName" type="text" formControlName="printTitle" />
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="form-group">
|
|
33
|
+
<label>Checkbox (Input)</label>
|
|
34
|
+
<div>
|
|
35
|
+
<input type="checkbox" id="agree" formControlName="useExistingCss" />
|
|
36
|
+
<label for="agree" style="display:inline;">useExistingCss</label>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</fieldset>
|
|
40
|
+
</form>
|
|
41
|
+
|
|
42
|
+
<button type="button" (click)="onPrint()">Print Section</button>
|
|
43
|
+
|
|
44
|
+
<!-- This is the section targeted by the ID -->
|
|
45
|
+
<div id="print-demo-section" class="print-container">
|
|
46
|
+
<h3>printable-section</h3>
|
|
47
|
+
|
|
48
|
+
<form [formGroup]="demoform">
|
|
49
|
+
<div class="form-group">
|
|
50
|
+
<label for="fullName">Full Name (Input)</label>
|
|
51
|
+
<input id="fullName" type="text" formControlName="name" placeholder="Enter your name" />
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="form-group">
|
|
55
|
+
<label for="role">Role (Select)</label>
|
|
56
|
+
<select id="role" formControlName="role">
|
|
57
|
+
<option value="developer">Developer</option>
|
|
58
|
+
<option value="designer">Designer</option>
|
|
59
|
+
<option value="manager">Manager</option>
|
|
60
|
+
</select>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="form-group">
|
|
64
|
+
<label for="bio">Biography (Textarea)</label>
|
|
65
|
+
<textarea id="bio" formControlName="bio" rows="4"></textarea>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="form-group">
|
|
69
|
+
<label>Checkbox (Input)</label>
|
|
70
|
+
<div>
|
|
71
|
+
<input type="checkbox" id="agree" formControlName="agree" />
|
|
72
|
+
<label for="agree" style="display:inline;">I agree to the terms</label>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</form>
|
|
76
|
+
|
|
77
|
+
<div class="form-group">
|
|
78
|
+
<label>Signature / Drawing (Canvas)</label>
|
|
79
|
+
<canvas #drawingCanvas width="400" height="150"></canvas>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<p>Swedish: Åå, Japanese: ひらがな, Chinese: 你好, Korean: 안녕하세요</p>
|
|
83
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
padding: 2rem;
|
|
4
|
+
font-family: sans-serif;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.form-group {
|
|
8
|
+
margin-bottom: 1rem;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
label {
|
|
12
|
+
display: block;
|
|
13
|
+
margin-bottom: 0.5rem;
|
|
14
|
+
font-weight: bold;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.option-form {
|
|
18
|
+
max-width: 600px;
|
|
19
|
+
margin-bottom: 2rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
button {
|
|
23
|
+
padding: 0.75rem 1.5rem;
|
|
24
|
+
background: #007bff;
|
|
25
|
+
color: white;
|
|
26
|
+
border: none;
|
|
27
|
+
border-radius: 4px;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
font-size: 1rem;
|
|
30
|
+
|
|
31
|
+
&:hover {
|
|
32
|
+
background: #0056b3;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.print-container {
|
|
37
|
+
border: 2px dashed #ccc;
|
|
38
|
+
padding: 2rem;
|
|
39
|
+
margin: 1rem 0;
|
|
40
|
+
max-width: 600px;
|
|
41
|
+
|
|
42
|
+
input,
|
|
43
|
+
select,
|
|
44
|
+
textarea {
|
|
45
|
+
width: 100%;
|
|
46
|
+
padding: 0.5rem;
|
|
47
|
+
margin-bottom: 0.5rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
canvas {
|
|
51
|
+
border: 1px solid #000;
|
|
52
|
+
background: #f0f0f0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { afterNextRender, Component, ElementRef, inject, viewChild } from '@angular/core';
|
|
2
|
+
import { NgxPrintService } from '../../../../src/lib/ngx-print.service';
|
|
3
|
+
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
|
4
|
+
import { PrintOptions } from '../../../../src/public_api';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'app-root',
|
|
8
|
+
imports: [ReactiveFormsModule],
|
|
9
|
+
templateUrl: './demo.component.html',
|
|
10
|
+
styleUrl: './demo.component.scss',
|
|
11
|
+
})
|
|
12
|
+
export class DemoComponent {
|
|
13
|
+
private _printService = inject(NgxPrintService);
|
|
14
|
+
private _fb = inject(FormBuilder);
|
|
15
|
+
|
|
16
|
+
protected readonly printMethods = ['iframe', 'window', 'tab'] as const;
|
|
17
|
+
|
|
18
|
+
canvasRef = viewChild.required<ElementRef<HTMLCanvasElement>>('drawingCanvas');
|
|
19
|
+
|
|
20
|
+
optionForm = this._fb.group({
|
|
21
|
+
printMethod: this._fb.nonNullable.control<typeof PrintOptions.prototype.printMethod>('iframe'),
|
|
22
|
+
printTitle: this._fb.nonNullable.control<string>('Demo Component Print'),
|
|
23
|
+
useExistingCss: this._fb.nonNullable.control<boolean>(true),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
demoform = this._fb.group({
|
|
27
|
+
name: ['Angular Developer'],
|
|
28
|
+
role: ['developer'],
|
|
29
|
+
bio: ['This text will be preserved in the print view.'],
|
|
30
|
+
agree: [true],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
// Draw on canvas once it is ready (SSR safe)
|
|
35
|
+
afterNextRender(() => {
|
|
36
|
+
this.initCanvas();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onPrint() {
|
|
41
|
+
this._printService.print({
|
|
42
|
+
printSectionId: 'print-demo-section',
|
|
43
|
+
printTitle: this.optionForm.controls.printTitle.value!,
|
|
44
|
+
printMethod: this.optionForm.controls.printMethod.value,
|
|
45
|
+
useExistingCss: this.optionForm.controls.useExistingCss.value,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private initCanvas() {
|
|
50
|
+
const canvas = this.canvasRef().nativeElement;
|
|
51
|
+
const ctx = canvas.getContext('2d');
|
|
52
|
+
|
|
53
|
+
if (ctx) {
|
|
54
|
+
// Draw a background and some shapes to prove canvas printing works
|
|
55
|
+
ctx.fillStyle = '#ffffff';
|
|
56
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
57
|
+
|
|
58
|
+
ctx.beginPath();
|
|
59
|
+
ctx.arc(75, 75, 50, 0, Math.PI * 2, true); // Outer circle
|
|
60
|
+
ctx.moveTo(110, 75);
|
|
61
|
+
ctx.arc(75, 75, 35, 0, Math.PI, false); // Mouth (clockwise)
|
|
62
|
+
ctx.moveTo(65, 65);
|
|
63
|
+
ctx.arc(60, 65, 5, 0, Math.PI * 2, true); // Left eye
|
|
64
|
+
ctx.moveTo(95, 65);
|
|
65
|
+
ctx.arc(90, 65, 5, 0, Math.PI * 2, true); // Right eye
|
|
66
|
+
ctx.stroke();
|
|
67
|
+
|
|
68
|
+
ctx.font = '20px Arial';
|
|
69
|
+
ctx.fillStyle = 'blue';
|
|
70
|
+
ctx.fillText('Canvas Content', 150, 85);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Demo</title>
|
|
6
|
+
<base href="/">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
8
|
+
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<app-root></app-root>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* You can add global styles to this file, and also import other style files */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/app",
|
|
7
|
+
"types": []
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*.ts"],
|
|
10
|
+
"exclude": ["src/**/*.spec.ts"],
|
|
11
|
+
"angularCompilerOptions": {
|
|
12
|
+
"extendedDiagnostics": {
|
|
13
|
+
"checks": {
|
|
14
|
+
"nullishCoalescingNotNullable": "suppress",
|
|
15
|
+
"optionalChainNotNullable": "suppress"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/spec",
|
|
7
|
+
"types": [
|
|
8
|
+
"vitest/globals"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*.d.ts",
|
|
13
|
+
"src/**/*.spec.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { CSP_NONCE, DOCUMENT, inject, Service } from '@angular/core';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
import { PrintOptions } from './print-options';
|
|
4
|
+
|
|
5
|
+
/** For example:
|
|
6
|
+
* {
|
|
7
|
+
* 'h2': { 'border': 'solid 1px' },
|
|
8
|
+
* 'h1': { 'color': 'red', 'border': '1px solid' },
|
|
9
|
+
* */
|
|
10
|
+
export type PrintStyle = Record<string, Record<string, string>>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Either a {@link PrintStyle} object, or a raw CSS string (e.g. `'h1 { color: red; }'`)
|
|
14
|
+
* to be injected into the print document's <style> tag as-is.
|
|
15
|
+
*/
|
|
16
|
+
export type PrintStyleInput = PrintStyle | string;
|
|
17
|
+
|
|
18
|
+
@Service()
|
|
19
|
+
export class PrintBase {
|
|
20
|
+
private document = inject(DOCUMENT);
|
|
21
|
+
private nonce = inject(CSP_NONCE, { optional: true });
|
|
22
|
+
|
|
23
|
+
private _iframeElement: HTMLIFrameElement | undefined;
|
|
24
|
+
private _printStyle: string[] = [];
|
|
25
|
+
private _styleSheetFile = '';
|
|
26
|
+
protected printComplete = new Subject<void>();
|
|
27
|
+
|
|
28
|
+
//#region Getters and Setters
|
|
29
|
+
/**
|
|
30
|
+
* Sets the print styles based on the provided values.
|
|
31
|
+
*
|
|
32
|
+
* @param values - Either a key-value pairs object representing print styles, or a raw CSS string.
|
|
33
|
+
* @protected
|
|
34
|
+
*/
|
|
35
|
+
protected setPrintStyle(values: PrintStyleInput) {
|
|
36
|
+
if (typeof values === 'string') {
|
|
37
|
+
this._printStyle = values ? [values] : [];
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this._printStyle = [];
|
|
42
|
+
for (const [selector, declarations] of Object.entries(values)) {
|
|
43
|
+
// Built declaration-by-declaration (rather than via JSON.stringify + a quote-stripping
|
|
44
|
+
// regex) so that quotes and commas that are part of a CSS value (e.g. quoted font names,
|
|
45
|
+
// comma-separated font-family fallback lists) survive instead of being stripped/mangled.
|
|
46
|
+
const body = Object.entries(declarations)
|
|
47
|
+
.map(([property, value]) => `${property}:${value}`)
|
|
48
|
+
.join(';');
|
|
49
|
+
this._printStyle.push(`${selector}{${body}}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @returns the string that create the stylesheet which will be injected
|
|
55
|
+
* later within <style></style> tag.
|
|
56
|
+
*/
|
|
57
|
+
public returnStyleValues(): string {
|
|
58
|
+
const styleNonce = this.nonce ? ` nonce="${this.nonce}"` : '';
|
|
59
|
+
return `<style${styleNonce}> ${this._printStyle.join(' ')} </style>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @returns string which contains the link tags containing the css which will
|
|
64
|
+
* be injected later within <head></head> tag.
|
|
65
|
+
*
|
|
66
|
+
*/
|
|
67
|
+
private returnStyleSheetLinkTags(): string {
|
|
68
|
+
return this._styleSheetFile;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sets the style sheet file based on the provided CSS list.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} cssList - CSS file or list of CSS files.
|
|
75
|
+
* @protected
|
|
76
|
+
*/
|
|
77
|
+
// prettier-ignore
|
|
78
|
+
protected setStyleSheetFile(cssList: string): void {
|
|
79
|
+
if (!cssList) {
|
|
80
|
+
this._styleSheetFile = '';
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const files = cssList.split(',').map(f => f.trim());
|
|
84
|
+
const nonceAttr = this.nonce ? ` nonce="${this.nonce}"` : '';
|
|
85
|
+
this._styleSheetFile = files
|
|
86
|
+
.map(url => `<link${nonceAttr} rel="stylesheet" type="text/css" href="${url}">`)
|
|
87
|
+
.join('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
|
|
92
|
+
//#region Private methods used by PrintBase
|
|
93
|
+
|
|
94
|
+
private syncFormValues(source: HTMLElement, clone: HTMLElement): void {
|
|
95
|
+
// Select all form elements
|
|
96
|
+
const selector = 'input, select, textarea';
|
|
97
|
+
const sourceEls = source.querySelectorAll(selector);
|
|
98
|
+
const cloneEls = clone.querySelectorAll(selector);
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < sourceEls.length; i++) {
|
|
101
|
+
const srcNode = sourceEls[i] as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
102
|
+
const cloneNode = cloneEls[i] as typeof srcNode;
|
|
103
|
+
|
|
104
|
+
if (srcNode instanceof HTMLInputElement) {
|
|
105
|
+
if (srcNode.type === 'checkbox' || srcNode.type === 'radio') {
|
|
106
|
+
if (srcNode.checked) cloneNode.setAttribute('checked', '');
|
|
107
|
+
else cloneNode.removeAttribute('checked'); // Remove if unchecked
|
|
108
|
+
} else if (srcNode.type === 'file') {
|
|
109
|
+
// File inputs can't be set programmatically for security
|
|
110
|
+
continue;
|
|
111
|
+
} else {
|
|
112
|
+
cloneNode.setAttribute('value', srcNode.value);
|
|
113
|
+
}
|
|
114
|
+
} else if (srcNode instanceof HTMLTextAreaElement) {
|
|
115
|
+
cloneNode.textContent = srcNode.value; // Use textContent, not innerHTML
|
|
116
|
+
} else if (srcNode instanceof HTMLSelectElement) {
|
|
117
|
+
Array.from((cloneNode as HTMLSelectElement).options).forEach((opt, idx) => {
|
|
118
|
+
if (idx === srcNode.selectedIndex) {
|
|
119
|
+
opt.setAttribute('selected', '');
|
|
120
|
+
} else {
|
|
121
|
+
opt.removeAttribute('selected'); // Remove from non-selected
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Converts a canvas element to an image and returns its HTML string.
|
|
130
|
+
*
|
|
131
|
+
* @param {HTMLCanvasElement} canvasElm - The canvas element to convert.
|
|
132
|
+
* @returns {HTMLImageElement | null} - HTML Element of the image.
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
private canvasToImageHtml(canvasElm: HTMLCanvasElement): HTMLImageElement | null {
|
|
136
|
+
try {
|
|
137
|
+
const dataUrl = canvasElm.toDataURL(); // may throw if canvas is tainted
|
|
138
|
+
const img = this.document.createElement('img');
|
|
139
|
+
img.src = dataUrl;
|
|
140
|
+
img.style.maxWidth = '100%';
|
|
141
|
+
|
|
142
|
+
// Preserve displayed size (not just bitmap size)
|
|
143
|
+
const rect = canvasElm.getBoundingClientRect();
|
|
144
|
+
if (rect.width) img.style.width = `${rect.width}px`;
|
|
145
|
+
if (rect.height) img.style.height = `${rect.height}px`;
|
|
146
|
+
|
|
147
|
+
return img;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.warn(`Canvas conversion failed for ${canvasElm}. Likely the canvas is tainted:`, err);
|
|
150
|
+
// If toDataURL() fails (e.g., tainted canvas), keep canvas as-is in print output
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Includes canvas contents in the print section via img tags.
|
|
157
|
+
*
|
|
158
|
+
* @private
|
|
159
|
+
* @param source
|
|
160
|
+
* @param clone
|
|
161
|
+
*/
|
|
162
|
+
private updateCanvasToImage(source: HTMLElement, clone: HTMLElement): void {
|
|
163
|
+
const sourceCanvases = source.querySelectorAll('canvas');
|
|
164
|
+
const cloneCanvases = clone.querySelectorAll('canvas');
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < sourceCanvases.length; i++) {
|
|
167
|
+
const srcCanvas = sourceCanvases[i];
|
|
168
|
+
const cloneCanvas = cloneCanvases[i];
|
|
169
|
+
const img = this.canvasToImageHtml(srcCanvas);
|
|
170
|
+
if (img) {
|
|
171
|
+
cloneCanvas.replaceWith(img);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Retrieves the HTML content of a specified printing section.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} printSectionId - Id of the printing section.
|
|
180
|
+
* @returns {string | null} - HTML content of the printing section, or null if not found.
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
private getHtmlContents(printSectionId: string): string | null {
|
|
184
|
+
const sourceElm = this.document.getElementById(printSectionId);
|
|
185
|
+
if (!sourceElm) return null;
|
|
186
|
+
|
|
187
|
+
const cloneElm = sourceElm.cloneNode(true) as HTMLElement; // cloneNode(true) deep clones subtree
|
|
188
|
+
|
|
189
|
+
this.syncFormValues(sourceElm, cloneElm);
|
|
190
|
+
this.updateCanvasToImage(sourceElm, cloneElm);
|
|
191
|
+
|
|
192
|
+
return cloneElm.outerHTML;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Retrieves the HTML content of elements with the specified tag.
|
|
197
|
+
*
|
|
198
|
+
* @param {keyof HTMLElementTagNameMap} tag - HTML tag name.
|
|
199
|
+
* @returns {string} - Concatenated outerHTML of elements with the specified tag.
|
|
200
|
+
* @private
|
|
201
|
+
*/
|
|
202
|
+
private getElementTag(tag: keyof HTMLElementTagNameMap): string {
|
|
203
|
+
const html: string[] = [];
|
|
204
|
+
const elements = this.document.getElementsByTagName(tag);
|
|
205
|
+
for (const el of Array.from(elements)) {
|
|
206
|
+
html.push((el as Element).outerHTML);
|
|
207
|
+
}
|
|
208
|
+
return html.join('\r\n');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
//#endregion
|
|
212
|
+
|
|
213
|
+
protected notifyPrintComplete() {
|
|
214
|
+
this.printComplete.next();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Prints the specified content using the provided print options.
|
|
219
|
+
*
|
|
220
|
+
* @public
|
|
221
|
+
* @param printOptionInput - Options for printing.
|
|
222
|
+
*/
|
|
223
|
+
protected print(printOptionInput?: Partial<PrintOptions>): void {
|
|
224
|
+
const printOptions = new PrintOptions(printOptionInput);
|
|
225
|
+
if (printOptions.printMethod === 'iframe') {
|
|
226
|
+
this.printWithIframe(printOptions);
|
|
227
|
+
} else {
|
|
228
|
+
this.printWithWindow(printOptions);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
protected printWithWindow(printOptions: PrintOptions) {
|
|
233
|
+
// If the openNewTab option is set to true, then set the popOut option to an empty string
|
|
234
|
+
// This will cause the print dialog to open in a new tab.
|
|
235
|
+
const popOut = printOptions.printMethod === 'tab' ? '' : 'top=0,left=0,height=auto,width=auto';
|
|
236
|
+
|
|
237
|
+
const popupWin = window.open('', '_blank', popOut);
|
|
238
|
+
|
|
239
|
+
if (!popupWin) {
|
|
240
|
+
// the popup window could not be opened.
|
|
241
|
+
console.error('Could not open print window.');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
popupWin.document.open();
|
|
246
|
+
// Create the HTML structure
|
|
247
|
+
this.buildPrintDocument(popupWin.document, printOptions);
|
|
248
|
+
|
|
249
|
+
popupWin.document.close();
|
|
250
|
+
|
|
251
|
+
// Listen for the window closing
|
|
252
|
+
const checkClosedInterval = setInterval(() => {
|
|
253
|
+
if (popupWin.closed) {
|
|
254
|
+
clearInterval(checkClosedInterval);
|
|
255
|
+
this.notifyPrintComplete();
|
|
256
|
+
}
|
|
257
|
+
}, 500);
|
|
258
|
+
|
|
259
|
+
popupWin.addEventListener('load', () => {
|
|
260
|
+
if (!printOptions.previewOnly) {
|
|
261
|
+
setTimeout(() => {
|
|
262
|
+
popupWin.print();
|
|
263
|
+
if (printOptions.closeWindow) popupWin.close();
|
|
264
|
+
}, printOptions.printDelay || 0);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private printWithIframe(printOptions: PrintOptions): void {
|
|
270
|
+
if (this._iframeElement) {
|
|
271
|
+
this._iframeElement.remove();
|
|
272
|
+
}
|
|
273
|
+
this._iframeElement = this.document.createElement('iframe');
|
|
274
|
+
const iframe = this._iframeElement;
|
|
275
|
+
iframe.id = 'print-iframe-' + new Date().getTime();
|
|
276
|
+
iframe.style.position = 'absolute';
|
|
277
|
+
iframe.style.left = '-9999px';
|
|
278
|
+
iframe.style.top = '-9999px';
|
|
279
|
+
iframe.style.width = '0px';
|
|
280
|
+
iframe.style.height = '0px';
|
|
281
|
+
iframe.ariaHidden = 'true';
|
|
282
|
+
|
|
283
|
+
this.document.body.appendChild(iframe);
|
|
284
|
+
|
|
285
|
+
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
286
|
+
if (!iframeDoc) {
|
|
287
|
+
console.error('Could not access iframe document.');
|
|
288
|
+
this.document.body.removeChild(iframe);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
iframeDoc.open();
|
|
293
|
+
const success = this.buildPrintDocument(iframeDoc, printOptions);
|
|
294
|
+
if (!success) {
|
|
295
|
+
iframeDoc.close();
|
|
296
|
+
this.document.body.removeChild(iframe);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
iframeDoc.close();
|
|
300
|
+
|
|
301
|
+
iframe.onload = () => {
|
|
302
|
+
const printWindow = iframe.contentWindow;
|
|
303
|
+
if (!printWindow) {
|
|
304
|
+
console.error('Could not access iframe window.');
|
|
305
|
+
this.document.body.removeChild(iframe);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
setTimeout(() => {
|
|
310
|
+
if (printOptions.previewOnly) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
printWindow.focus();
|
|
314
|
+
printWindow.print();
|
|
315
|
+
|
|
316
|
+
const mediaQueryList = printWindow.matchMedia('print');
|
|
317
|
+
const listener = (mql: MediaQueryListEvent) => {
|
|
318
|
+
if (!mql.matches) {
|
|
319
|
+
this.notifyPrintComplete();
|
|
320
|
+
mediaQueryList.removeEventListener('change', listener);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
mediaQueryList.addEventListener('change', listener);
|
|
325
|
+
}, printOptions.printDelay || 0);
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private prepareDocumentComponents(printOptions: PrintOptions) {
|
|
330
|
+
let styles = '';
|
|
331
|
+
let links = '';
|
|
332
|
+
const baseTag = this.getElementTag('base');
|
|
333
|
+
|
|
334
|
+
if (printOptions.useExistingCss) {
|
|
335
|
+
styles = this.getElementTag('style');
|
|
336
|
+
links = this.getElementTag('link');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const printContents = this.getHtmlContents(printOptions.printSectionId);
|
|
340
|
+
|
|
341
|
+
return { styles, links, baseTag, printContents };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private buildPrintDocument(doc: Document, printOptions: PrintOptions): boolean {
|
|
345
|
+
const components = this.prepareDocumentComponents(printOptions);
|
|
346
|
+
|
|
347
|
+
if (!components.printContents) {
|
|
348
|
+
console.error(`Print section with id "${printOptions.printSectionId}" not found.`);
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const html = doc.createElement('html');
|
|
353
|
+
const head = doc.createElement('head');
|
|
354
|
+
const body = doc.createElement('body');
|
|
355
|
+
|
|
356
|
+
// Set title
|
|
357
|
+
const title = doc.createElement('title');
|
|
358
|
+
title.textContent = printOptions.printTitle || '';
|
|
359
|
+
head.appendChild(title);
|
|
360
|
+
|
|
361
|
+
// Add all head content
|
|
362
|
+
if (components.baseTag) {
|
|
363
|
+
head.innerHTML += components.baseTag;
|
|
364
|
+
}
|
|
365
|
+
head.innerHTML += this.returnStyleValues();
|
|
366
|
+
head.innerHTML += this.returnStyleSheetLinkTags();
|
|
367
|
+
head.innerHTML += components.styles;
|
|
368
|
+
head.innerHTML += components.links;
|
|
369
|
+
|
|
370
|
+
// Set body class and content
|
|
371
|
+
if (printOptions.bodyClass) body.className = printOptions.bodyClass;
|
|
372
|
+
body.innerHTML += components.printContents;
|
|
373
|
+
|
|
374
|
+
// Assemble document
|
|
375
|
+
html.appendChild(head);
|
|
376
|
+
html.appendChild(body);
|
|
377
|
+
doc.appendChild(html);
|
|
378
|
+
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
}
|