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.
484 lines (445 loc) • 21.2 kB
JavaScript
import Command from '../commands/Command';
import SetValueCmd from '../commands/SetValueCmd';
import * as utils from '../utils';
import Delta from 'quill-delta';
/**
* Base class for all panels. Contains shared functionality.
* @class
*/
export default class PanelBase {
constructor(idPrefix, dataBaseClass, rootElement, rb) {
this.idPrefix = idPrefix;
this.dataBaseClass = dataBaseClass;
this.panelId = idPrefix + '_panel';
this.rootElement = rootElement;
this.rb = rb;
this.propertyDescriptors = {}; // is overriden in derived class
this.differentValuesLabel = this.rb.getLabel('differentValues');
this.differentFilesLabel = this.rb.getLabel('differentFiles');
this.quill = null;
this.controls = {};
// fields which are referenced in the visibleIf property
this.visibleIfFields = [];
}
/**
* Collect all fields which are referenced in the visibleIf property.
*/
initVisibleIfFields() {
for (const property in this.propertyDescriptors) {
if (this.propertyDescriptors.hasOwnProperty(property)) {
const propertyDescriptor = this.propertyDescriptors[property];
if ('visibleIf' in propertyDescriptor) {
// add all fields used in visibleIf expression to visibileIfFields list of property descriptor
const tokens = utils.tokenize(propertyDescriptor['visibleIf'], null);
for (const token of tokens) {
if (token.type === 'field') {
if (!('visibleIfFields' in propertyDescriptor)) {
propertyDescriptor.visibleIfFields = [token.value];
} else if (!propertyDescriptor.visibleIfFields.includes(token.value)) {
propertyDescriptor.visibleIfFields.push(token.value);
}
if (!this.visibleIfFields.includes(token.value)) {
this.visibleIfFields.push(token.value);
}
}
}
}
}
}
}
render(data) {
}
/**
* Is called when the ReportBro instance is deleted and should be used
* to cleanup elements and event handlers.
*/
destroy() {
}
setValue(propertyDescriptor, value, differentValues) {
let propertyId = `${this.idPrefix}_${propertyDescriptor['fieldId']}`;
if (differentValues) {
document.getElementById(propertyId).classList.add('rbroDifferentValues');
} else {
document.getElementById(propertyId).classList.remove('rbroDifferentValues');
}
// set value for current property
const el = document.getElementById(propertyId);
if (propertyDescriptor['type'] === SetValueCmd.type.text) {
if (differentValues) {
el.value = '';
el.setAttribute('placeholder', this.differentValuesLabel);
} else {
el.value = value;
el.setAttribute('placeholder', '');
}
} else if (propertyDescriptor['type'] === SetValueCmd.type.richText) {
if (this.quill) {
if (differentValues || !value) {
this.quill.setContents({}, 'silent');
} else {
this.quill.setContents(value, 'silent');
}
}
} else if (propertyDescriptor['type'] === SetValueCmd.type.select) {
el.value = value;
} else if (propertyDescriptor['type'] === SetValueCmd.type.checkbox) {
if (differentValues) {
el.checked = false;
} else {
el.checked = value;
}
} else if (propertyDescriptor['type'] === SetValueCmd.type.button) {
if (differentValues) {
el.classList.remove('rbroButtonActive');
} else {
if (value) {
el.classList.add('rbroButtonActive');
} else {
el.classList.remove('rbroButtonActive');
}
}
} else if (propertyDescriptor['type'] === SetValueCmd.type.buttonGroup) {
const elButtons = el.querySelectorAll('button');
for (const elButton of elButtons) {
elButton.classList.remove('rbroButtonActive');
}
if (!differentValues) {
el.querySelector(`button[value="${value}"]`).classList.add('rbroButtonActive');
}
} else if (propertyDescriptor['type'] === SetValueCmd.type.color) {
const elColorSelect = document.getElementById(propertyId + '_select');
if (differentValues) {
if (propertyDescriptor['allowEmpty']) {
el.value = '';
elColorSelect.style.color = '';
elColorSelect.classList.add('rbroTransparentColorSelect');
} else {
el.value = '#000000';
elColorSelect.style.color = '#000000';
elColorSelect.classList.remove('rbroTransparentColorSelect');
}
} else {
el.value = value;
elColorSelect.style.color = value;
if (value) {
elColorSelect.classList.remove('rbroTransparentColorSelect');
} else {
elColorSelect.classList.add('rbroTransparentColorSelect');
}
}
} else if (propertyDescriptor['type'] === SetValueCmd.type.filename) {
if (differentValues) {
el.textContent = this.differentFilesLabel;
document.getElementById(propertyId + '_container').classList.remove('rbroHidden');
} else {
el.textContent = value;
if (value === '') {
document.getElementById(propertyId + '_container').classList.add('rbroHidden');
} else {
document.getElementById(propertyId + '_container').classList.remove('rbroHidden');
}
}
}
}
/**
* Is called when the selection is changed or the selected element was changed.
* The panel is updated to show the values of the selected data objects.
* @param {String} [field] - affected field in case of change operation.
*/
updateDisplay(field) {
let selectedObjects = this.rb.getSelectedObjects();
let sectionPropertyCount = {};
let sharedProperties = {};
for (let obj of selectedObjects) {
let properties = obj.getProperties();
for (let property of properties) {
if (property in sharedProperties) {
sharedProperties[property] += 1;
} else {
sharedProperties[property] = 1;
}
}
}
// show/hide property depending on if it is available in all selected objects
for (let property in this.propertyDescriptors) {
if (this.propertyDescriptors.hasOwnProperty(property)) {
const propertyDescriptor = this.propertyDescriptors[property];
let visibleIf = '';
if ('visibleIf' in propertyDescriptor) {
visibleIf = propertyDescriptor['visibleIf'];
}
if (field === null || property === field ||
(visibleIf && propertyDescriptor.visibleIfFields.includes(field))) {
let show = false;
if (property in sharedProperties) {
if (sharedProperties[property] === selectedObjects.length) {
let value = null;
let differentValues = false;
for (let obj of selectedObjects) {
let objValue = obj.getUpdateValue(property, obj.getValue(property));
if (value === null) {
value = objValue;
} else if (propertyDescriptor['type'] === SetValueCmd.type.richText) {
if (objValue && value) {
let diff = new Delta(objValue).diff(new Delta(value));
if (diff.ops.length > 0) {
differentValues = true;
break;
}
}
} else if (objValue !== value) {
differentValues = true;
break;
}
}
if (differentValues && propertyDescriptor['type'] === SetValueCmd.type.select &&
propertyDescriptor['allowEmpty']) {
// if values are different and dropdown has empty option then select
// empty dropdown option
value = '';
}
this.setValue(propertyDescriptor, value, differentValues);
if ('section' in propertyDescriptor) {
let sectionName = propertyDescriptor['section'];
if (sectionName in sectionPropertyCount) {
sectionPropertyCount[sectionName] += 1;
} else {
sectionPropertyCount[sectionName] = 1;
}
}
show = true;
} else {
delete sharedProperties[property];
}
}
if (show && visibleIf) {
for (let obj of selectedObjects) {
if (!utils.evaluateExpression(visibleIf, obj)) {
show = false;
delete sharedProperties[property];
break;
}
}
}
if ('singleRowProperty' in propertyDescriptor && !propertyDescriptor['singleRowProperty']) {
// only handle visibility of control and not of whole row.
// row visibility will be handled below, e.g. for button groups
const propertyId = `${this.idPrefix}_${propertyDescriptor['fieldId']}`;
if (show) {
document.getElementById(propertyId).classList.remove('rbroHidden');
} else {
document.getElementById(propertyId).classList.add('rbroHidden');
}
} else {
const rowId = this.getRowId(propertyDescriptor);
if (show) {
document.getElementById(rowId).classList.remove('rbroHidden');
} else {
document.getElementById(rowId).classList.add('rbroHidden');
}
}
}
}
}
if (field === null || this.visibleIfFields.includes(field)) {
// only update labels, visible rows and sections if selection was changed (no specific field update)
// or field is referenced in visibleIf property (and therefor could have
// influence on visibility of other fields)
// sharedProperties now only contains properties shared by all objects
for (let property in this.propertyDescriptors) {
if (this.propertyDescriptors.hasOwnProperty(property)) {
let propertyDescriptor = this.propertyDescriptors[property];
if ('rowId' in propertyDescriptor && 'rowProperties' in propertyDescriptor) {
let shownPropertyCount = 0;
for (let rowProperty of propertyDescriptor['rowProperties']) {
if (rowProperty in sharedProperties) {
shownPropertyCount++;
}
}
if ('labelId' in propertyDescriptor) {
let label = propertyDescriptor['defaultLabel'];
if (shownPropertyCount === 1) {
// get label of single property shown in this property group, e.g. label
// is changed to "Width" instead of "Size (Width, Height)" if only width property
// is shown and not both width and height.
for (let rowProperty of propertyDescriptor['rowProperties']) {
if (rowProperty in sharedProperties) {
label = this.propertyDescriptors[rowProperty]['singlePropertyLabel'];
break;
}
}
}
document.getElementById(propertyDescriptor['labelId']).textContent =
this.rb.getLabel(label) + ':';
}
if (shownPropertyCount > 0) {
document.getElementById(propertyDescriptor['rowId']).classList.remove('rbroHidden');
} else {
document.getElementById(propertyDescriptor['rowId']).classList.add('rbroHidden');
}
}
}
}
}
// only update section visibility when selection was changed, otherwise section could be hidden when
// no section property was updated (i.e. no section property changed or affected by changed field)
if (field === null) {
// show section if there is at least one property shown in section
for (const section of this.getSections()) {
const sectionId = `${this.idPrefix}_${section}_section_container`;
if (section in sectionPropertyCount) {
document.getElementById(sectionId).classList.remove('rbroHidden');
} else {
document.getElementById(sectionId).classList.add('rbroHidden');
}
}
}
this.updateAutosizeInputs(field);
}
/**
* Update size of all autosize textareas in panel.
* @param {?String} field - affected field in case of change operation.
*/
updateAutosizeInputs(field) {
}
show() {
document.getElementById(this.panelId).classList.remove('rbroHidden');
}
hide() {
document.getElementById(this.panelId).classList.add('rbroHidden');
}
/**
* Returns true if global key events are disabled.
*
* If rich text editor has focus we do not allow any global key events,
* otherwise the text element would be moved or deleted when special keys like
* cursor or backspace/del are pressed.
* @returns {Boolean}
*/
isKeyEventDisabled() {
return this.quill && this.quill.hasFocus();
}
/**
* 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) {
if (obj instanceof this.dataBaseClass && this.rb.isSelectedObject(obj.id) &&
operation === Command.operation.change) {
this.updateDisplay(field);
}
}
/**
* Updates displayed errors of currently selected data objects.
*/
updateErrors() {
const elPanel = document.getElementById(this.panelId);
const elFormRows = elPanel.querySelectorAll('.rbroFormRow');
const elErrorMessages = elPanel.querySelectorAll('.rbroErrorMessage');
for (const elFormRow of elFormRows) {
elFormRow.classList.remove('rbroError');
}
for (const elErrorMessage of elErrorMessages) {
elErrorMessage.textContent = '';
}
let obj = this.rb.getSelectedObject();
if (obj !== null) {
for (let error of obj.getErrors()) {
let propertyDescriptor = this.propertyDescriptors[error.field];
if (propertyDescriptor) {
if ('section' in propertyDescriptor) {
let sectionName = propertyDescriptor['section'];
document.getElementById(`${this.idPrefix}_${sectionName}_header`).classList.add('rbroError');
}
let errorMsg = this.rb.getLabel(error.msg_key);
if (error.info) {
errorMsg = errorMsg.replaceAll('${info}', '<span class="rbroErrorMessageInfo">' +
error.info.replaceAll('<', '<').replaceAll('>', '>') + '</span>');
}
// highlight row containing error
let rowId = this.getRowId(propertyDescriptor);
document.getElementById(rowId).classList.add('rbroError');
// show error message
let errorMsgId;
if ('errorMsgId' in propertyDescriptor) {
errorMsgId = propertyDescriptor['errorMsgId'];
} else {
errorMsgId = `${this.idPrefix}_${propertyDescriptor['fieldId']}_error`;
}
document.getElementById(errorMsgId).innerHTML = errorMsg;
}
}
}
}
/**
* Is called when the selected element was changed.
*/
selectionChanged() {
this.updateDisplay(null);
this.updateErrors();
}
/**
* Return available sections in the property panel.
* @returns {[String]}
*/
getSections() {
return [];
}
/**
* Expands all sections in case there is an error for a field inside a section and
* scrolls to the uppermost error.
*/
scrollToFirstError() {
let obj = this.rb.getSelectedObject();
if (obj !== null) {
// open all sections containing errors
for (let error of obj.getErrors()) {
let propertyDescriptor = this.propertyDescriptors[error.field];
if (propertyDescriptor) {
if ('section' in propertyDescriptor) {
let sectionName = propertyDescriptor['section'];
const elHeader = document.getElementById(`${this.idPrefix}_${sectionName}_header`);
if (!elHeader.classList.contains('rbroPanelSectionHeaderOpen')) {
elHeader.dispatchEvent(new Event('click'));
}
}
}
}
// scroll to first visible error
let firstErrorRowId = '';
let firstErrorRowOffset = -1;
for (let error of obj.getErrors()) {
let propertyDescriptor = this.propertyDescriptors[error.field];
if (propertyDescriptor) {
let rowId = this.getRowId(propertyDescriptor);
let elRow = document.getElementById(rowId);
let rowOffset = utils.getElementOffset(elRow).top;
if (firstErrorRowId === '' || rowOffset < firstErrorRowOffset) {
firstErrorRowId = rowId;
firstErrorRowOffset = rowOffset;
}
}
}
if (firstErrorRowId !== '') {
const elErrorRow = document.getElementById(firstErrorRowId);
elErrorRow.scrollIntoView({ behavior: 'smooth' });
}
}
}
/**
* Returns row id of html element for given property.
* @param {String} propertyDescriptor - object containing all information about a property.
* @returns {String}
*/
getRowId(propertyDescriptor) {
let rowId;
if ('rowId' in propertyDescriptor) {
rowId = propertyDescriptor['rowId'];
} else {
rowId = `${this.idPrefix}_${propertyDescriptor['fieldId']}_row`;
}
return rowId;
}
}