devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
718 lines (703 loc) • 25.9 kB
JavaScript
/**
* DevExtreme (cjs/viz/core/export.js)
* Version: 24.2.6
* Build date: Mon Mar 17 2025
*
* Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
;
exports.plugin = exports.getMarkup = exports.exportWidgets = exports.exportFromMarkup = exports.combineMarkups = exports.ExportMenu = void 0;
var _extend = require("../../core/utils/extend");
var _window = require("../../core/utils/window");
var _utils = require("./utils");
var _svg = require("../../core/utils/svg");
var _exporter = require("../../exporter");
var _message = _interopRequireDefault(require("../../common/core/localization/message"));
var _type = require("../../core/utils/type");
var _themes = require("../themes");
var _hover = require("../../common/core/events/hover");
var _pointer = _interopRequireDefault(require("../../common/core/events/pointer"));
var _console = require("../../core/utils/console");
var _size = require("../../core/utils/size");
var _renderer = require("./renderers/renderer");
var _renderer2 = _interopRequireDefault(require("../../core/renderer"));
function _interopRequireDefault(e) {
return e && e.__esModule ? e : {
default: e
}
}
const pointerActions = [_pointer.default.down, _pointer.default.move].join(" ");
const BUTTON_SIZE = 35;
const ICON_COORDS = [
[9, 12, 26, 12, 26, 14, 9, 14],
[9, 17, 26, 17, 26, 19, 9, 19],
[9, 22, 26, 22, 26, 24, 9, 24]
];
const LIST_PADDING_TOP = 4;
const LIST_WIDTH = 120;
const VERTICAL_TEXT_MARGIN = 8;
const HORIZONTAL_TEXT_MARGIN = 15;
const MENU_ITEM_HEIGHT = 30;
const LIST_STROKE_WIDTH = 1;
const MARGIN = 10;
const SHADOW_OFFSET = 2;
const SHADOW_BLUR = 3;
const DEFAULT_EXPORT_FORMAT = "PNG";
const ALLOWED_IMAGE_FORMATS = ["PNG", "JPEG", "GIF"];
const ALLOWED_EXTRA_FORMATS = ["PDF", "SVG"];
const EXPORT_CSS_CLASS = "dx-export-menu";
const A4WidthCm = "21cm";
const EXPORT_DATA_KEY = "export-element-type";
const FORMAT_DATA_KEY = "export-element-format";
const GET_COLOR_REGEX = /data-backgroundcolor="([^"]*)"/;
function getRendererWrapper(width, height, backgroundColor) {
const rendererContainer = (0, _renderer2.default)("<div>").get(0);
const renderer = new _renderer.Renderer({
container: rendererContainer
});
renderer.resize(width, height);
renderer.root.element.setAttribute("data-backgroundcolor", backgroundColor);
return {
createGroup: () => renderer.g(),
getRootContent: () => renderer.root.element.cloneNode(true),
dispose() {
renderer.dispose();
rendererContainer.remove()
}
}
}
function getValidFormats() {
const imageFormats = _exporter.image.testFormats(ALLOWED_IMAGE_FORMATS);
return {
unsupported: imageFormats.unsupported,
supported: imageFormats.supported.concat(ALLOWED_EXTRA_FORMATS)
}
}
function validateFormat(format, incidentOccurred, validFormats) {
validFormats = validFormats || getValidFormats();
format = String(format).toUpperCase();
if (-1 !== validFormats.supported.indexOf(format)) {
return format
}
if (-1 !== validFormats.unsupported.indexOf(format)) {
incidentOccurred && incidentOccurred("W2108", [format])
}
}
function getCreatorFunc(format) {
if ("SVG" === format) {
return _exporter.svg.getData
} else if ("PDF" === format) {
return _exporter.pdf.getData
} else {
return _exporter.image.getData
}
}
function print(imageSrc, options) {
const document = (0, _window.getWindow)().document;
const iFrame = document.createElement("iframe");
iFrame.onload = setPrint(imageSrc, options);
iFrame.style.position = "fixed";
iFrame.style.width = "0";
iFrame.style.height = "0";
iFrame.style.right = "0";
iFrame.style.bottom = "0";
document.body.appendChild(iFrame)
}
function calculatePrintPageWidth(iFrameBody) {
iFrameBody.style.width = "21cm";
const width = (0, _size.getWidth)(iFrameBody);
iFrameBody.style.width = "";
return width
}
function setPrint(imageSrc, options) {
return function() {
let window = this.contentWindow;
const img = window.document.createElement("img");
window.document.body.appendChild(img);
const widthRatio = calculatePrintPageWidth(window.document.body) / options.width;
if (widthRatio < 1) {
window.document.body.style.transform = `scale(${widthRatio})`;
window.document.body.style["transform-origin"] = "0 0"
}
const removeFrame = () => {
this.parentElement.removeChild(this)
};
img.addEventListener("load", (() => {
window.focus();
window.print()
}));
img.addEventListener("error", removeFrame);
window.addEventListener("afterprint", (() => {
setTimeout(removeFrame, 0)
}));
img.src = imageSrc
}
}
function getItemAttributes(options, type, itemIndex) {
const x = -85;
const y = 40 + 30 * itemIndex;
const attr = {
rect: {
width: 118,
height: 30,
x: -84,
y: y
},
text: {
x: x + (options.rtl ? 105 : 15),
y: y + 30 - 8
}
};
if ("printing" === type) {
attr.separator = {
stroke: options.button.default.borderColor,
"stroke-width": 1,
cursor: "pointer",
sharp: "v",
d: "M -85 " + (y + 30 - 1) + " L 35 " + (y + 30 - 1)
}
}
return attr
}
function createMenuItem(renderer, options, settings) {
const itemData = {};
const type = settings.type;
const format = settings.format;
const attr = getItemAttributes(options, type, settings.itemIndex);
const fontStyle = (0, _utils.patchFontOptions)(options.font);
fontStyle["pointer-events"] = "none";
const menuItem = renderer.g().attr({
class: "dx-export-menu-list-item"
});
itemData[EXPORT_DATA_KEY] = type;
if (format) {
itemData[FORMAT_DATA_KEY] = format
}
const rect = renderer.rect();
rect.attr(attr.rect).css({
cursor: "pointer",
"pointer-events": "all"
}).data(itemData);
rect.on(_hover.start + ".export", (() => rect.attr({
fill: options.button.hover.backgroundColor
}))).on(_hover.end + ".export", (() => rect.attr({
fill: null
})));
rect.append(menuItem);
const text = renderer.text(settings.text).css(fontStyle).attr(attr.text).append(menuItem);
if ("printing" === type) {
renderer.path(null, "line").attr(attr.separator).append(menuItem)
}
return {
g: menuItem,
rect: rect,
resetState: () => rect.attr({
fill: null
}),
fixPosition: () => {
const textBBox = text.getBBox();
text.move(attr.text.x - textBBox.x - (options.rtl ? textBBox.width : 0))
}
}
}
function createMenuItems(renderer, options) {
let items = [];
if (options.printingEnabled) {
items.push(createMenuItem(renderer, options, {
type: "printing",
text: _message.default.format("vizExport-printingButtonText"),
itemIndex: items.length
}))
}
items = options.formats.reduce(((r, format) => {
r.push(createMenuItem(renderer, options, {
type: "exporting",
text: _message.default.getFormatter("vizExport-exportButtonText")(format),
format: format,
itemIndex: r.length
}));
return r
}), items);
return items
}
function getBackgroundColorFromMarkup(markup) {
const parsedMarkup = GET_COLOR_REGEX.exec(markup);
return null === parsedMarkup || void 0 === parsedMarkup ? void 0 : parsedMarkup[1]
}
const exportFromMarkup = function(markup, options) {
options.format = validateFormat(options.format) || "PNG";
options.fileName = options.fileName || "file";
options.exportingAction = options.onExporting;
options.exportedAction = options.onExported;
options.fileSavingAction = options.onFileSaving;
options.margin = (0, _type.isDefined)(options.margin) ? options.margin : 10;
options.backgroundColor = (0, _type.isDefined)(options.backgroundColor) ? options.backgroundColor : getBackgroundColorFromMarkup(markup) || (0, _themes.getTheme)().backgroundColor;
(0, _exporter.export)(markup, options, getCreatorFunc(options.format))
};
exports.exportFromMarkup = exportFromMarkup;
const getMarkup = widgets => combineMarkups(widgets).root.outerHTML;
exports.getMarkup = getMarkup;
const exportWidgets = function(widgets, options) {
options = options || {};
const markupInfo = combineMarkups(widgets, {
gridLayout: options.gridLayout,
verticalAlignment: options.verticalAlignment,
horizontalAlignment: options.horizontalAlignment
});
options.width = markupInfo.width;
options.height = markupInfo.height;
exportFromMarkup(markupInfo.root, options)
};
exports.exportWidgets = exportWidgets;
let combineMarkups = function(widgets) {
let options = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {};
if (!Array.isArray(widgets)) {
widgets = [
[widgets]
]
} else if (!Array.isArray(widgets[0])) {
widgets = widgets.map((item => [item]))
}
const compactView = !options.gridLayout;
const exportItems = widgets.reduce(((r, row, rowIndex) => {
const rowInfo = row.reduce(((r, item, colIndex) => {
const size = item.getSize();
const backgroundColor = item.option("backgroundColor") || (0, _themes.getTheme)(item.option("theme")).backgroundColor;
const node = (0, _renderer2.default)(item.element()).find("svg").get(0).cloneNode(true);
backgroundColor && -1 === r.backgroundColors.indexOf(backgroundColor) && r.backgroundColors.push(backgroundColor);
r.hOffset = r.width;
r.width += size.width;
r.height = Math.max(r.height, size.height);
r.itemWidth = Math.max(r.itemWidth, size.width);
r.items.push({
node: node,
width: size.width,
height: size.height,
c: colIndex,
r: rowIndex,
hOffset: r.hOffset
});
return r
}), {
items: [],
height: 0,
itemWidth: 0,
hOffset: 0,
width: 0,
backgroundColors: r.backgroundColors
});
r.rowOffsets.push(r.totalHeight);
r.rowHeights.push(rowInfo.height);
r.totalHeight += rowInfo.height;
r.items = r.items.concat(rowInfo.items);
r.itemWidth = Math.max(r.itemWidth, rowInfo.itemWidth);
r.maxItemLen = Math.max(r.maxItemLen, rowInfo.items.length);
r.totalWidth = compactView ? Math.max(r.totalWidth, rowInfo.width) : r.maxItemLen * r.itemWidth;
return r
}), {
items: [],
rowOffsets: [],
rowHeights: [],
itemWidth: 0,
totalHeight: 0,
maxItemLen: 0,
totalWidth: 0,
backgroundColors: []
});
const backgroundColor = `${1===exportItems.backgroundColors.length?exportItems.backgroundColors[0]:""}`;
const {
totalWidth: totalWidth,
totalHeight: totalHeight
} = exportItems;
const rootElement = wrapItemsToElement(totalWidth, totalHeight, backgroundColor, {
options: options,
exportItems: exportItems,
compactView: compactView
});
return {
root: rootElement,
width: totalWidth,
height: totalHeight
}
};
exports.combineMarkups = combineMarkups;
function wrapItemsToElement(width, height, backgroundColor, _ref) {
let {
exportItems: exportItems,
options: options,
compactView: compactView
} = _ref;
const rendererWrapper = getRendererWrapper(width, height, backgroundColor);
const getVOffset = item => {
const align = options.verticalAlignment;
const dy = exportItems.rowHeights[item.r] - item.height;
return exportItems.rowOffsets[item.r] + ("bottom" === align ? dy : "center" === align ? dy / 2 : 0)
};
const getHOffset = item => {
if (compactView) {
return item.hOffset
}
const align = options.horizontalAlignment;
const colWidth = exportItems.itemWidth;
const dx = colWidth - item.width;
return item.c * colWidth + ("right" === align ? dx : "center" === align ? dx / 2 : 0)
};
exportItems.items.forEach((item => {
const container = rendererWrapper.createGroup();
container.attr({
translateX: getHOffset(item),
translateY: getVOffset(item)
});
container.element.appendChild(item.node);
container.append()
}));
const result = rendererWrapper.getRootContent();
rendererWrapper.dispose();
return result
}
let ExportMenu = function(params) {
const renderer = this._renderer = params.renderer;
this._incidentOccurred = params.incidentOccurred;
this._exportTo = params.exportTo;
this._print = params.print;
this._shadow = renderer.shadowFilter("-50%", "-50%", "200%", "200%", 2, 6, 3);
this._shadow.attr({
opacity: .8
});
this._group = renderer.g().attr({
class: "dx-export-menu",
[_svg.HIDDEN_FOR_EXPORT]: true
}).linkOn(renderer.root, {
name: "export-menu",
after: "peripheral"
});
this._buttonGroup = renderer.g().attr({
class: "dx-export-menu-button"
}).append(this._group);
this._listGroup = renderer.g().attr({
class: "dx-export-menu-list"
}).append(this._group);
this._overlay = renderer.rect(-85, 39, 120, 0);
this._overlay.attr({
"stroke-width": 1,
cursor: "pointer",
rx: 4,
ry: 4,
filter: this._shadow.id
});
this._overlay.data({
"export-element-type": "list"
});
this.validFormats = getValidFormats();
this._subscribeEvents()
};
exports.ExportMenu = ExportMenu;
(0, _extend.extend)(ExportMenu.prototype, {
getLayoutOptions() {
if (this._hiddenDueToLayout) {
return {
width: 0,
height: 0,
cutSide: "vertical",
cutLayoutSide: "top"
}
}
const bBox = this._buttonGroup.getBBox();
bBox.cutSide = "vertical";
bBox.cutLayoutSide = "top";
bBox.height += 10;
bBox.position = {
vertical: "top",
horizontal: "right"
};
bBox.verticalAlignment = "top";
bBox.horizontalAlignment = "right";
return bBox
},
shift(_, y) {
this._group.attr({
translateY: this._group.attr("translateY") + y
})
},
draw(width, height, canvas) {
this._group.move(width - 35 - 2 - 3 + canvas.left, Math.floor(height / 2 - 17.5));
const layoutOptions = this.getLayoutOptions();
if (layoutOptions.width > width || layoutOptions.height > height) {
this.freeSpace()
}
return this
},
show() {
this._group.linkAppend()
},
hide() {
this._group.linkRemove()
},
setOptions(options) {
this._options = options;
if (options.formats) {
options.formats = options.formats.reduce(((r, format) => {
format = validateFormat(format, this._incidentOccurred, this.validFormats);
format && r.push(format);
return r
}), [])
} else {
options.formats = this.validFormats.supported.slice()
}
options.printingEnabled = void 0 === options.printingEnabled ? true : options.printingEnabled;
if (options.enabled && (options.formats.length || options.printingEnabled)) {
this.show();
this._updateButton();
this._updateList();
this._hideList()
} else {
this.hide()
}
},
dispose() {
this._unsubscribeEvents();
this._group.linkRemove().linkOff();
this._group.dispose();
this._shadow.dispose()
},
layoutOptions() {
return this._options.enabled && {
horizontalAlignment: "right",
verticalAlignment: "top",
weak: true
}
},
measure() {
this._fillSpace();
const margin = this._options.button.margin;
return [35 + margin.left + margin.right, 35 + margin.top + margin.bottom]
},
move(rect) {
const margin = this._options.button.margin;
this._group.attr({
translateX: Math.round(rect[0]) + margin.left,
translateY: Math.round(rect[1]) + margin.top
})
},
_fillSpace() {
this._hiddenDueToLayout = false;
this.show()
},
freeSpace() {
this._incidentOccurred("W2107");
this._hiddenDueToLayout = true;
this.hide()
},
_hideList() {
this._listGroup.remove();
this._listShown = false;
this._setButtonState("default");
this._menuItems.forEach((item => item.resetState()))
},
_showList() {
this._listGroup.append(this._group);
this._listShown = true;
this._menuItems.forEach((item => item.fixPosition()))
},
_setButtonState(state) {
const style = this._options.button[state];
this._button.attr({
stroke: style.borderColor,
fill: style.backgroundColor
});
this._icon.attr({
fill: style.color
})
},
_subscribeEvents() {
this._renderer.root.on(_pointer.default.up + ".export", (e => {
const elementType = e.target[EXPORT_DATA_KEY];
if (!elementType) {
if (this._button) {
this._hideList()
}
return
}
if ("button" === elementType) {
if (this._listShown) {
this._setButtonState("default");
this._hideList()
} else {
this._setButtonState("focus");
this._showList()
}
} else if ("printing" === elementType) {
this._print();
this._hideList()
} else if ("exporting" === elementType) {
this._exportTo(e.target[FORMAT_DATA_KEY]);
this._hideList()
}
}));
this._listGroup.on(pointerActions, (e => e.stopPropagation()));
this._buttonGroup.on(_pointer.default.enter, (() => this._setButtonState("hover")));
this._buttonGroup.on(_pointer.default.leave, (() => this._setButtonState(this._listShown ? "focus" : "default")));
this._buttonGroup.on(_pointer.default.down + ".export", (() => this._setButtonState("active")))
},
_unsubscribeEvents() {
this._renderer.root.off(".export");
this._listGroup.off();
this._buttonGroup.off()
},
_updateButton() {
const renderer = this._renderer;
const options = this._options;
const exportData = {
"export-element-type": "button"
};
if (!this._button) {
this._button = renderer.rect(0, 0, 35, 35).append(this._buttonGroup);
this._button.attr({
rx: 4,
ry: 4,
fill: options.button.default.backgroundColor,
stroke: options.button.default.borderColor,
"stroke-width": 1,
cursor: "pointer"
});
this._button.data(exportData);
this._icon = renderer.path(ICON_COORDS).append(this._buttonGroup);
this._icon.attr({
fill: options.button.default.color,
cursor: "pointer"
});
this._icon.data(exportData);
this._buttonGroup.setTitle(_message.default.format("vizExport-titleMenuText"))
}
},
_updateList() {
const options = this._options;
const buttonDefault = options.button.default;
const listGroup = this._listGroup;
const items = createMenuItems(this._renderer, options);
this._shadow.attr({
color: options.shadowColor
});
this._overlay.attr({
height: 30 * items.length + 2,
fill: buttonDefault.backgroundColor,
stroke: buttonDefault.borderColor
});
listGroup.clear();
this._overlay.append(listGroup);
items.forEach((item => item.g.append(listGroup)));
this._menuItems = items
}
});
function getExportOptions(widget, exportOptions, fileName, format) {
if (format || exportOptions.format) {
format = validateFormat(format || exportOptions.format, widget._incidentOccurred)
}
const {
width: width,
height: height
} = widget.getSize();
return {
format: format || "PNG",
fileName: fileName || exportOptions.fileName || "file",
backgroundColor: exportOptions.backgroundColor,
width: width,
height: height,
margin: exportOptions.margin,
svgToCanvas: exportOptions.svgToCanvas,
exportingAction: widget._createActionByOption("onExporting", {
excludeValidators: ["disabled"]
}),
exportedAction: widget._createActionByOption("onExported", {
excludeValidators: ["disabled"]
}),
fileSavingAction: widget._createActionByOption("onFileSaving", {
excludeValidators: ["disabled"]
})
}
}
const plugin = exports.plugin = {
name: "export",
init() {
this._exportMenu = new ExportMenu({
renderer: this._renderer,
incidentOccurred: this._incidentOccurred,
print: () => this.print(),
exportTo: format => this.exportTo(void 0, format)
});
this._layout.add(this._exportMenu)
},
dispose() {
this._exportMenu.dispose()
},
members: {
_getExportMenuOptions() {
return (0, _extend.extend)({}, this._getOption("export"), {
rtl: this._getOption("rtlEnabled", true)
})
},
_disablePointerEvents() {
const pointerEventsValue = this._renderer.root.attr("pointer-events");
this._renderer.root.attr({
"pointer-events": "none"
});
return pointerEventsValue
},
exportTo(fileName, format) {
const menu = this._exportMenu;
const options = getExportOptions(this, this._getOption("export") || {}, fileName, format);
menu && menu.hide();
const pointerEventsValue = this._disablePointerEvents();
const promise = (0, _exporter.export)(this._renderer.root.element, options, getCreatorFunc(options.format)).fail(_console.logger.error).always((() => {
this._renderer.root.attr({
"pointer-events": pointerEventsValue
})
}));
menu && menu.show();
return promise
},
print() {
const menu = this._exportMenu;
const options = getExportOptions(this, this._getOption("export") || {});
options.exportingAction = null;
options.exportedAction = null;
options.margin = 0;
options.format = "PNG";
options.useBase64 = true;
options.fileSavingAction = eventArgs => {
print(`data:image/png;base64,${eventArgs.data}`, {
width: options.width,
__test: options.__test
});
eventArgs.cancel = true
};
const pointerEventsValue = this._disablePointerEvents();
menu && menu.hide();
const promise = (0, _exporter.export)(this._renderer.root.element, options, getCreatorFunc(options.format)).fail(_console.logger.error).always((() => {
this._renderer.root.attr({
"pointer-events": pointerEventsValue
})
}));
menu && menu.show();
return promise
}
},
customize(constructor) {
const proto = constructor.prototype;
constructor.addChange({
code: "EXPORT",
handler() {
this._exportMenu.setOptions(this._getExportMenuOptions());
this._change(["LAYOUT"])
},
isThemeDependent: true,
isOptionChange: true,
option: "export"
});
proto._optionChangesMap.onExporting = "EXPORT";
proto._optionChangesMap.onExported = "EXPORT";
proto._optionChangesMap.onFileSaving = "EXPORT"
},
fontFields: ["export.font"]
};