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,168 lines (1,079 loc) • 44.1 kB
JavaScript
import MovePanelItemCmd from '../commands/MovePanelItemCmd';
import AddDeleteDocElementCmd from '../commands/AddDeleteDocElementCmd';
import SetValueCmd from '../commands/SetValueCmd';
import Band from '../container/Band';
import Parameter from '../data/Parameter';
import * as utils from '../utils';
/**
* Base class for all doc elements.
* @class
*/
export default class DocElement {
constructor(name, id, defaultWidth, defaultHeight, rb) {
this.rb = rb;
this.id = id;
this.name = name;
this.panelItem = null;
this.x = '0';
this.y = '0';
this.width = '' + defaultWidth;
this.height = '' + defaultHeight;
this.containerId = null;
// in case of frame or band element, this is the container represented by the element
this.linkedContainerId = null;
this.printIf = '';
this.removeEmptyElement = false;
this.styleId = '';
this.el = null;
this.selected = false;
this.xVal = 0;
this.yVal = 0;
this.widthVal = 0;
this.heightVal = 0;
this.errors = [];
}
setInitialData(initialData) {
for (let key in initialData) {
if (initialData.hasOwnProperty(key) && this.hasOwnProperty(key)) {
this[key] = initialData[key];
}
}
// make sure x, y, width and height are strings (they are stored as numbers when serialized)
this.x = '' + this.x;
this.y = '' + this.y;
this.width = '' + this.width;
this.height = '' + this.height;
this.xVal = utils.convertInputToNumber(this.x);
this.yVal = utils.convertInputToNumber(this.y);
this.widthVal = utils.convertInputToNumber(this.width);
this.heightVal = utils.convertInputToNumber(this.height);
}
/**
* Called after initialization is finished.
*/
setup(openPanelItem) {
let container = this.getContainer();
if (container !== null && this.hasBoundaries()) {
// adapt position if new element is outside container
let containerSize = container.getContentSize();
if (this.xVal + this.widthVal > containerSize.width) {
this.xVal = containerSize.width - this.widthVal;
}
if (this.xVal < 0) {
this.xVal = 0;
}
if (this.yVal + this.heightVal > containerSize.height) {
this.yVal = containerSize.height - this.heightVal;
}
if (this.yVal < 0) {
this.yVal = 0;
}
this.x = '' + this.xVal;
this.y = '' + this.yVal;
}
}
/**
* Register event handler for a container element so it can be dragged and
* allow selection on double click.
*/
registerContainerEventHandlers() {
this.el.addEventListener('dblclick', (event) => {
if (!this.rb.isSelectedObject(this.id)) {
this.rb.selectObject(this.id, true);
event.stopPropagation();
}
});
this.el.addEventListener('mousedown', (event) => {
if (event.shiftKey) {
this.rb.deselectObject(this.id);
event.stopPropagation();
} else {
if (this.rb.isSelectedObject(this.id)) {
this.rb.getDocument().startDrag(
event.pageX, event.pageY, this.id, this.containerId, this.getElementType(),
DocElement.dragType.element);
event.stopPropagation();
} else {
this.rb.deselectAll(true);
}
}
});
this.el.addEventListener('touchstart', (event) => {
if (this.rb.isSelectedObject(this.id)) {
const absPos = utils.getEventAbsPos(event);
this.rb.getDocument().startDrag(
absPos.x, absPos.y, this.id, this.containerId, this.getElementType(), DocElement.dragType.element);
}
event.preventDefault();
});
this.el.addEventListener('touchmove', (event) => {
this.rb.getDocument().processDrag(event);
});
this.el.addEventListener('touchend', (event) => {
this.rb.getDocument().stopDrag();
});
}
/**
* Register event handlers so element can be selected, dragged and resized.
*/
registerEventHandlers() {
this.el.addEventListener('dblclick', (event) => {
this.handleDoubleClick(event);
});
this.el.addEventListener('mousedown', (event) => {
this.handleClick(event, false);
});
this.el.addEventListener('touchstart', (event) => {
if (!this.rb.isSelectedObject(this.id)) {
this.handleClick(event, true);
} else {
const absPos = utils.getEventAbsPos(event);
this.rb.getDocument().startDrag(
absPos.x, absPos.y, this.id, this.containerId, this.getElementType(), DocElement.dragType.element);
event.preventDefault();
}
});
this.el.addEventListener('touchmove', (event) => {
if (this.rb.isSelectedObject(this.id)) {
this.rb.getDocument().processDrag(event);
}
});
this.el.addEventListener('touchend', (event) => {
if (this.rb.isSelectedObject(this.id)) {
this.rb.getDocument().stopDrag();
}
});
}
handleDoubleClick(event) {
this.handleClick(event, true);
}
/**
* Handle mouse click on this element so the element can be selected, dragged and resized.
* @param {MouseEvent} event - browser event object.
* @param {Boolean} ignoreSelectedContainer - if true the element will always be selected in case it
* was not selected before. Otherwise the element will only be selected if it's container is
* not selected (e.g. the frame container when this element is inside a frame).
*/
handleClick(event, ignoreSelectedContainer) {
if (!this.rb.isSelectedObject(this.id)) {
if (ignoreSelectedContainer || !this.isContainerSelected()) {
let allowSelection = true;
if (event.shiftKey) {
// do not allow selecting element if one of its children is already selected
let children = [];
this.appendContainerChildren(children);
for (let child of children) {
if (this.rb.isSelectedObject(child.getId())) {
allowSelection = false;
break;
}
}
}
if (allowSelection) {
this.rb.selectObject(this.id, !event.shiftKey);
}
event.stopPropagation();
}
} else {
if (event.shiftKey) {
this.rb.deselectObject(this.id);
} else if (!ignoreSelectedContainer) {
this.rb.getDocument().startDrag(
event.pageX, event.pageY, this.id, this.containerId, this.getElementType(),
DocElement.dragType.element);
}
event.stopPropagation();
}
}
getId() {
return this.id;
}
/**
* Returns highest id of this component including all its child components.
* @returns {Number}
*/
getMaxId() {
return this.id;
}
getName() {
return this.name;
}
getPanelItem() {
return this.panelItem;
}
setPanelItem(panelItem) {
this.panelItem = panelItem;
}
getContainerId() {
return this.containerId;
}
getContainer() {
return this.rb.getDataObject(this.getContainerId());
}
getLinkedContainer() {
if (this.linkedContainerId !== null) {
return this.rb.getDataObject(this.linkedContainerId);
}
return null;
}
/**
* Return array with linked container(s) of this element.
* @return {Container[]}
*/
getLinkedContainers() {
const linkedContainer = this.getLinkedContainer();
if (linkedContainer) {
return [linkedContainer];
}
return [];
}
getContainerContentSize() {
let container = this.getContainer();
return (container !== null) ? container.getContentSize() : { width: 0, height: 0 };
}
appendToContainer() {
let container = this.getContainer();
if (container !== null) {
container.appendElement(this.el);
}
}
isContainerSelected() {
let container = this.getContainer();
if (container !== null) {
return container.isSelected();
}
return false;
}
appendContainerChildren(elements) {
if (this.linkedContainerId !== null) {
if (this.panelItem !== null) {
let children = this.panelItem.getChildren();
for (let child of children) {
if (child.getData() instanceof DocElement) {
elements.push(child.getData());
child.getData().appendContainerChildren(elements);
}
}
}
}
}
/**
* Returns absolute position inside document.
* @returns {Object} x and y coordinates.
*/
getAbsolutePosition() {
let pos = { x: this.xVal, y: this.yVal };
let container = this.getContainer();
if (container !== null) {
let offset = container.getOffset();
pos.x += offset.x;
pos.y += offset.y;
}
return pos;
}
/**
* Add commands for updated position/size.
*
* This should be called when an element is moved, resized or moved to another container.
* @param {Number} x - x value of doc element.
* @param {Number} y - y value of doc element.
* @param {Number} width - width value of doc element.
* @param {Number} height - height value of doc element.
* @param {Object} containerSize - width and height of container where this doc element belongs to.
* @param {CommandGroupCmd} cmdGroup - possible SetValue commands will be added to this command group.
*/
updatePositionAndSize(x, y, width, height, containerSize, cmdGroup) {
if (this.hasBoundaries()) {
// Check element bounds within container and adapt position/size if necessary
if ((x + width) > containerSize.width) {
x = containerSize.width - width;
}
if (x < 0) {
x = 0;
}
if ((x + width) > containerSize.width) {
width = containerSize.width - x;
}
if ((y + height) > containerSize.height) {
y = containerSize.height - height;
}
if (y < 0) {
y = 0;
}
if ((y + height) > containerSize.height) {
height = containerSize.height - y;
}
}
if (x !== this.xVal && this.hasProperty('x')) {
let cmd = new SetValueCmd(
this.id, 'x', '' + x, SetValueCmd.type.text, this.rb);
cmd.disableSelect();
cmdGroup.addCommand(cmd);
}
if (y !== this.yVal && this.hasProperty('y')) {
let cmd = new SetValueCmd(
this.id, 'y', '' + y, SetValueCmd.type.text, this.rb);
cmd.disableSelect();
cmdGroup.addCommand(cmd);
}
if (width !== this.getDisplayWidth() && this.hasProperty('width')) {
this.addCommandsForChangedWidth(width, true, cmdGroup);
}
if (height !== this.getDisplayHeight() && this.hasProperty('height')) {
let cmd = new SetValueCmd(
this.id, 'height', '' + height, SetValueCmd.type.text, this.rb);
cmd.disableSelect();
cmdGroup.addCommand(cmd);
}
let linkedContainer = this.getLinkedContainer();
if (linkedContainer !== null && linkedContainer.getPanelItem() !== null) {
let linkedContainerSize = { width: width, height: height };
for (let child of linkedContainer.getPanelItem().getChildren()) {
if (child.getData() instanceof DocElement) {
let docElement = child.getData();
docElement.updatePositionAndSize(docElement.getValue('xVal'), docElement.getValue('yVal'),
docElement.getDisplayWidth(), docElement.getDisplayHeight(),
linkedContainerSize, cmdGroup);
}
}
}
}
getValue(field) {
return this[field];
}
setValue(field, value) {
this[field] = value;
if (field === 'x' || field === 'y' || field === 'width' || field === 'height') {
this[field + 'Val'] = utils.convertInputToNumber(value);
this.updateDisplay();
} else if (field === 'containerId') {
if (this.el !== null) {
// detach dom node from container and then attach it to new container
this.el.parentElement.removeChild(this.el);
this.appendToContainer();
}
// set new parent for linked containers
const linkedContainers = this.getLinkedContainers();
if (linkedContainers.length > 0) {
const container = this.getContainer();
for (const linkedContainer of linkedContainers) {
linkedContainer.setParent(container);
}
}
// update level of all containers as the level could have changed in case container
// belongs to this element
for (const container of this.rb.getContainers()) {
container.initLevel();
}
// update display because element is now inside another container, necessary when
// updateDisplay is not called due to other changed field (e.g. element is moved to
// other container but x, y, width, height and so on stay unchanged)
this.updateDisplay();
} else if (['styleId', 'bold', 'italic', 'underline', 'strikethrough',
'horizontalAlignment', 'verticalAlignment',
'textColor', 'backgroundColor', 'font', 'fontSize', 'lineSpacing', 'borderColor', 'borderWidth',
'borderAll', 'borderLeft', 'borderTop', 'borderRight', 'borderBottom',
'paddingLeft', 'paddingTop', 'paddingRight', 'paddingBottom'].indexOf(field) !== -1) {
this.updateStyle();
if (['borderWidth', 'borderAll', 'borderLeft', 'borderTop', 'borderRight', 'borderBottom',
'paddingLeft', 'paddingTop', 'paddingRight', 'paddingBottom'].indexOf(field) !== -1) {
this.updateDisplay();
}
}
}
/**
* Returns value to use for updating input control.
* Can be overridden in case update value can be different from internal value, e.g.
* width for table cells with colspan > 1.
* @param {String} field - field name.
* @param {String} value - value for update.
*/
getUpdateValue(field, value) {
return value;
}
getDisplayWidth() {
return this.widthVal;
}
getDisplayHeight() {
return this.heightVal;
}
/**
* Returns all data fields of this object. The fields are used when serializing the object.
* @returns {String[]}
*/
getFields() {
let fields = this.getProperties();
fields.splice(0, 0, 'id', 'containerId');
return fields;
}
/**
* Returns all fields of this object that can be modified in the properties panel.
* @returns {String[]}
*/
getProperties() {
return [];
}
/**
* Returns true if the given property is available for this object.
* @param {String} property - property name.
* @returns {Boolean}
*/
hasProperty(property) {
return this.getProperties().indexOf(property) !== -1;
}
getElementType() {
return DocElement.type.none;
}
updateDisplay() {
this.updateDisplayInternal(this.xVal, this.yVal, this.widthVal, this.heightVal);
}
updateDisplayInternal(x, y, width, height) {
if (this.el !== null) {
this.el.style.left = this.rb.toPixel(x);
this.el.style.top = this.rb.toPixel(y);
this.el.style.width = this.rb.toPixel(width);
this.el.style.height = this.rb.toPixel(height);
}
}
getStyle() {
let style = this;
if (this.styleId !== '') {
let styleObj = this.rb.getDataObject(this.styleId);
if (styleObj !== null) {
style = styleObj;
}
}
return style;
}
/**
* Adds commands to command group parameter to set style properties of given style.
*
* This should be called when the style was changed so all style properties
* will be updated as well.
*
* @param {Number|String} styleId - id of new style or empty string if no style was selected.
* @param {String} fieldPrefix - field prefix when accessing properties.
* @param {Object[]} propertyDescriptors - list of all property descriptors to get
* property type for SetValueCmd.
* @param {CommandGroupCmd} cmdGroup - commands will be added to this command group.
*/
addCommandsForChangedStyle(styleId, fieldPrefix, propertyDescriptors, cmdGroup) {
if (styleId) {
const style = this.rb.getStyleById(styleId);
if (style !== null) {
const docElementProperties = this.getProperties();
const styleProperties = style.getStyleProperties();
for (let styleProperty of styleProperties) {
// test if style property is part of doc element properties (style contains properties for
// all different doc element types)
if (docElementProperties.indexOf(styleProperty) !== -1) {
const objField = fieldPrefix + styleProperty;
const value = style.getValue(styleProperty);
if (value !== this.getValue(objField)) {
const propertyDescriptor = propertyDescriptors[objField];
const cmd = new SetValueCmd(
this.getId(), objField, value, propertyDescriptor['type'], this.rb);
cmd.disableSelect();
cmdGroup.addCommand(cmd);
}
}
}
}
}
cmdGroup.addCommand(new SetValueCmd(
this.getId(), fieldPrefix + 'styleId', styleId, SetValueCmd.type.select, this.rb));
}
updateStyle() {
}
updateChangedStyle(styleId) {
if (utils.convertInputToNumber(this.styleId) === styleId) {
this.updateStyle();
}
}
getDragDiff(diffX, diffY, dragType, gridSize) {
let rv = { x: 0, y: 0 };
let dragX, dragY;
let posX1 = this.xVal;
let posY1 = this.yVal;
let posX2 = posX1 + this.getDisplayWidth();
let posY2 = posY1 + this.getDisplayHeight();
let minWidth = this.getMinWidth();
let maxWidth = this.getMaxWidth();
let minHeight = this.getMinHeight();
if (dragType === DocElement.dragType.element) {
dragX = posX1 + diffX;
if (gridSize !== 0) {
dragX = utils.roundValueToInterval(dragX, gridSize);
}
dragY = posY1 + diffY;
if (gridSize !== 0) {
dragY = utils.roundValueToInterval(dragY, gridSize);
}
rv.x = dragX - posX1;
rv.y = dragY - posY1;
} else {
let containerSize = this.getContainerContentSize();
if (dragType === DocElement.dragType.sizerNW || dragType === DocElement.dragType.sizerN ||
dragType === DocElement.dragType.sizerNE) {
dragY = posY1 + diffY;
if (gridSize !== 0) {
dragY = utils.roundValueToInterval(dragY, gridSize);
}
if (dragY > posY2 - minHeight) {
if (gridSize !== 0) {
dragY = utils.roundValueToLowerInterval(posY2 - minHeight, gridSize);
} else {
dragY = posY2 - minHeight;
}
} else if (dragY < 0) {
dragY = 0;
}
rv.y = dragY - posY1;
}
if (dragType === DocElement.dragType.sizerNE || dragType === DocElement.dragType.sizerE ||
dragType === DocElement.dragType.sizerSE) {
dragX = posX2 + diffX;
if (gridSize !== 0) {
dragX = utils.roundValueToInterval(dragX, gridSize);
}
if (dragX < posX1 + minWidth) {
if (gridSize !== 0) {
dragX = utils.roundValueToUpperInterval(posX1 + minWidth, gridSize);
} else {
dragX = posX1 + minWidth;
}
} else if (dragX > maxWidth) {
dragX = maxWidth;
}
rv.x = dragX - posX2;
}
if (dragType === DocElement.dragType.sizerSE || dragType === DocElement.dragType.sizerS ||
dragType === DocElement.dragType.sizerSW) {
dragY = posY2 + diffY;
if (gridSize !== 0) {
dragY = utils.roundValueToInterval(dragY, gridSize);
}
if (dragY < posY1 + minHeight) {
if (gridSize !== 0) {
dragY = utils.roundValueToUpperInterval(posY1 + minHeight, gridSize);
} else {
dragY = posY1 + minHeight;
}
} else if (dragY > containerSize.height) {
dragY = containerSize.height;
}
rv.y = dragY - posY2;
}
if (dragType === DocElement.dragType.sizerSW || dragType === DocElement.dragType.sizerW ||
dragType === DocElement.dragType.sizerNW) {
dragX = posX1 + diffX;
if (gridSize !== 0) {
dragX = utils.roundValueToInterval(dragX, gridSize);
}
if (dragX > posX2 - minWidth) {
if (gridSize !== 0) {
dragX = utils.roundValueToLowerInterval(posX2 - minWidth, gridSize);
} else {
dragX = posX2 - minWidth;
}
} else if (dragX < 0) {
dragX = 0;
}
rv.x = dragX - posX1;
}
}
return rv;
}
updateDrag(diffX, diffY, dragType, dragContainer, cmdGroup) {
let posX1 = this.xVal;
let posY1 = this.yVal;
let posX2 = posX1 + this.getDisplayWidth();
let posY2 = posY1 + this.getDisplayHeight();
let maxWidth = this.getMaxWidth();
let containerSize = this.getContainerContentSize();
if (dragType === DocElement.dragType.element) {
posX1 += diffX;
posX2 = posX1 + this.getDisplayWidth();
posY1 += diffY;
posY2 = posY1 + this.getDisplayHeight();
} else {
if (dragType === DocElement.dragType.sizerNW || dragType === DocElement.dragType.sizerN ||
dragType === DocElement.dragType.sizerNE) {
posY1 += diffY;
}
if (dragType === DocElement.dragType.sizerNE || dragType === DocElement.dragType.sizerE ||
dragType === DocElement.dragType.sizerSE) {
posX2 += diffX;
}
if (dragType === DocElement.dragType.sizerSE || dragType === DocElement.dragType.sizerS ||
dragType === DocElement.dragType.sizerSW) {
posY2 += diffY;
}
if (dragType === DocElement.dragType.sizerSW || dragType === DocElement.dragType.sizerW ||
dragType === DocElement.dragType.sizerNW) {
posX1 += diffX;
}
if (posX1 < 0) {
posX1 = 0;
}
if (posX2 < posX1) {
posX2 = posX1;
}
if (posY1 < 0) {
posY1 = 0;
}
if (posY2 < posY1) {
posY2 = posY1;
}
if (posX2 > maxWidth) {
posX2 = maxWidth;
}
if (posY2 > containerSize.height) {
posY2 = containerSize.height;
}
}
let width = posX2 - posX1;
let height = posY2 - posY1;
if (cmdGroup !== null) {
let containerChanged = false;
let container = this.getContainer();
let containerSize = { width: 0, height: 0};
if (dragContainer !== null && dragContainer.getId() !== this.getContainerId()) {
containerChanged = true;
containerSize = dragContainer.getContentSize();
if (container !== null) {
let relativeOffset = dragContainer.getOffsetTo(container);
posX1 -= relativeOffset.x;
posY1 -= relativeOffset.y;
}
} else {
containerSize = container.getContentSize();
}
if (!containerChanged || dragContainer.isElementAllowed(this.getElementType())) {
const cmdCountBefore = cmdGroup.getCommands().length;
this.updatePositionAndSize(posX1, posY1, width, height, containerSize, cmdGroup);
if (containerChanged) {
let cmd = new SetValueCmd(
this.id, 'containerId', dragContainer.getId(), SetValueCmd.type.internal, this.rb);
cmdGroup.addCommand(cmd);
cmd = new MovePanelItemCmd(this.getPanelItem(), dragContainer.getPanelItem(),
dragContainer.getPanelItem().getChildren().length, this.rb);
cmdGroup.addCommand(cmd);
}
// compare command count to check if something was changed (there could be commands
// for other elements in the command group in case multiple elements are selected and modified)
if (cmdGroup.getCommands().length === cmdCountBefore) {
// nothing was changed, make sure displayed element is updated to saved position/size after drag
this.updateDisplay();
}
} else {
this.updateDisplay();
}
} else {
this.updateDisplayInternal(posX1, posY1, width, height);
}
}
select() {
if (this.el !== null) {
let elSizerContainer = this.getSizerContainerElement();
let sizers = this.getSizers();
for (let sizer of sizers) {
let sizerVal = sizer;
let elSizer = utils.createElement('div', { class: `rbroSizer rbroSizer${sizer}` });
elSizer.addEventListener('mousedown', (event) => {
this.rb.getDocument().startDrag(
event.pageX, event.pageY, this.id, this.containerId, this.getElementType(),
DocElement.dragType['sizer' + sizerVal]);
event.stopPropagation();
});
elSizer.addEventListener('touchstart', (event) => {
if (this.rb.isSelectedObject(this.id)) {
const absPos = utils.getEventAbsPos(event);
this.rb.getDocument().startDrag(
absPos.x, absPos.y, this.id, this.containerId, this.getElementType(),
DocElement.dragType['sizer' + sizerVal]);
}
event.preventDefault();
event.stopPropagation();
});
elSizer.addEventListener('touchmove', (event) => {
this.rb.getDocument().processDrag(event);
});
elSizer.addEventListener('touchend', (event) => {
this.rb.getDocument().stopDrag();
});
elSizerContainer.append(elSizer);
}
this.el.classList.add('rbroSelected');
this.el.style.zIndex = '10';
}
this.selected = true;
}
deselect() {
if (this.el !== null) {
let elSizerContainer = this.getSizerContainerElement();
const elSizers = elSizerContainer.querySelectorAll('.rbroSizer');
for (const elSizer of elSizers) {
elSizer.remove();
}
this.el.style.zIndex = '';
this.el.classList.remove('rbroSelected');
}
this.selected = false;
}
/**
* Returns allowed sizers when element is selected.
* @returns {String[]}
*/
getSizers() {
return ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
}
hasBorderSettings() {
return false;
}
/**
* Returns true if element is restricted within container boundaries.
* @returns {boolean}
*/
hasBoundaries() {
return true;
}
/**
* Returns true if the element can be selected when it is inside a
* selection area (rectangle specified with pressed mouse button).
*/
isAreaSelectionAllowed() {
return true;
}
isDraggingAllowed() {
return true;
}
/**
* Returns true if another element can be dropped into this element (or its corresponding panel item).
*/
isDroppingAllowed() {
return true;
}
/**
* Returns minimum allowed width of element.
* @returns {Number}.
*/
getMinWidth() {
return 20;
}
/**
* Returns maximum allowed width of element.
* This is needed when the element is resized by dragging so the resized element does not overflow its container.
* @returns {Number}.
*/
getMaxWidth() {
let containerSize = this.getContainerContentSize();
return containerSize.width;
}
/**
* Returns minimum allowed height of element.
* @returns {Number}.
*/
getMinHeight() {
return 20;
}
createElement() {
}
getElement() {
return this.el;
}
getSizerContainerElement() {
return this.el;
}
/**
* Returns dom node where elements will be added if they are inside this element.
* Is null in case this element is not a container element like a frame or a band.
* @returns {?Object} dom node
*/
getContentElement() {
return null;
}
/**
* Returns true if this element has a data source setting.
* This does not necessarily mean that a data source is available.
* Must be overridden when the element has a data source.
* @return {boolean}
*/
hasDataSource() {
return false;
}
/**
* Returns the data source parameter name.
* @returns {?String} contains the data source parameter name.
* Is null in case element does not have a data source or the data source does not contain
* a parameter reference.
*/
getDataSourceParameterName() {
if (this.hasDataSource()) {
const dataSource = this.getValue('dataSource').trim();
if (dataSource.length >= 3 && dataSource.substring(0, 2) === '${' &&
dataSource.charAt(dataSource.length - 1) === '}') {
return dataSource.substring(2, dataSource.length - 1);
}
}
return null;
}
/**
* Returns all data sources of this element and any possible parent elements.
* @returns {Object[]} array with all data sources where each item contains the name
* of the data source parameter and all data source parameters.
*/
getAllDataSources() {
const dataSources = [];
const dataSourceNames = [];
this.getAllDataSourceParameterNames(dataSourceNames, null);
// iterate data sources in reverse order -> start from root, the last data source will
// be from this element. this way we can find data sources which are parameters
// of a parent data source
for (let i = dataSourceNames.length - 1; i >= 0; i--) {
const dataSourceName = dataSourceNames[i];
let param = null;
// test if this data source is a parameter of a parent data source
for (const parentDataSource of dataSources) {
for (const dataSourceParameter of parentDataSource.parameters) {
if (dataSourceParameter.getName() === dataSourceName) {
param = dataSourceParameter;
break;
}
}
}
if (param === null) {
// root data source
param = this.rb.getParameterByName(dataSourceName);
}
if (param !== null && param.getValue('type') === Parameter.type.array) {
dataSources.unshift({ name: dataSourceName, parameters: param.getChildren() });
}
}
return dataSources;
}
/**
* Returns all data source parameter names of this element and any possible parent elements.
* @param {String[]} dataSourceParameterNames - array where the data source names will be appended to.
* @param {?DocElement} child - optional child element where the method was called from.
*/
getAllDataSourceParameterNames(dataSourceParameterNames, child) {
if (this.getElementType() === DocElement.type.table || this.getElementType() === DocElement.type.section) {
if (child && child.getValue('bandType') === Band.bandType.content) {
const dataSourceParameterName = this.getDataSourceParameterName();
if (dataSourceParameterName !== null) {
dataSourceParameterNames.push(dataSourceParameterName);
}
}
}
let panelItem = this.getPanelItem();
if (panelItem !== null) {
let parentPanelItem = panelItem.getParent();
if (parentPanelItem !== null && parentPanelItem.getData() instanceof DocElement) {
parentPanelItem.getData().getAllDataSourceParameterNames(dataSourceParameterNames, this);
}
}
}
/**
* Adds SetValue commands to command group parameter in case the specified parameter is used in any of
* the object fields.
* @param {Parameter} parameter - parameter which will be renamed.
* @param {String} newParameterName - new name of the parameter.
* @param {CommandGroupCmd} cmdGroup - possible SetValue commands will be added to this command group.
*/
addCommandsForChangedParameterName(parameter, newParameterName, cmdGroup) {
}
/**
* Adds SetValue command to command group parameter in case the specified parameter is used in the
* specified object field.
* @param {Parameter} parameter - parameter which will be renamed.
* @param {String} newParameterName - new name of the parameter.
* @param {String} field
* @param {CommandGroupCmd} cmdGroup - possible SetValue command will be added to this command group.
*/
addCommandForChangedParameterName(parameter, newParameterName, field, cmdGroup) {
let paramParent = parameter.getParent();
const dataSources = this.getAllDataSources();
let parameterBelongsToArray = false;
let parameterPrefix = '';
let arrayName = null, mapName = null;
while (paramParent !== null) {
if (paramParent.getValue('type') === Parameter.type.map) {
parameterPrefix = paramParent.getName() + '.' + parameterPrefix;
mapName = paramParent.getName();
paramParent = paramParent.getParent();
} else if (paramParent.getValue('type') === Parameter.type.array) {
parameterBelongsToArray = true;
arrayName = paramParent.getName();
break;
} else {
// not possible (nested parameter can only belong to map or array)
return;
}
}
let oldParameterText = '${' + parameterPrefix + parameter.getName();
let newParameterText = '${' + parameterPrefix + newParameterName;
// add suffix (either "." for a map parameter or closing bracket "}" for parameter reference) to avoid
// unintended renaming other parameter where the name starts the same (e.g. "client" and "clientName")
if (parameter.getValue('type') === Parameter.type.map) {
oldParameterText += '.';
newParameterText += '.';
} else {
oldParameterText += '}';
newParameterText += '}';
}
let parameterNameExistsInCurrentScope = false;
if (dataSources.length > 0) {
let parameterName = mapName ? mapName : parameter.getName();
for (const dataSourceParam of dataSources[0].parameters) {
if (dataSourceParam.getName() === parameterName) {
parameterNameExistsInCurrentScope = true;
break;
}
}
}
if (parameterBelongsToArray) {
let scopeLevel = -1;
for (let i = 0; i < dataSources.length; i++) {
const dataSource = dataSources[i];
if (dataSource.name === arrayName) {
scopeLevel = i;
break;
}
}
// scopeLevel >= 0: there must be at least one data source for this doc element
// because the parameter belongs to an array
if (scopeLevel === 0 && parameterNameExistsInCurrentScope && dataSources[0].name === arrayName) {
// if the parameter occurs in the current scope the parent array of
// the parameter must match the data source because the parameter is
// referenced directly (i.e. without specifying the data source)
this.addCommandForChangedText(oldParameterText, newParameterText, field, cmdGroup);
} else if (scopeLevel > 0) {
// specify data source name when referencing parameter from outer scope
oldParameterText = '${' + arrayName + ':' + oldParameterText.substring(2);
newParameterText = '${' + arrayName + ':' + newParameterText.substring(2);
this.addCommandForChangedText(oldParameterText, newParameterText, field, cmdGroup);
}
} else {
// avoid unintentionally changing name of other parameter in case the name exists in current scope
if (!parameterNameExistsInCurrentScope) {
if (dataSources.length > 0) {
// element has a data source, therefor root parameters are specified with a ':' prefix,
// so they can be explicitly referenced
oldParameterText = '${:' + oldParameterText.substring(2);
newParameterText = '${:' + newParameterText.substring(2);
}
this.addCommandForChangedText(oldParameterText, newParameterText, field, cmdGroup);
}
}
}
/**
* Adds SetValue command to command group parameter in case the given oldText occurs in the
* specified object field and replace it with newText.
* @param {String} oldText - old text in field content which will be replaced.
* @param {String} newText - new text which will be used as replacement for oldText.
* @param {String} field
* @param {CommandGroupCmd} cmdGroup - possible SetValue command will be added to this command group.
*/
addCommandForChangedText(oldText, newText, field, cmdGroup) {
let value = this.getValue(field);
let valueType = SetValueCmd.type.text;
if (typeof value === 'object') {
// for rich text we have to convert the rich text content to a string to replace all
// parameter occurrences and afterwards convert it back to a JS object
value = JSON.stringify(value);
valueType = SetValueCmd.type.richText;
}
if (value.indexOf(oldText) !== -1) {
let updatedValue = value.replaceAll(oldText, newText);
if (valueType === SetValueCmd.type.richText) {
updatedValue = JSON.parse(updatedValue);
}
let cmd = new SetValueCmd(this.id, field, updatedValue, valueType, this.rb);
cmdGroup.addCommand(cmd);
}
}
/**
* Adds AddDeleteDocElementCmd commands to command group parameter to delete this element and
* any possible existing children.
* @param {CommandGroupCmd} cmdGroup - AddDeleteDocElementCmd commands will be added to this command group.
*/
addCommandsForDelete(cmdGroup) {
let elements = [];
this.appendContainerChildren(elements);
elements.push(this);
for (let element of elements) {
let cmd = new AddDeleteDocElementCmd(
false, element.getPanelItem().getPanelName(),
element.toJS(), element.getId(), element.getContainerId(),
element.getPanelItem().getSiblingPosition(), this.rb);
cmdGroup.addCommand(cmd);
}
}
addCommandsForChangedWidth(newWidth, disableSelect, cmdGroup) {
let cmd = new SetValueCmd(
this.id, 'width', '' + newWidth, SetValueCmd.type.text, this.rb);
if (disableSelect) {
cmd.disableSelect();
}
cmdGroup.addCommand(cmd);
}
addChildren(docElements) {
}
addError(error) {
this.errors.push(error);
}
clearErrors() {
this.errors = [];
}
getErrors() {
return this.errors;
}
remove() {
if (this.el !== null) {
this.el.remove();
this.el = null;
}
if (this.panelItem !== null) {
this.panelItem.getParent().removeChild(this.panelItem);
this.panelItem = null;
}
}
toJS() {
const rv = { elementType: this.getElementType() };
for (const field of this.getFields()) {
if (['x', 'y', 'width', 'height'].indexOf(field) === -1) {
rv[field] = this.getValue(field);
} else {
rv[field] = this.getValue(field + 'Val');
}
}
return rv;
}
}
DocElement.type = {
none: 'none',
text: 'text',
image: 'image',
line: 'line',
table: 'table',
pageBreak: 'page_break',
tableText: 'table_text',
barCode: 'bar_code',
frame: 'frame',
section: 'section',
watermarkText: 'watermark_text',
watermarkImage: 'watermark_image',
};
DocElement.dragType = {
none: -1,
element: 0,
sizerN: 1,
sizerNE: 2,
sizerE: 3,
sizerSE: 4,
sizerS: 5,
sizerSW: 6,
sizerW: 7,
sizerNW: 8
};