devexpress-richedit
Version:
DevExpress Rich Text Editor is an advanced word-processing tool designed for working with rich text documents.
306 lines (305 loc) • 15.3 kB
JavaScript
import { HtmlBuilder } from '../../formats/html/export/html-builder';
import { RendererClassNames } from '../../canvas/renderer-class-names';
import { UpdateFieldsOptions } from '../../model/fields/tree-creator';
import { ControlOptions } from '../../model/options/control';
import { PrintMode } from '../../model/options/printing';
import { PaperKind } from '../../model/section/paper-kind';
import { SubDocumentIntervals } from '../../model/sub-document';
import { ViewType } from '../../view-settings/views-settings';
import { Browser } from '@devexpress/utils/lib/browser';
import { UnitConverter } from '@devexpress/utils/lib/class/unit-converter';
import { Size } from '@devexpress/utils/lib/geometry/size';
import { ListUtils } from '@devexpress/utils/lib/utils/list';
import { NumberMapUtils } from '@devexpress/utils/lib/utils/map/number';
import { pdfExport } from '../../formats/pdf/api/pdf';
import { RichEditClientCommand } from '../client-command';
import { CommandBase } from '../command-base';
import { SimpleCommandState } from '../command-states';
import { UpdateFieldCommandBase } from '../fields/update-field-command-base';
import { InformationCreator } from '../../utils/information-creator';
export class PrintDocumentOnClient extends CommandBase {
_nonce;
isEnabled() {
return super.isEnabled() && ControlOptions.isEnabled(this.control.modelManager.richOptions.control.printing) &&
this.control.modelManager.model.isLoaded();
}
isEnabledInReadOnlyMode() {
return true;
}
constructor(control, _nonce) {
super(control);
this._nonce = _nonce;
}
getState() {
return new SimpleCommandState(this.isEnabled());
}
getMode(param) {
return param !== null && param !== undefined ? param : this.control.modelManager.richOptions.printing.mode;
}
getclosePrintDialogWithHtmlPreview(param) {
return param !== null && param !== undefined ? param : this.control.modelManager.richOptions.printing.closePrintDialogWithHtmlPreview;
}
executeCore(_state, options) {
if (this.control.commandManager.isPrintingProcessing)
return false;
this.control.commandManager.isPrintingProcessing = true;
let mode;
let closePrintDialogWithHtmlPreview;
if (typeof options.param === 'object' && options.param != null) {
mode = this.getMode(options.param.mode);
closePrintDialogWithHtmlPreview = this.getclosePrintDialogWithHtmlPreview(options.param.closePrintDialogWithHtmlPreview);
}
else {
mode = this.getMode(options.param);
closePrintDialogWithHtmlPreview = this.control.modelManager.richOptions.printing.closePrintDialogWithHtmlPreview;
}
const htmlPrinting = mode == PrintMode.ClientHtml;
let printWindow;
if (htmlPrinting)
printWindow = window.open('', 'print', `height=${window.innerHeight},width=${window.innerWidth},tabbar=no`);
let needSwitchViewType = false;
if (this.control.innerClientProperties.viewsSettings.viewType == ViewType.Simple) {
needSwitchViewType = true;
this.control.commandManager.getCommand(RichEditClientCommand.SwitchToPrintLayoutView).execute(this.control.commandManager.isPublicApiCall);
}
let needToggleHiddenSymbols = false;
if (this.control.innerClientProperties.showHiddenSymbols) {
needToggleHiddenSymbols = true;
this.control.commandManager.getCommand(RichEditClientCommand.ToggleShowWhitespace).execute(this.control.commandManager.isPublicApiCall);
}
InformationCreator.saveWithInformation(this.control, () => {
while (!this.control.layout.isFullyFormatted)
this.control.layoutFormatterManager.forceFormatPage(this.control.layout.validPageCount + 1);
const subDocsInfo = NumberMapUtils.toListBy(this.modelManipulator.model.subDocuments, (sd) => new SubDocumentIntervals(sd, [sd.interval]));
if (this.control.modelManager.richOptions.fields.updateFieldsBeforePrint) {
UpdateFieldCommandBase.updateFields(this, subDocsInfo, () => {
this.printCore(htmlPrinting, printWindow, closePrintDialogWithHtmlPreview, needSwitchViewType, needToggleHiddenSymbols);
}, new UpdateFieldsOptions(false, false));
}
else
this.printCore(htmlPrinting, printWindow, closePrintDialogWithHtmlPreview, needSwitchViewType, needToggleHiddenSymbols);
});
return true;
}
printCore(htmlPrinting, printWindow, closePrintDialogWithHtmlPreview, needSwitchViewType, needToggleHiddenSymbols) {
if (htmlPrinting) {
this.generatePrintDocument(printWindow.document);
printWindow.focus();
const interval = setInterval(() => {
if (printWindow.document.readyState == 'complete') {
printWindow.print();
if (closePrintDialogWithHtmlPreview && !Browser.AndroidMobilePlatform)
printWindow.close();
clearInterval(interval);
}
}, 100);
}
else {
pdfExport(this.control, (blob, _stream) => {
if (window.navigator.msSaveOrOpenBlob && !Browser.Edge)
window.navigator.msSaveOrOpenBlob(blob, this.control.documentInfo.getFileNameForDownload() + ".pdf");
else
this.control.pdfHelperFrame.showPrintDialog(URL.createObjectURL(blob));
}, () => { });
}
if (needSwitchViewType)
this.control.commandManager.getCommand(RichEditClientCommand.SwitchToSimpleView).execute(this.control.commandManager.isPublicApiCall);
if (needToggleHiddenSymbols)
this.control.commandManager.getCommand(RichEditClientCommand.ToggleShowWhitespace).execute(this.control.commandManager.isPublicApiCall);
this.control.commandManager.isPrintingProcessing = false;
}
generatePrintDocument(document) {
const dynamicStyleAttrName = 'data-rich-dynamic-style';
const height = this.control.layout.pages[0].height;
const width = this.control.layout.pages[0].width;
let fontLink = "";
let divsToLoadFonts = "";
const googleFonts = this.getGoogleFonts();
if (googleFonts.length > 0) {
fontLink = this.createGoogleFontStyleLink(googleFonts);
divsToLoadFonts = googleFonts.reduce((prev, curr) => {
const fontStyles = [`font-family:${curr}`, 'font-weight:bold', 'font-style:italic'];
const result = [];
for (let i = 1; i <= fontStyles.length; i++)
result.push(`<div ${dynamicStyleAttrName}="font-size:1pt;position:absolute;top:-1000px;${fontStyles.slice(0, i).join(';')}">${curr}</div>`);
return prev ? [prev, ...result].join('\n') : result.join('\n');
}, null);
}
document.documentElement.innerHTML =
`<!DOCTYPE html>
<html moznomarginboxes mozdisallowselectionprint>
<head>
${fontLink}
</head>
<body>
${divsToLoadFonts}
</body>
</html>`;
const styleElement = document.createElement("STYLE");
if (this._nonce)
styleElement.setAttribute("nonce", this._nonce);
styleElement.innerHTML = ` { margin: 0; size:${width}px ${width > height ? height + 4 : height}px; }`;
document.head.appendChild(styleElement);
this.generatePrintContent().forEach((child) => {
document.body.appendChild(child);
});
document.documentElement.querySelectorAll(`[${dynamicStyleAttrName}]`).forEach((element) => {
element.style.cssText = element.getAttribute(dynamicStyleAttrName);
});
const styleRules = this.getPrintDocumentStyleRules(width, height);
for (let selector in styleRules) {
const props = styleRules[selector];
document.querySelectorAll(selector).forEach(el => {
for (let i = 0; i < props.length; i++) {
const prop = props[i];
const priority = prop.length == 3 && prop[2] ? 'important' : '';
el.style.setProperty(prop[0], prop[1], priority);
}
});
}
}
getPrintDocumentStyleRules(width, height) {
const result = {};
result["html, body"] = [
['margin', '0', true],
['width', `${width}px`],
['height', `${height}px`]
];
result[".dxrePageArea, .dxreColumn, .dxreRow, .dxreBox, .dxreBoxBg, .dxreParFrame, .dxreBoxSpace, .dxreAncPic, .dxreTable, .dxreTableCellBg, .dxreTableBrd, .dxreTextBoxBg"] = [
['position', 'absolute']
];
result[".dxreTableRowCursor, .dxreTableColumnCursor, br"] = [
['display', 'none']
];
this.fillZIndexStyles(result);
return result;
}
getGoogleFonts() {
return this.control.modelManager.richOptions.fonts.fonts.reduce((res, f) => {
if (f.useGoogleFonts)
res.push(f.fontFamily);
return res;
}, []);
}
createGoogleFontStyleLink(fontFamilies) {
const url = new URL('https://fonts.googleapis.com/css');
url.searchParams.append('family', fontFamilies.join('|'));
url.searchParams.append('display', 'auto');
return `<link href="${url.toString()}" rel="stylesheet" />`;
}
generatePrintContent() {
const layout = this.control.layout;
const htmlBuilder = new HtmlBuilder();
for (let i = 0, layoutPage; layoutPage = layout.pages[i]; i++) {
const pageSize = PrintPageSizesCalculator.getPrintPageSize(layoutPage);
const pageWidth = pageSize.width;
const pageHeight = pageSize.height;
const page = this.control.viewManager.printLayoutRenderer.getPageRender(i, layoutPage);
const main = page.getElementsByClassName(RendererClassNames.MAIN_PAGE_AREA_CONTAINER)[0].cloneNode(true);
const fo = page.getElementsByClassName(RendererClassNames.FLOATING_OBJECTS_CONTAINER)[0].cloneNode(true);
const other = page.getElementsByClassName(RendererClassNames.OTHER_PAGE_AREA_CONTAINER)[0].cloneNode(true);
const shapeBg = page.getElementsByClassName(RendererClassNames.SHAPE_BG_CONTAINER)[0].cloneNode(true);
if (Browser.IE) {
htmlBuilder
.startChild('div')
.configure((el) => {
el.style.cssText = `position: relative; margin:0px; display:block; width:${pageWidth}px; height:${pageHeight}px;`;
})
.addElement(main)
.addElement(fo)
.addElement(other)
.addElement(shapeBg)
.endChild('div')
.startChild('br')
.endChild('br');
}
else {
htmlBuilder
.startChild('svg', 'http://www.w3.org/2000/svg')
.configure((el) => {
el.setAttribute("width", pageWidth.toString());
el.setAttribute("height", pageHeight.toString());
})
.startChild('foreignObject', 'http://www.w3.org/2000/svg')
.configure((el) => {
el.setAttribute("x", "0");
el.setAttribute("y", "0");
el.setAttribute("width", pageWidth.toString());
el.setAttribute("height", pageHeight.toString());
})
.startChild('div')
.configure((el) => {
el.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
})
.addElement(main)
.addElement(fo)
.addElement(other)
.addElement(shapeBg)
.endChild('div')
.endChild('foreignObject')
.endChild('svg');
}
}
return htmlBuilder.childElements;
}
fillZIndexStyles(styleRules) {
let ind = 0;
const names = ['TextBoxBg', 'TblRowBg', 'TblCellBg', 'ParBg', 'BoxBg', 'BoxSpace', 'Box', 'TableBorder', 'AnchoredPicture', 'TextBox'];
for (let level = 0; level <= 8; level++) {
for (let i = 0, name; name = names[i]; i++) {
styleRules[`.dxre${name}ZL${level}`] = [['z-index', `${ind}`]];
ind++;
}
}
}
}
class PrintPageSize {
paperKind;
vertical;
horizontal;
accuracy;
converterToPixels;
constructor(paperKind, vertical, horizontal = vertical, accuracy = 5, converterToPixels = UnitConverter.millimetersToPixel) {
this.paperKind = paperKind;
this.vertical = vertical;
this.horizontal = horizontal;
this.accuracy = accuracy;
this.converterToPixels = converterToPixels;
vertical.applyConverter(converterToPixels);
horizontal.applyConverter(converterToPixels);
}
getDifference(landscape, currSize) {
const size = landscape ? this.horizontal : this.vertical;
return new Size(Math.abs(size.width - currSize.width), Math.abs(size.height - currSize.height));
}
isAccuracyAchieved(diff) {
return diff.width < this.accuracy && diff.height < this.accuracy;
}
}
class Diff {
pageSizeInfo;
diff;
maxDiff;
constructor(pageSizeInfo, diff) {
this.pageSizeInfo = pageSizeInfo;
this.diff = diff;
this.maxDiff = Math.max(diff.width, diff.height);
}
}
class PrintPageSizesCalculator {
static sizesList = [
new PrintPageSize(PaperKind.A4, new Size(210, 296), new Size(297, 209)),
new PrintPageSize(PaperKind.Letter, new Size(UnitConverter.millimetersToPixel(216), UnitConverter.millimetersToPixel(278) - 1), new Size(UnitConverter.millimetersToPixel(280), (215)), 7, v => v),
];
static getPrintPageSize(layoutSize) {
const landscape = layoutSize.width > layoutSize.height;
const list = ListUtils.reducedMap(PrintPageSizesCalculator.sizesList, (info) => {
const diff = info.getDifference(landscape, layoutSize);
return info.isAccuracyAchieved(diff) ? new Diff(info, diff) : null;
});
if (!list.length)
return new Size(layoutSize.width, layoutSize.height);
const finalPageInfo = ListUtils.minByCmp(list, (a, b) => a.maxDiff - b.maxDiff).pageSizeInfo;
return landscape ? finalPageInfo.horizontal : finalPageInfo.vertical;
}
}