reportbro-designer
Version:
Designer to create pdf and excel report layouts. The reports can be generated with reportbro-lib (a Python package) on the server.
1,042 lines (983 loc) • 90.3 kB
JavaScript
import Document from './Document';
import PopupWindow from './PopupWindow';
import AddDeleteDocElementCmd from './commands/AddDeleteDocElementCmd';
import AddDeleteParameterCmd from './commands/AddDeleteParameterCmd';
import AddDeleteStyleCmd from './commands/AddDeleteStyleCmd';
import Command from './commands/Command';
import CommandGroupCmd from './commands/CommandGroupCmd';
import SetValueCmd from './commands/SetValueCmd';
import Band from './container/Band';
import Container from './container/Container';
import Page from './container/Page';
import DocumentProperties from './data/DocumentProperties';
import Parameter from './data/Parameter';
import Style from './data/Style';
import DocElement from './elements/DocElement';
import FrameElement from './elements/FrameElement';
import PageBreakElement from './elements/PageBreakElement';
import SectionElement from './elements/SectionElement';
import TableElement from './elements/TableElement';
import TableTextElement from './elements/TableTextElement';
import locales from './i18n/locales';
import DocElementPanel from './panels/DocElementPanel';
import DocumentPropertiesPanel from './panels/DocumentPropertiesPanel';
import EmptyDetailPanel from './panels/EmptyDetailPanel';
import ParameterPanel from './panels/ParameterPanel';
import StylePanel from './panels/StylePanel';
import MainPanel from './menu/MainPanel';
import MainPanelItem from './menu/MainPanelItem';
import MenuPanel from './menu/MenuPanel';
import * as utils from './utils';
/**
* Used for the main ReportBro instance.
* @class
*/
export default class ReportBro {
constructor(element, properties) {
this.element = element;
this.nextId = 1;
// version of returned report data, version is needed when loading reports in older format
this.version = 5;
this.locale = locales[(properties && properties.localeKey) || 'en_us'];
if (properties && properties['locale']) {
Object.assign(this.locale, properties['locale']);
}
this.properties = {
additionalFonts: [],
adminMode: true,
autoSaveOnPreview: false,
cmdExecutedCallback: null,
colors: [
"#000000","#444444","#666666","#999999","#cccccc","#eeeeee","#f3f3f3","#ffffff",
"#ff0000","#ff9900","#ffff00","#00ff00","#00ffff","#0000ff","#9900ff","#ff00ff",
"#ea9999","#f9cb9c","#ffe599","#b6d7a8","#a2c4c9","#9fc5e8","#b4a7d6","#d5a6bd",
"#e06666","#f6b26b","#ffd966","#93c47d","#76a5af","#6fa8dc","#8e7cc3","#c27ba0",
"#cc0000","#e69138","#f1c232","#6aa84f","#45818e","#3d85c6","#674ea7","#a64d79",
"#990000","#b45f06","#bf9000","#38761d","#134f5c","#0b5394","#351c75","#741b47",
"#660000","#783f04","#7f6000","#274e13","#0c343d","#073763","#20124d","#4c1130"
],
defaultFont: Style.font.helvetica,
enableSpreadsheet: true,
fontSizes: [4,5,6,7,8,9,10,11,12,13,14,15,16,18,20,22,24,26,28,32,36,40,44,48,54,60,66,72,80],
fonts: [
{ name: 'Courier', value: Style.font.courier },
{ name: 'Helvetica', value: Style.font.helvetica },
{ name: 'Times New Roman', value: Style.font.times }
],
highlightUnusedParameters: false,
imageLimit: null,
imageMaxSize: null,
imageRequireWebPFormat: false,
localStorageReportKey: null,
menuShowButtonLabels: false,
menuShowDebug: false,
menuSidebar: false,
patternAdditionalDates: [],
patternAdditionalNumbers: [],
patternCurrencySymbol: '$',
patternDates: [
{ name: 'd.M.yyyy', description: this.locale['patternDate1'] },
{ name: 'd.M.yy, H:mm', description: this.locale['patternDate2'] },
{ name: 'd/MMM/yyyy', description: this.locale['patternDate3'] },
{ name: 'MM/dd/yyyy', description: this.locale['patternDate4'] }
],
patternLocale: 'en',
patternLocales: ['de', 'en', 'es', 'fr', 'it', 'pt'],
patternNumberGroupSymbol: '',
patternNumbers: [
{ name: '#,##0', description: this.locale['patternNumber1'] },
{ name: '0.000', description: this.locale['patternNumber2'] },
{ name: '0.00##', description: this.locale['patternNumber3'] },
{ name: '#,##0.00', description: this.locale['patternNumber4'] },
{ name: '$ #,##0.00', description: this.locale['patternNumber5'] }
],
reportServerBasicAuth: null,
reportServerHeaders: {},
reportServerTimeout: 20000,
reportServerUrl: 'https://www.reportbro.com/report/run',
reportServerUrlCrossDomain: false,
requestCallback: null,
saveCallback: null,
selectCallback: null,
showGrid: true,
showPlusFeatures: true,
showPlusFeaturesInfo: true,
theme: ''
};
if (properties) {
for (let prop in properties) {
if (this.properties.hasOwnProperty(prop)) {
this.properties[prop] = properties[prop];
}
}
}
if (this.properties.additionalFonts.length > 0) {
this.properties.fonts = this.properties.fonts.concat(this.properties.additionalFonts);
}
// make sure defaultFont is available, otherwise use first entry of font list
let defaultFontExists = false;
for (let font of this.properties.fonts) {
if (this.properties.defaultFont === font.value) {
defaultFontExists = true;
break;
}
}
if (!defaultFontExists) {
if (this.properties.fonts.length > 0) {
this.properties.defaultFont = this.properties.fonts[0].value;
} else {
this.properties.defaultFont = '';
}
}
if (this.properties.patternAdditionalDates.length > 0) {
this.properties.patternDates = this.properties.patternDates.concat(this.properties.patternAdditionalDates);
}
if (this.properties.patternAdditionalNumbers.length > 0) {
this.properties.patternNumbers =
this.properties.patternNumbers.concat(this.properties.patternAdditionalNumbers);
}
if (!this.validateProperties(this.properties)) {
throw 'Invalid properties for ReportBro instance, check console error log for further details';
}
this.document = new Document(element, this.properties.showGrid, this);
this.popupWindow = new PopupWindow(element, this);
this.docElements = [];
this.headerBand = new Band(Band.bandType.header, false, '', '', this);
this.contentBand = new Band(Band.bandType.content, false, '', '', this);
this.footerBand = new Band(Band.bandType.footer, false, '', '', this);
this.parameterContainer = new Container('0_parameters', this.getLabel('parameters'), this);
this.styleContainer = new Container('0_styles', this.getLabel('styles'), this);
this.watermarkTextContainer = new Page('0_watermark_texts', this.getLabel('watermarkTexts'), this);
this.watermarkImageContainer = new Page('0_watermark_images', this.getLabel('watermarkImages'), this);
this.documentProperties = new DocumentProperties(this);
this.mainPanel = new MainPanel(
element, this.headerBand, this.contentBand, this.footerBand,
this.parameterContainer, this.styleContainer, this);
this.menuPanel = new MenuPanel(element, this);
this.activeDetailPanel = 'none';
this.detailPanels = {
'none': new EmptyDetailPanel(element, this),
'docElement': new DocElementPanel(element, this),
'parameter': new ParameterPanel(element, this),
'style': new StylePanel(element, this),
'documentProperties': new DocumentPropertiesPanel(element, this)
};
this.commandStack = [];
this.lastCommandIndex = -1;
this.savedCommandIndex = -1;
this.modified = false;
this.selectionSinceLastCommand = false;
this.objectMap = {};
this.containers = [this.headerBand, this.contentBand, this.footerBand];
this.selections = [];
this.reportKey = null; // key of last report preview to allow download of xlsx file for this report
this.browserDragType = '';
this.browserDragId = '';
this.documentProperties.setPanelItem(this.mainPanel.getDocumentPropertiesItem());
this.initObjectMap();
// Ctrl + C: copy
document.addEventListener('copy', (event) => {
if (document.activeElement instanceof HTMLInputElement ||
document.activeElement instanceof HTMLTextAreaElement ||
(document.activeElement && document.activeElement.classList.contains('ql-editor'))) {
// if current active element is an input, textarea or rich text editor we ignore the copy event
// and keep the default behavior
return;
}
if (this.selections.length > 0) {
const clipboardElements = [];
const idMap = {};
let serializedObj;
let i;
for (const selectionId of this.selections) {
const obj = this.getDataObject(selectionId);
if ((obj instanceof DocElement && !(obj instanceof TableTextElement)) ||
(obj instanceof Parameter && !obj.showOnlyNameType) ||
(obj instanceof Style)) {
if (!(obj.getId() in idMap)) {
idMap[obj.getId()] = true;
serializedObj = obj.toJS();
clipboardElements.push(serializedObj);
if (obj instanceof DocElement) {
serializedObj.baseClass = 'DocElement';
if (obj instanceof FrameElement) {
let nestedElements = [];
obj.appendContainerChildren(nestedElements);
for (let nestedElement of nestedElements) {
if (nestedElement.getId() in idMap) {
// in case a nested element is also selected we make sure
// to add it only once to the clipboard objects and to
// add it after its parent element
for (i = 0; i < clipboardElements.length; i++) {
if (nestedElement.getId() === clipboardElements[i].id) {
clipboardElements.splice(i, 1);
break;
}
}
} else {
idMap[nestedElement.getId()] = true;
}
serializedObj = nestedElement.toJS();
serializedObj.baseClass = 'DocElement';
clipboardElements.push(serializedObj);
}
}
} else if (obj instanceof Parameter) {
serializedObj.baseClass = 'Parameter';
} else if (obj instanceof Style) {
serializedObj.baseClass = 'Style';
}
}
}
}
event.preventDefault();
const json = JSON.stringify({ elements: clipboardElements, version: this.version });
event.clipboardData.setData('application/json', json);
}
});
// Ctrl + V: paste
document.addEventListener('paste', (event) => {
if (document.activeElement instanceof HTMLInputElement ||
document.activeElement instanceof HTMLTextAreaElement ||
(document.activeElement && document.activeElement.classList.contains('ql-editor'))) {
// if current active element is an input, textarea or rich text editor we ignore the paste event
// and keep the default behavior
return;
}
for (const item of event.clipboardData.items) {
const { kind, type } = item;
if (kind === 'string' && type === 'application/json') {
item.getAsString((content) => {
try {
const data = JSON.parse(content);
// only paste elements in clipboard if they are stored in the same report version
// as the current ReportBro version
if (data.version === this.version) {
let cmd;
let cmdGroup = new CommandGroupCmd('Paste from clipboard', this);
let mappedContainerIds = {};
let pastedElements = [];
for (let clipboardElement of data.elements) {
// create new pasted element to change properties (id, name, etc.) and
// leave clipboard elements unchanged
let pastedElement = Object.assign({}, clipboardElement);
pastedElement.id = this.getUniqueId();
pastedElements.push(pastedElement);
if (pastedElement.baseClass === 'DocElement') {
if (pastedElement.linkedContainerId) {
let linkedContainerId = this.getUniqueId();
mappedContainerIds[pastedElement.linkedContainerId] = linkedContainerId;
pastedElement.linkedContainerId = linkedContainerId;
}
if (pastedElement.elementType === DocElement.type.table) {
TableElement.removeIds(pastedElement);
}
}
}
for (let pastedElement of pastedElements) {
if (pastedElement.baseClass === 'DocElement') {
// map id of container in case element is inside other
// pasted container (frame/band)
if (pastedElement.containerId in mappedContainerIds) {
pastedElement.containerId = mappedContainerIds[pastedElement.containerId];
// since element is inside pasted container we can keep x/y coordinates
} else {
let pasteToY = 0;
let container = this.getDataObject(pastedElement.containerId);
if (container !== null) {
// determine new y-coord so pasted element is in
// visible area of scrollable document
let containerOffset = container.getOffset();
let containerSize = container.getContentSize();
let contentScrollY = this.getDocument().getContentScrollPosY();
if (contentScrollY > containerOffset.y &&
(contentScrollY + pastedElement.height) <
(containerOffset.y + containerSize.height)) {
pasteToY = contentScrollY - containerOffset.y;
}
}
pastedElement.x = 0;
pastedElement.y = pasteToY;
}
cmd = new AddDeleteDocElementCmd(
true, pastedElement.elementType, pastedElement,
pastedElement.id, pastedElement.containerId, -1, this);
cmdGroup.addCommand(cmd);
} else if (pastedElement.baseClass === 'Parameter' ||
pastedElement.baseClass === 'Style') {
// try to find unique name for pasted element by using a suffix
let copySuffix = this.getLabel('nameCopySuffix');
let pastedElementName = pastedElement.name + ` (${copySuffix})`;
let panelItem = (pastedElement.baseClass === 'Parameter') ?
this.parameterContainer.getPanelItem() : this.styleContainer.getPanelItem();
if (panelItem !== null) {
if (panelItem.getChildByName(pastedElementName)) {
for (let paramNr = 2; paramNr <= 99; paramNr++) {
pastedElementName = pastedElement.name +
` (${copySuffix} ${paramNr})`;
if (panelItem.getChildByName(pastedElementName) === null) {
break;
}
}
}
}
pastedElement.name = pastedElementName;
if (pastedElement.baseClass === 'Parameter') {
Parameter.removeIds(pastedElement);
cmd = new AddDeleteParameterCmd(
true, pastedElement, pastedElement.id,
this.parameterContainer.getId(), -1, this);
cmdGroup.addCommand(cmd);
} else if (pastedElement.baseClass === 'Style') {
cmd = new AddDeleteStyleCmd(
true, pastedElement, pastedElement.id,
this.styleContainer.getId(), -1, this);
cmdGroup.addCommand(cmd);
}
}
}
if (!cmdGroup.isEmpty()) {
this.executeCommand(cmdGroup);
let clearSelection = true;
for (let pastedElement of pastedElements) {
this.selectObject(pastedElement.id, clearSelection);
clearSelection = false;
}
}
event.preventDefault();
}
} catch (e) {
}
});
}
}
});
this.keydownEventListener = (event) => {
if (this.detailPanels[this.activeDetailPanel].isKeyEventDisabled()) {
return;
}
// check metaKey instead of ctrl for Mac
if (event.metaKey || event.ctrlKey) {
switch (event.which) {
case 89: {
// Ctrl + Y: redo
this.redoCommand();
event.preventDefault();
break;
}
case 90: {
// Ctrl + Z: undo
this.undoCommand();
event.preventDefault();
break;
}
}
} else {
if (event.which === 27) { // escape
this.popupWindow.hide();
}
else if (!(event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement)) {
switch (event.which) {
case 8: // backspace
case 46: { // delete
let cmdGroup = new CommandGroupCmd('Delete', this);
for (let selectionId of this.selections) {
let obj = this.getDataObject(selectionId);
if (obj instanceof DocElement) {
obj.addCommandsForDelete(cmdGroup);
}
}
if (!cmdGroup.isEmpty()) {
this.executeCommand(cmdGroup);
}
event.preventDefault();
break;
}
case 37: // left
case 38: // up
case 39: // right
case 40: { // down
let cmdGroup = new CommandGroupCmd('Move element', this);
let field = (event.which === 37 || event.which === 39) ? 'x' : 'y';
let bandWidth = this.getDocumentProperties().getContentSize().width;
for (let selectionId of this.selections) {
let obj = this.getDataObject(selectionId);
if (obj instanceof DocElement) {
if (obj.hasProperty(field)) {
let val = null;
if (event.which === 37) {
if (obj.getValue('xVal') > 0) {
val = obj.getValue('xVal') - 1;
}
} else if (event.which === 38) {
if (obj.getValue('yVal') > 0) {
val = obj.getValue('yVal') - 1;
}
} else if (event.which === 39) {
let containerSize = obj.getContainerContentSize();
if ((obj.getValue('xVal') + obj.getValue('widthVal')) < containerSize.width) {
val = obj.getValue('xVal') + 1;
}
} else if (event.which === 40) {
let containerSize = obj.getContainerContentSize();
if ((obj.getValue('yVal') + obj.getValue('heightVal')) < containerSize.height) {
val = obj.getValue('yVal') + 1;
}
}
if (val !== null) {
let cmd = new SetValueCmd(
selectionId, field, val, SetValueCmd.type.text, this);
cmdGroup.addCommand(cmd);
}
}
}
}
if (!cmdGroup.isEmpty()) {
this.executeCommand(cmdGroup);
}
event.preventDefault();
break;
}
}
}
}
};
document.addEventListener('keydown', this.keydownEventListener);
this.render();
this.setup();
}
/**
* Validate properties used to initialize ReportBro.
* In case of invalid properties additional info will be printed to the JS console.
* @param {Object[]} properties - properties to validate
* @returns {Boolean} true if all properties are valid, false otherwise.
*/
validateProperties(properties) {
if (!Array.isArray(properties.colors)) {
console.error('"colors" property must be an array');
return false;
}
let colorBlackExists = false;
for (const color of properties.colors) {
if (!color || !utils.isValidColor(color)) {
console.error('"colors" property contains invalid color value');
return false;
}
if (color === '#000000') {
colorBlackExists = true;
}
}
if (!colorBlackExists) {
console.error('"colors" property is missing black color "#000000"');
return false;
}
if (!Array.isArray(properties.fontSizes)) {
console.error('"fontSizes" property must be an array');
return false;
}
for (const fontSize of properties.fontSizes) {
if (typeof fontSize !== 'number' || fontSize < 1) {
console.error('"fontSizes" property must contain only numbers (> 0)');
return false;
}
}
if (!Array.isArray(properties.patternLocales)) {
console.error('"patternLocales" property must be an array');
return false;
}
return true;
}
/**
* Add main panel items for sub categories like watermark texts and images.
*
* This must be called after the main panel was rendered because children of main panel items can
* only be added after the parent is rendered.
*/
addMainPanelItemSubCategories() {
const watermarkTextsItem = new MainPanelItem(
'watermarkText', this.mainPanel.getWatermarksItem(), this.watermarkTextContainer, {
hasChildren: true, showAdd: true, showDelete: false, hasDetails: false, draggable: false }, this,
);
this.watermarkTextContainer.setPanelItem(watermarkTextsItem);
this.mainPanel.getWatermarksItem().appendChild(watermarkTextsItem);
this.watermarkTextContainer.setup();
const watermarkImagesItem = new MainPanelItem(
'watermarkImage', this.mainPanel.getWatermarksItem(), this.watermarkImageContainer, {
hasChildren: true, showAdd: true, showDelete: false, hasDetails: false, draggable: false }, this,
);
this.watermarkImageContainer.setPanelItem(watermarkImagesItem);
this.mainPanel.getWatermarksItem().appendChild(watermarkImagesItem);
this.watermarkImageContainer.setup();
}
/**
* Adds default parameters like page count/number.
*/
addDefaultParameters() {
for (let parameterData of [
{ name: 'page_count', type: Parameter.type.number, eval: false, editable: false, showOnlyNameType: true },
{ name: 'page_number', type: Parameter.type.number, eval: false, editable: false, showOnlyNameType: true }
]) {
let parameter = new Parameter(this.getUniqueId(), parameterData, this);
let parentPanel = this.mainPanel.getParametersItem();
let panelItem = new MainPanelItem(
'parameter', parentPanel, parameter, {
hasChildren: false, showAdd: false, showDelete: false, draggable: false }, this
);
parameter.setPanelItem(panelItem);
parentPanel.appendChild(panelItem);
parameter.setup();
this.addParameter(parameter);
}
}
render() {
utils.emptyElement(this.element);
if (this.getProperty('menuSidebar')) {
this.element.classList.add('rbroMenuPanelSidebar');
}
if (this.getProperty('theme') === 'classic') {
document.body.classList.add('rbroClassicTheme');
} else {
document.body.classList.add('rbroDefaultTheme');
}
this.element.append(utils.createElement('div', { class: 'rbroLogo' }));
this.element.append(utils.createElement('div', { id: 'rbro_menu_panel', class: 'rbroMenuPanel' }));
const elContainer = utils.createElement('div', { class: 'rbroContainer' });
const elMainPanel = utils.createElement('div', { id: 'rbro_main_panel', class: 'rbroMainPanel' });
elMainPanel.append(utils.createElement('ul', { id: 'rbro_main_panel_list' }));
elContainer.append(elMainPanel);
elContainer.append(utils.createElement('div', { id: 'rbro_main_panel_sizer', class: 'rbroMainPanelSizer' }));
elContainer.append(utils.createElement('div', { id: 'rbro_detail_panel', class: 'rbroDetailPanel' }));
elContainer.append(utils.createElement('div', { id: 'rbro_document_panel', class: 'rbroDocumentPanel' }));
this.element.append(elContainer);
this.mainPanel.render();
this.menuPanel.render();
for (let panelName in this.detailPanels) {
this.detailPanels[panelName].render();
}
this.detailPanels[this.activeDetailPanel].show();
this.document.render();
this.popupWindow.render();
this.updateMenuButtons();
this.mouseupEventListener = (event) => {
this.mainPanel.mouseUp(event);
this.document.mouseUp(event);
this.popupWindow.hide();
};
document.addEventListener('mouseup', this.mouseupEventListener);
window.addEventListener('resize', (event) => {
this.document.windowResized();
});
this.element.addEventListener('dragstart', (event) => {
// disable dragging per default, otherwise e.g. a text selection can be dragged in Chrome
event.preventDefault();
});
this.element.addEventListener('mousemove', (event) => {
if (!this.mainPanel.processMouseMove(event)) {
this.document.processMouseMove(event);
}
});
}
/**
* Returns total width of element containing ReportBro Designer.
* @returns {Number}
*/
getWidth() {
return this.element.clientWidth;
}
setup() {
this.addMainPanelItemSubCategories();
this.addDefaultParameters();
this.headerBand.setup();
this.contentBand.setup();
this.footerBand.setup();
this.documentProperties.setup();
}
initObjectMap() {
this.addDataObject(this.headerBand);
this.addDataObject(this.contentBand);
this.addDataObject(this.footerBand);
this.addDataObject(this.parameterContainer);
this.addDataObject(this.styleContainer);
this.addDataObject(this.watermarkTextContainer);
this.addDataObject(this.watermarkImageContainer);
this.addDataObject(this.documentProperties);
}
/**
* Returns the label for given key.
* @param {String} key
* @returns {String} Label for given key, if it does not exist then the key is returned.
*/
getLabel(key) {
if (key in this.locale) {
return this.locale[key];
}
return key;
}
/**
* Get ReportBro property.
* @param {String} key - property name
* @returns {*}
*/
getProperty(key) {
return this.properties[key];
}
getMainPanel() {
return this.mainPanel;
}
getMenuPanel() {
return this.menuPanel;
}
getDocument() {
return this.document;
}
getPopupWindow() {
return this.popupWindow;
}
getFonts() {
return this.properties.fonts;
}
/**
* Returns a list of all number and date patterns.
* @returns {Object[]} Each item contains name (String), optional description (String) and
* optional separator (Boolean).
*/
getPatterns() {
let patterns = [];
if (this.properties.patternNumbers.length > 0) {
patterns.push({ separator: true, name: this.getLabel('patternSeparatorNumbers') });
for (let pattern of this.properties.patternNumbers) {
patterns.push(pattern);
}
}
if (this.properties.patternDates.length > 0) {
patterns.push({ separator: true, name: this.getLabel('patternSeparatorDates') });
for (let pattern of this.properties.patternDates) {
patterns.push(pattern);
}
}
return patterns;
}
/**
* Returns a list of parameter items.
* Used for parameter popup window.
* @param {DocElement|Parameter} obj - adds all parameters available for
* this object (which is either a doc element or a parameter).
* For doc elements the parameters from the data source
* are included (e.g. array field parameters of a table data source).
* @param {?String[]} allowedTypes - specify allowed parameter types which will be added to the
* parameters list. If not set all parameter types are allowed.
* @returns {Object[]} Each item contains name (String), optional description (String) and
* optional separator (Boolean).
*/
getParameterItems(obj, allowedTypes) {
const parameters = [];
const parameterItems = this.getMainPanel().getParametersItem().getChildren();
// dataSourceIndex is only needed for separator id which is used to hide the separator
// when there are no data source parameters available (due to search filter)
let dataSourceIndex = 0;
if (obj instanceof DocElement) {
const dataSources = obj.getAllDataSources();
let firstDataSource = true;
for (const dataSource of dataSources) {
if (dataSource.parameters.length > 0) {
let groupName;
if (firstDataSource) {
groupName = this.getLabel('parametersDataSource');
} else {
// include data source name in group label to better distinguish groups
// of additional data sources
groupName = this.getLabel('parametersDataSourceName').replace('${name}', dataSource.name);
}
parameters.push({
separator: true,
separatorClass: 'rbroParameterDataSourceGroup',
id: 'ds' + dataSourceIndex,
name: groupName
});
// add all parameters of collections at end of data source parameters
// with a header containing the collection name
const mapParameters = [];
// do not append data source name for first data source as this is the default data source
const dataSourceName = firstDataSource ? null : dataSource.name;
for (const dataSourceParameter of dataSource.parameters) {
if (dataSourceParameter.type === Parameter.type.map) {
dataSourceParameter.appendParameterItems(mapParameters, allowedTypes, dataSourceName);
} else {
dataSourceParameter.appendParameterItems(parameters, allowedTypes, dataSourceName);
}
}
for (const mapParameter of mapParameters) {
parameters.push(mapParameter);
}
}
firstDataSource = false;
dataSourceIndex++;
}
} else if (obj instanceof Parameter) {
let parent = obj.getParent();
while (parent !== null) {
if (parent.type === Parameter.type.array) {
// parameter is inside a list -> set dataSourceIndex so data source prefix is
// set for root parameters
dataSourceIndex = 1;
parent.appendFieldParameterItems(parameters, allowedTypes, true, null);
break;
}
parent = parent.getParent();
}
}
// if there is at least one data source the parameter list is returned for an element with a data source.
// therefor we set the data source for root parameters to empty string instead of null. this way
// the root parameters are displayed with a colon prefix (e.g. ":address" instead of "address")
// in the parameter popup window which makes it possible to explicitly reference a root parameter.
const rootDataSourceName = dataSourceIndex > 0 ? '' : null;
parameters.push({ separator: true, name: this.getLabel('parameters') });
// add all parameters of collections at end of list with a header containing the collection name
const mapParameters = [];
for (const parameterItem of parameterItems) {
const parameter = parameterItem.getData();
if (parameter.getValue('type') === Parameter.type.map) {
parameter.appendParameterItems(mapParameters, allowedTypes, rootDataSourceName);
} else {
parameter.appendParameterItems(parameters, allowedTypes, rootDataSourceName);
}
}
return parameters.concat(mapParameters);
}
/**
* Returns a list of all array field parameter items.
* Used for parameter popup window.
* @param {String[]} allowedTypes - specify allowed parameter types which will
* be added to the parameters list. If not set all parameter types are allowed.
* @returns {Object[]} Each item contains name (String), optional description (String) and
* optional separator (Boolean).
*/
getArrayFieldParameterItems(allowedTypes) {
let parameters = [];
let parameterItems = this.getMainPanel().getParametersItem().getChildren();
parameters.push({ separator: true, name: this.getLabel('parameters') });
for (let parameterItem of parameterItems) {
let parameter = parameterItem.getData();
if (parameter.getValue('type') === Parameter.type.array) {
parameter.appendFieldParameterItems(parameters, allowedTypes, false, null);
}
}
return parameters;
}
/**
* Append document elements of given container.
* @param {Container} container
* @param {Boolean} asObjects - if true the document element instances are returned, otherwise
* each instance is transformed to a js map.
* @param {DocElement[]} docElements - list where document elements will be appended to.
*/
appendContainerDocElements(container, asObjects, docElements) {
let children = container.getPanelItem().getChildren();
for (let child of children) {
if (child.getData() instanceof DocElement) {
let docElement = child.getData();
if (asObjects) {
docElements.push(docElement);
// we are also adding all internal children (document elements which belong
// to other document elements and cannot be created independently),
// e.g. a table band or a table cell (table text) of a table element.
docElement.addChildren(docElements);
} else {
// js map also includes data of internal children
docElements.push(docElement.toJS());
}
let containers = [];
if (docElement instanceof SectionElement) {
containers = docElement.getLinkedContainers();
} else {
let linkedContainer = docElement.getLinkedContainer();
if (linkedContainer !== null) {
containers.push(linkedContainer);
}
}
// add children of doc elements which represent containers, e.g. frames or section bands
for (let container of containers) {
this.appendContainerDocElements(container, asObjects, docElements);
}
}
}
};
/**
* Get document elements of all bands.
* @param {Boolean} asObjects - if true the document element instances are returned, otherwise
* each instance is transformed to a js map.
* @returns {DocElement[]} List of document elements.
*/
getDocElements(asObjects) {
let docElements = [];
this.appendContainerDocElements(this.headerBand, asObjects, docElements);
this.appendContainerDocElements(this.contentBand, asObjects, docElements);
this.appendContainerDocElements(this.footerBand, asObjects, docElements);
return docElements;
}
setDetailPanel(panelName) {
if (panelName !== this.activeDetailPanel) {
this.detailPanels[this.activeDetailPanel].hide();
this.activeDetailPanel = panelName;
this.detailPanels[panelName].show();
}
}
/**
* Is called when a data object was modified (including new and deleted data objects).
* @param {*} obj - new/deleted/modified data object.
* @param {String} operation - operation which caused the notification.
* @param {String} [field] - affected field in case of change operation.
*/
notifyEvent(obj, operation, field) {
this.detailPanels[this.activeDetailPanel].notifyEvent(obj, operation, field);
}
addParameter(parameter) {
this.addDataObject(parameter);
}
addStyle(style) {
this.addDataObject(style);
this.notifyEvent(style, Command.operation.add);
}
getStyles() {
let styles = [];
for (const styleItem of this.getMainPanel().getStylesItem().getChildren()) {
styles.push(styleItem.getData());
}
return styles;
}
getParameters() {
const parameters = [];
for (const parameterItem of this.getMainPanel().getParametersItem().getChildren()) {
parameters.push(parameterItem.getData());
}
return parameters;
}
getWatermarks() {
const watermarks = [];
for (const watermarkTextItem of this.watermarkTextContainer.getPanelItem().getChildren()) {
watermarks.push(watermarkTextItem.getData());
}
for (const watermarkImageItem of this.watermarkImageContainer.getPanelItem().getChildren()) {
watermarks.push(watermarkImageItem.getData());
}
return watermarks;
}
addDocElement(element) {
this.docElements.push(element);
this.addDataObject(element);
}
deleteDocElements() {
for (let i=0; i < this.docElements.length; i++) {
this.deleteDataObject(this.docElements[i]);
}
this.docElements = [];
}
getDocumentProperties() {
return this.documentProperties;
}
executeCommand(cmd) {
cmd.do();
if (this.lastCommandIndex < (this.commandStack.length - 1)) {
this.commandStack = this.commandStack.slice(0, this.lastCommandIndex + 1);
}
if (!this.selectionSinceLastCommand && this.commandStack.length > 0) {
// if previous command can be replaced by current command
// we can discard the previous command and only keep the latest update
let prevCmd = this.commandStack[this.commandStack.length - 1];
if (cmd.allowReplace(prevCmd)) {
cmd.replace(prevCmd);
this.commandStack = this.commandStack.slice(0, this.commandStack.length - 1);
this.lastCommandIndex--;
}
}
this.commandStack.push(cmd);
this.lastCommandIndex++;
this.modified = true;
this.selectionSinceLastCommand = false;
this.updateMenuButtons();
if (this.properties.cmdExecutedCallback) {
this.properties.cmdExecutedCallback(cmd, true);
}
}
undoCommand() {
if (this.lastCommandIndex >= 0) {
let cmd = this.commandStack[this.lastCommandIndex];
cmd.undo();
this.lastCommandIndex--;
this.modified = (this.lastCommandIndex !== this.savedCommandIndex);
this.updateMenuButtons();
if (this.properties.cmdExecutedCallback) {
this.properties.cmdExecutedCallback(cmd, false);
}
}
}
redoCommand() {
if (this.lastCommandIndex < (this.commandStack.length - 1)) {
this.lastCommandIndex++;
let cmd = this.commandStack[this.lastCommandIndex];
cmd.do();
this.modified = (this.lastCommandIndex !== this.savedCommandIndex);
this.updateMenuButtons();
if (this.properties.cmdExecutedCallback) {
this.properties.cmdExecutedCallback(cmd, false);
}
}
}
updateMenuButtons() {
document.getElementById('rbro_menu_save').disabled = !this.modified;
document.getElementById('rbro_menu_undo').disabled = (this.lastCommandIndex < 0);
document.getElementById('rbro_menu_redo').disabled = (this.lastCommandIndex >= (this.commandStack.length - 1));
}
updateMenuActionButtons() {
let elementCount = 0;
let previousContainerOffset = { x: 0, y: 0 };
let elementSameContainerOffsetX = true;
let elementSameContainerOffsetY = true;
for (let selectionId of this.selections) {
let obj = this.getDataObject(selectionId);
if (obj instanceof DocElement && obj.hasProperty('x')) {
elementCount++;
let container = obj.getContainer();
let offset = container.getOffset();
if (elementCount === 1) {
previousContainerOffset = offset;
} else {
if (offset.x !== previousContainerOffset.x) {
elementSameContainerOffsetX = false;
}
if (offset.y !== previousContainerOffset.y) {
elementSameContainerOffsetY = false;
}
}
}
}
const menuButtons = document.getElementById('rbo_menu_elements').querySelectorAll('.rbroMenuButton');
if (elementCount > 1) {
// allow alignment of elements if their parent container has the same x/y offset
if (elementSameContainerOffsetX) {
document.getElementById('rbro_menu_align').removeAttribute('style');
} else {
document.getElementById('rbro_menu_align').style.display = 'none';
}
if (elementSameContainerOffsetY) {
document.getElementById('rbro_menu_valign').removeAttribute('style');
} else {
document.getElementById('rbro_menu_valign').style.display = 'none';
}
for (const menuButton of menuButtons) {
menuButton.style.display = 'none';
}
document.getElementById('rbro_menu_column_actions').style.display = 'none';
document.getElementById('rbro_menu_row_actions').style.display = 'none';
} else {
let obj = null;
if (this.selections.length === 1) {
obj = this.getDataObject(this.selections[0]);
}
document.getElementById('rbro_menu_align').style.display = 'none';
document.getElementById('rbro_menu_valign').style.display = 'none';
if (obj instanceof TableTextElement) {
for (const menuButton of menuButtons) {
menuButton.style.display = 'none';
}
const table = obj.getTab