ngx-print
Version:
Plug n' Play Angular directive to print your stuff
494 lines (485 loc) • 22.3 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, DOCUMENT, CSP_NONCE, Service, input, output, Directive, NgModule } from '@angular/core';
import { Subject, take } from 'rxjs';
class PrintOptions {
printSectionId = '';
printTitle = '';
useExistingCss = false;
bodyClass = '';
printMethod = 'window';
previewOnly = false;
closeWindow = true;
printDelay = 0;
constructor(options) {
if (options) {
Object.assign(this, options);
}
}
}
class PrintBase {
document = inject(DOCUMENT);
nonce = inject(CSP_NONCE, { optional: true });
_iframeElement;
_printStyle = [];
_styleSheetFile = '';
printComplete = new Subject();
//#region Getters and Setters
/**
* Sets the print styles based on the provided values.
*
* @param values - Either a key-value pairs object representing print styles, or a raw CSS string.
* @protected
*/
setPrintStyle(values) {
if (typeof values === 'string') {
this._printStyle = values ? [values] : [];
return;
}
this._printStyle = [];
for (const [selector, declarations] of Object.entries(values)) {
// Built declaration-by-declaration (rather than via JSON.stringify + a quote-stripping
// regex) so that quotes and commas that are part of a CSS value (e.g. quoted font names,
// comma-separated font-family fallback lists) survive instead of being stripped/mangled.
const body = Object.entries(declarations)
.map(([property, value]) => `${property}:${value}`)
.join(';');
this._printStyle.push(`${selector}{${body}}`);
}
}
/**
* @returns the string that create the stylesheet which will be injected
* later within <style></style> tag.
*/
returnStyleValues() {
const styleNonce = this.nonce ? ` nonce="${this.nonce}"` : '';
return `<style${styleNonce}> ${this._printStyle.join(' ')} </style>`;
}
/**
* @returns string which contains the link tags containing the css which will
* be injected later within <head></head> tag.
*
*/
returnStyleSheetLinkTags() {
return this._styleSheetFile;
}
/**
* Sets the style sheet file based on the provided CSS list.
*
* @param {string} cssList - CSS file or list of CSS files.
* @protected
*/
// prettier-ignore
setStyleSheetFile(cssList) {
if (!cssList) {
this._styleSheetFile = '';
return;
}
const files = cssList.split(',').map(f => f.trim());
const nonceAttr = this.nonce ? ` nonce="${this.nonce}"` : '';
this._styleSheetFile = files
.map(url => `<link${nonceAttr} rel="stylesheet" type="text/css" href="${url}">`)
.join('');
}
//#endregion
//#region Private methods used by PrintBase
syncFormValues(source, clone) {
// Select all form elements
const selector = 'input, select, textarea';
const sourceEls = source.querySelectorAll(selector);
const cloneEls = clone.querySelectorAll(selector);
for (let i = 0; i < sourceEls.length; i++) {
const srcNode = sourceEls[i];
const cloneNode = cloneEls[i];
if (srcNode instanceof HTMLInputElement) {
if (srcNode.type === 'checkbox' || srcNode.type === 'radio') {
if (srcNode.checked)
cloneNode.setAttribute('checked', '');
else
cloneNode.removeAttribute('checked'); // Remove if unchecked
}
else if (srcNode.type === 'file') {
// File inputs can't be set programmatically for security
continue;
}
else {
cloneNode.setAttribute('value', srcNode.value);
}
}
else if (srcNode instanceof HTMLTextAreaElement) {
cloneNode.textContent = srcNode.value; // Use textContent, not innerHTML
}
else if (srcNode instanceof HTMLSelectElement) {
Array.from(cloneNode.options).forEach((opt, idx) => {
if (idx === srcNode.selectedIndex) {
opt.setAttribute('selected', '');
}
else {
opt.removeAttribute('selected'); // Remove from non-selected
}
});
}
}
}
/**
* Converts a canvas element to an image and returns its HTML string.
*
* @param {HTMLCanvasElement} canvasElm - The canvas element to convert.
* @returns {HTMLImageElement | null} - HTML Element of the image.
* @private
*/
canvasToImageHtml(canvasElm) {
try {
const dataUrl = canvasElm.toDataURL(); // may throw if canvas is tainted
const img = this.document.createElement('img');
img.src = dataUrl;
img.style.maxWidth = '100%';
// Preserve displayed size (not just bitmap size)
const rect = canvasElm.getBoundingClientRect();
if (rect.width)
img.style.width = `${rect.width}px`;
if (rect.height)
img.style.height = `${rect.height}px`;
return img;
}
catch (err) {
console.warn(`Canvas conversion failed for ${canvasElm}. Likely the canvas is tainted:`, err);
// If toDataURL() fails (e.g., tainted canvas), keep canvas as-is in print output
return null;
}
}
/**
* Includes canvas contents in the print section via img tags.
*
* @private
* @param source
* @param clone
*/
updateCanvasToImage(source, clone) {
const sourceCanvases = source.querySelectorAll('canvas');
const cloneCanvases = clone.querySelectorAll('canvas');
for (let i = 0; i < sourceCanvases.length; i++) {
const srcCanvas = sourceCanvases[i];
const cloneCanvas = cloneCanvases[i];
const img = this.canvasToImageHtml(srcCanvas);
if (img) {
cloneCanvas.replaceWith(img);
}
}
}
/**
* Retrieves the HTML content of a specified printing section.
*
* @param {string} printSectionId - Id of the printing section.
* @returns {string | null} - HTML content of the printing section, or null if not found.
* @private
*/
getHtmlContents(printSectionId) {
const sourceElm = this.document.getElementById(printSectionId);
if (!sourceElm)
return null;
const cloneElm = sourceElm.cloneNode(true); // cloneNode(true) deep clones subtree
this.syncFormValues(sourceElm, cloneElm);
this.updateCanvasToImage(sourceElm, cloneElm);
return cloneElm.outerHTML;
}
/**
* Retrieves the HTML content of elements with the specified tag.
*
* @param {keyof HTMLElementTagNameMap} tag - HTML tag name.
* @returns {string} - Concatenated outerHTML of elements with the specified tag.
* @private
*/
getElementTag(tag) {
const html = [];
const elements = this.document.getElementsByTagName(tag);
for (const el of Array.from(elements)) {
html.push(el.outerHTML);
}
return html.join('\r\n');
}
//#endregion
notifyPrintComplete() {
this.printComplete.next();
}
/**
* Prints the specified content using the provided print options.
*
* @public
* @param printOptionInput - Options for printing.
*/
print(printOptionInput) {
const printOptions = new PrintOptions(printOptionInput);
if (printOptions.printMethod === 'iframe') {
this.printWithIframe(printOptions);
}
else {
this.printWithWindow(printOptions);
}
}
printWithWindow(printOptions) {
// If the openNewTab option is set to true, then set the popOut option to an empty string
// This will cause the print dialog to open in a new tab.
const popOut = printOptions.printMethod === 'tab' ? '' : 'top=0,left=0,height=auto,width=auto';
const popupWin = window.open('', '_blank', popOut);
if (!popupWin) {
// the popup window could not be opened.
console.error('Could not open print window.');
return;
}
popupWin.document.open();
// Create the HTML structure
this.buildPrintDocument(popupWin.document, printOptions);
popupWin.document.close();
// Listen for the window closing
const checkClosedInterval = setInterval(() => {
if (popupWin.closed) {
clearInterval(checkClosedInterval);
this.notifyPrintComplete();
}
}, 500);
popupWin.addEventListener('load', () => {
if (!printOptions.previewOnly) {
setTimeout(() => {
popupWin.print();
if (printOptions.closeWindow)
popupWin.close();
}, printOptions.printDelay || 0);
}
});
}
printWithIframe(printOptions) {
if (this._iframeElement) {
this._iframeElement.remove();
}
this._iframeElement = this.document.createElement('iframe');
const iframe = this._iframeElement;
iframe.id = 'print-iframe-' + new Date().getTime();
iframe.style.position = 'absolute';
iframe.style.left = '-9999px';
iframe.style.top = '-9999px';
iframe.style.width = '0px';
iframe.style.height = '0px';
iframe.ariaHidden = 'true';
this.document.body.appendChild(iframe);
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
if (!iframeDoc) {
console.error('Could not access iframe document.');
this.document.body.removeChild(iframe);
return;
}
iframeDoc.open();
const success = this.buildPrintDocument(iframeDoc, printOptions);
if (!success) {
iframeDoc.close();
this.document.body.removeChild(iframe);
return;
}
iframeDoc.close();
iframe.onload = () => {
const printWindow = iframe.contentWindow;
if (!printWindow) {
console.error('Could not access iframe window.');
this.document.body.removeChild(iframe);
return;
}
setTimeout(() => {
if (printOptions.previewOnly) {
return;
}
printWindow.focus();
printWindow.print();
const mediaQueryList = printWindow.matchMedia('print');
const listener = (mql) => {
if (!mql.matches) {
this.notifyPrintComplete();
mediaQueryList.removeEventListener('change', listener);
}
};
mediaQueryList.addEventListener('change', listener);
}, printOptions.printDelay || 0);
};
}
prepareDocumentComponents(printOptions) {
let styles = '';
let links = '';
const baseTag = this.getElementTag('base');
if (printOptions.useExistingCss) {
styles = this.getElementTag('style');
links = this.getElementTag('link');
}
const printContents = this.getHtmlContents(printOptions.printSectionId);
return { styles, links, baseTag, printContents };
}
buildPrintDocument(doc, printOptions) {
const components = this.prepareDocumentComponents(printOptions);
if (!components.printContents) {
console.error(`Print section with id "${printOptions.printSectionId}" not found.`);
return false;
}
const html = doc.createElement('html');
const head = doc.createElement('head');
const body = doc.createElement('body');
// Set title
const title = doc.createElement('title');
title.textContent = printOptions.printTitle || '';
head.appendChild(title);
// Add all head content
if (components.baseTag) {
head.innerHTML += components.baseTag;
}
head.innerHTML += this.returnStyleValues();
head.innerHTML += this.returnStyleSheetLinkTags();
head.innerHTML += components.styles;
head.innerHTML += components.links;
// Set body class and content
if (printOptions.bodyClass)
body.className = printOptions.bodyClass;
body.innerHTML += components.printContents;
// Assemble document
html.appendChild(head);
html.appendChild(body);
doc.appendChild(html);
return true;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: PrintBase, deps: [], target: i0.ɵɵFactoryTarget.Service });
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.2", ngImport: i0, type: PrintBase });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: PrintBase, decorators: [{
type: Service
}] });
class NgxPrintDirective extends PrintBase {
/**
* Prevents the print dialog from opening on the window
*/
previewOnly = input(false, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "previewOnly" }] : /* istanbul ignore next */ []));
printSectionId = input('', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "printSectionId" }] : /* istanbul ignore next */ []));
printTitle = input('', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "printTitle" }] : /* istanbul ignore next */ []));
useExistingCss = input(false, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "useExistingCss" }] : /* istanbul ignore next */ []));
/**
* A delay in milliseconds to force the print dialog to wait before opened. Default: 0
*/
printDelay = input(0, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "printDelay" }] : /* istanbul ignore next */ []));
/**
* Whether to close the window after print() returns.
*/
closeWindow = input(true, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "closeWindow" }] : /* istanbul ignore next */ []));
/**
* Class attribute to apply to the body element.
*/
bodyClass = input('', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "bodyClass" }] : /* istanbul ignore next */ []));
/**
* Which PrintMethod (iframe/window/tab) to use.
*/
printMethod = input('window', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "printMethod" }] : /* istanbul ignore next */ []));
printStyle = input({}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "printStyle" }] : /* istanbul ignore next */ []));
styleSheetFile = input('', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "styleSheetFile" }] : /* istanbul ignore next */ []));
printCompleted = output();
print() {
// Inputs carry side effects on PrintBase's internal style state, so they're applied
// synchronously here rather than via effect() (effects shouldn't propagate state).
super.setPrintStyle(this.printStyle());
super.setStyleSheetFile(this.styleSheetFile());
super.print({
printSectionId: this.printSectionId(),
printTitle: this.printTitle(),
useExistingCss: this.useExistingCss(),
bodyClass: this.bodyClass(),
printMethod: this.printMethod(),
previewOnly: this.previewOnly(),
closeWindow: this.closeWindow(),
printDelay: this.printDelay(),
});
this.printComplete.pipe(take(1)).subscribe(() => {
this.printCompleted.emit();
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: NgxPrintDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: NgxPrintDirective, isStandalone: true, selector: "[ngxPrint]", inputs: { previewOnly: { classPropertyName: "previewOnly", publicName: "previewOnly", isSignal: true, isRequired: false, transformFunction: null }, printSectionId: { classPropertyName: "printSectionId", publicName: "printSectionId", isSignal: true, isRequired: false, transformFunction: null }, printTitle: { classPropertyName: "printTitle", publicName: "printTitle", isSignal: true, isRequired: false, transformFunction: null }, useExistingCss: { classPropertyName: "useExistingCss", publicName: "useExistingCss", isSignal: true, isRequired: false, transformFunction: null }, printDelay: { classPropertyName: "printDelay", publicName: "printDelay", isSignal: true, isRequired: false, transformFunction: null }, closeWindow: { classPropertyName: "closeWindow", publicName: "closeWindow", isSignal: true, isRequired: false, transformFunction: null }, bodyClass: { classPropertyName: "bodyClass", publicName: "bodyClass", isSignal: true, isRequired: false, transformFunction: null }, printMethod: { classPropertyName: "printMethod", publicName: "printMethod", isSignal: true, isRequired: false, transformFunction: null }, printStyle: { classPropertyName: "printStyle", publicName: "printStyle", isSignal: true, isRequired: false, transformFunction: null }, styleSheetFile: { classPropertyName: "styleSheetFile", publicName: "styleSheetFile", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { printCompleted: "printCompleted" }, host: { listeners: { "click": "print()" } }, usesInheritance: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: NgxPrintDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngxPrint]',
standalone: true,
host: {
'(click)': 'print()',
},
}]
}], propDecorators: { previewOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewOnly", required: false }] }], printSectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "printSectionId", required: false }] }], printTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "printTitle", required: false }] }], useExistingCss: [{ type: i0.Input, args: [{ isSignal: true, alias: "useExistingCss", required: false }] }], printDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "printDelay", required: false }] }], closeWindow: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeWindow", required: false }] }], bodyClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "bodyClass", required: false }] }], printMethod: [{ type: i0.Input, args: [{ isSignal: true, alias: "printMethod", required: false }] }], printStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "printStyle", required: false }] }], styleSheetFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "styleSheetFile", required: false }] }], printCompleted: [{ type: i0.Output, args: ["printCompleted"] }] } });
class NgxPrintModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: NgxPrintModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.2", ngImport: i0, type: NgxPrintModule, imports: [NgxPrintDirective], exports: [NgxPrintDirective] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: NgxPrintModule });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: NgxPrintModule, decorators: [{
type: NgModule,
args: [{
imports: [NgxPrintDirective],
exports: [NgxPrintDirective],
}]
}] });
/**
* Service for handling printing functionality in Angular applications.
* Extends the base printing class (PrintBase).
*
* @export
* @class NgxPrintService
* @extends {PrintBase}
*/
class NgxPrintService extends PrintBase {
printComplete$ = this.printComplete.asObservable();
/**
* Initiates the printing process using the provided print options.
*
* @param {PrintOptions} printOptions - Options for configuring the printing process.
* @memberof NgxPrintService
* @returns {void}
*/
print(printOptions) {
// Call the print method in the parent class
super.print(printOptions);
}
/**
* Sets the print style for the printing process.
*
* @param values - Either a dictionary representing the print styles, or a raw CSS string.
* @memberof NgxPrintService
* @setter
*/
set printStyle(values) {
super.setPrintStyle(values);
}
/**
* Sets the stylesheet file for the printing process.
*
* @param {string} cssList - A string representing the path to the stylesheet file.
* @memberof NgxPrintService
* @setter
*/
set styleSheetFile(cssList) {
super.setStyleSheetFile(cssList);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: NgxPrintService, deps: [], target: i0.ɵɵFactoryTarget.Service });
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.2", ngImport: i0, type: NgxPrintService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: NgxPrintService, decorators: [{
type: Service
}] });
/*
* Public API Surface of ngx-print
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgxPrintDirective, NgxPrintModule, NgxPrintService, PrintOptions };
//# sourceMappingURL=ngx-print.mjs.map