fluentreports
Version:
A simple, Fluent API for creating PDF Reports
1,297 lines (1,132 loc) • 390 kB
JavaScript
/**************************************************************************************
* (c) 2019-2022, Master Technology
* Licensed under the MIT license or contact me for a support, changes, enhancements.
*
* Any questions please feel free to put an issue up on GitHub
*
* Nathan@master-technology.com
*************************************************************************************/
"use strict";
/* global PlainDraggable, ShadowRoot */
// Notes:
// plainDraggable .top / .left calculations use the parent containers.getBoundingClientRect() + the objects.getBoundingClientRect()
// This counter makes sure we generate unique elements against every instance, every object -- this needs to stay static
let _frItemUUID = 10000; // jshint ignore:line
// Used to track if dialogs are opened, so that global key handler won't interfere
let _frDialogCounter = 0;
const _frScale = 1.5;
class FluentReportsGenerator {
/**
* The Constructor
* @param options
*/
constructor(options) {
this._UIBuilderClass = null;
this._UIBuilder = UI;
// Tracking Information
this._onfocus = null;
this._formatterFunctions = {};
this._parentElement = null;
this._includeCSS = options.css !== false;
this._includeJS = options.js !== false;
this._builtUI = false;
this._previewButton = null;
this._reportData = {header: [], footer: [], detail: [], variables: {}};
this._reportScroller = null;
this._reportLayout = null;
this._toolBarLayout = null;
this._sectionConstrainer = null;
this._propertiesLayout = null;
this._currentSelected = [];
this._inDragDrop = false;
this._sectionIn = 0;
this._debugging = false;
this._scale = _frScale;
this._version = 2;
this._multiSelectKey = "ctrlKey";
/**
* This tracks all the Elements on the Screen
* @type {*[]}
* @private
*/
this._frElements = [];
/**
* This tracks all the visible sections on the screen, this has NOTHING to do with actual report sections.
* @type {*[]}
* @private
*/
this._frSections = [];
this._subReportCounter = 0;
this._groupCounter = {counter: 0};
this._registeredFonts = [];
this._includeData = false;
this._data = [];
this._parsedData = new frReportData([], null, "primary");
// Internal Data for UI
this._calculations = [];
this._totals = {};
this._functions = [];
this._groupBys = [];
this._saveFunction = (value, done) => {
done();
};
this._previewFunction = null;
this._uuid = _frItemUUID++;
this._gridSnapping = {snapping: false, size: 10};
this._saveTemporaryData = null;
// Report Properties
this._paperSize = "letter";
this._paperOrientation = "portrait";
this._paperDims = [612.00, 792.00]; // 72px per inch
this._fontSize = 0;
this._autoPrint = false;
this._marginLeft = 72;
this._marginRight = 72;
this._marginTop = 72;
this._marginBottom = 72;
this._copiedElementClasses = null;
this._copiedOptions = null;
this._name = "report.pdf";
this._properties = [
{type: 'string', field: 'name', functionable: true, lined: false},
{type: 'boolean', field: 'autoPrint', default: false},
{type: 'number', field: 'fontSize', default: 0},
{type: 'number', title: 'margin.left', field: 'marginLeft', default: 72},
{type: 'number', title: 'margin.right', field: 'marginRight', default: 72},
{type: 'number', title: 'margin.top', field: 'marginTop', default: 72},
{type: 'number', title: 'margin.bottom', field: 'marginBottom', default: 72},
{
type: 'selection',
title: 'Paper Size',
field: 'paperSize',
properCase: true,
values: ['letter', 'legal'],
default: 'letter'
},
{
type: 'selection',
title: 'Orientation',
field: 'paperOrientation',
properCase: true,
values: ['portrait', 'landscape'],
default: 'portrait'
},
{type: 'button', title: 'Variables', click: this._setVariables.bind(this)},
{type: 'button', title: 'Totals', click: this._setTotals.bind(this)},
{type: 'button', title: 'Fonts', click: this._setFonts.bind(this)},
{type: 'button', title: 'Data', click: this._setData.bind(this)}
];
if (typeof options.multiSelectKey !== 'undefined') {
this.setConfig("multiSelectKey", options.multiSelectKey);
}
if (options.scale) {
this.setConfig('scale', options.scale);
} else {
// TODO: Maybe determine size of UI layout and scale dynamically?
this._scale = _frScale;
}
// Allows overriding UI System
if (typeof options.UIBuilder !== 'undefined') {
this.setConfig('UIBuilder', options.UIBuilder);
}
if (typeof options.data !== 'undefined') {
this._parseData(options.data);
}
if (options.report) {
// TODO - FUTURE: Maybe save & verify report based on parsed data to verify data layout being sent into
// - editor matches the report layout's last data, and so we have the field layout in the event no data is passed in.
this._parseReport(options.report);
} else if (options.blankReport === 2) {
this.newBlankReport(typeof options.data === 'undefined');
} else {
this._createReportOnData();
}
if (typeof options.save === 'function') {
this.setConfig('save', options.save);
}
if (typeof options.element !== 'undefined') {
this.setConfig('element', options.element);
} else if (options.id) {
this.setConfig('id', options.id);
}
if (options.debug) {
this.setConfig('debug', options.debug);
}
if (typeof options.preview !== 'undefined') {
this.setConfig('preview', options.preview);
}
if (options.onfocus) {
this.setConfig('onfocus', options.onfocus);
}
if (options.formatterFunctions) {
this.setConfig('formatterFunctions', options.formatterFunctions);
}
this.buildUI(this._parentElement);
}
/*
* Private Properties
*/
get uuid() {
return this._uuid;
}
get reportLayout() {
return this._reportLayout;
}
get reportScroller() {
return this._reportScroller;
}
get sectionConstrainer() {
return this._sectionConstrainer;
}
// noinspection JSUnusedGlobalSymbols
get reportData() {
return this._reportData;
}
get reportFields() {
return this._parsedData;
}
get reportCalculations() {
return this._calculations;
}
get reportVariables() {
return this._reportData.variables;
}
get reportTotals() {
return this._totals;
}
// noinspection JSUnusedGlobalSymbols
get reportSections() {
return this._sections;
}
// noinspection JSUnusedGlobalSymbols
get reportGroups() {
return this._groupBys;
}
// noinspection JSUnusedGlobalSymbols
get reportFunctions() {
return this._functions;
}
// noinspection JSUnusedGlobalSymbols
get sectionIn() {
return this._sectionIn;
}
get additionalFonts() {
return this._registeredFonts;
}
get currentSelected() {
return this._currentSelected || [];
}
set currentSelected(val) {
if (Array.isArray(val)) {
this._currentSelected = val;
} else {
this._currentSelected = [val];
}
}
get multiSelectKey() {
return this._multiSelectKey;
}
get properties() {
return this._properties;
}
get elementTitle() {
return "Report";
}
get gridSnapping() {
return this._gridSnapping;
}
get debugging() {
return this._debugging;
}
get scale() {
return this._scale;
}
get UIBuilder() {
if (this._UIBuilderClass == null) {
this._UIBuilderClass = new this._UIBuilder(this);
}
return this._UIBuilderClass;
}
get frSections() {
return this._frSections;
}
get frElements() {
return this._frElements;
}
get pageWidth() {
return this._paperDims[0] - this._marginLeft - this._marginRight;
}
/*
* Public Properties
*/
get pageHeight() {
return (this._paperDims[1] - this._marginTop) - this._marginBottom;
}
get version() {
return this._version;
}
/**
* Enable/Disable auto printing
* @returns {boolean}
*/
get autoPrint() {
return this._autoPrint;
}
set autoPrint(val) {
this._autoPrint = !!val;
}
/**
* Expose Margins
* @returns {*}
*/
get marginLeft() {
return this._marginLeft;
}
set marginLeft(val) {
this._marginLeft = parseInt(val, 10);
this._resetPaperSizeLocation();
}
get marginRight() {
return this._marginRight;
}
set marginRight(val) {
this._marginRight = parseInt(val, 10);
this._resetPaperSizeLocation();
}
get marginTop() {
return this._marginTop;
}
set marginTop(val) {
this._marginTop = parseInt(val, 10);
}
get marginBottom() {
return this._marginBottom;
}
set marginBottom(val) {
this._marginBottom = parseInt(val, 10);
}
/**
* Set/get the file name of the report
* @returns {string}
*/
get name() {
return this._name;
}
set name(val) {
this._name = val;
}
/**
* Set/Get the font size
* @returns {number}
*/
get fontSize() {
return this._fontSize;
}
set fontSize(val) {
this._fontSize = parseInt(val, 10);
}
/**
* set/get the paper size
* @returns {string}
*/
get paperSize() {
return this._paperSize;
}
// noinspection JSUnusedGlobalSymbols
set paperSize(val) {
if (val === this._paperSize) {
return;
}
switch (val) {
case 'letter':
this._paperDims = [612.00, 792.00];
break;
case 'legal':
this._paperDims = [612.00, 1008.00];
break;
default:
val = 'letter';
this._paperDims = [612.00, 792.00];
}
this._paperSize = val;
if (this._paperOrientation === "landscape") {
this._switchOrientation();
}
this._resetPaperSizeLocation();
}
/**
* Set / get the reports data
* @returns {object}
*/
get data() {
return this._data;
}
set data(val) {
if (this._data !== val) {
this._parseData(val);
}
}
/**
* Set/Get report
* @returns {{type: string}}
*/
get report() {
return this._generateSave();
}
set report(val) {
if (val !== this._reportData) {
this._parseReport(val);
}
}
/**
* Set/Get the Paper orientation
* @returns {string}
*/
get paperOrientation() {
return this._paperOrientation;
}
// noinspection JSUnusedGlobalSymbols
set paperOrientation(val) {
if (val === this._paperOrientation) {
return;
}
if (val === 'landscape') {
this._paperOrientation = "landscape";
} else {
this._paperOrientation = "portrait";
}
this._switchOrientation();
this._resetPaperSizeLocation();
}
get formatterFunctions() {
return this._formatterFunctions;
}
set formatterFunctions(val) {
this._formatterFunctions = val;
}
get onfocus() {
return this._onfocus;
}
set onfocus(val) {
this._onfocus = val;
}
/**
*
* @param val The value you wish to parse
* @param which If you input a %, do you wish to have it a % of width/height
* @return {*}
*/
_parseSize(val, which) {
if (val == null) {
return 0;
}
if (typeof val === 'number') {
return val;
}
if (val.indexOf("%") > 0) {
let temp = parseInt(val, 10) / 100;
if (which === "width") {
return parseInt(this.pageWidth * temp, 10);
} else if (which === "height") {
return parseInt(this.pageHeight * temp, 10);
}
}
return val;
}
/**
* Set the Configuration for a report parameter
* @param option
* @param value
*/
setConfig(option, value) {
if (value == null) {
return;
}
switch (option) {
case 'formatterFunctions':
for (let key in value) {
if (!value.hasOwnProperty(key)) {
continue;
}
if (typeof value[key] === 'function') {
this._formatterFunctions[key] = value[key];
} else {
this._formatterFunctions[key] = new Function('value', 'row', 'callback', value[key]); // jshint ignore:line
}
}
break;
case 'scale':
this._scale = parseFloat(value);
if (isNaN(this._scale)) {
this._scale = _frScale;
}
break;
case 'UIBuilder':
if (typeof value.clearArea !== 'undefined') {
this._UIBuilder = value;
if (this._UIBuilderClass) {
this._UIBuilderClass.destroy();
this._UIBuilderClass = null;
}
}
break;
case 'data':
this._parseData(value);
break;
case 'report':
this._parseReport(value);
break;
case 'onfocus':
if (typeof value === 'function') {
this._onfocus = value;
}
break;
case 'save':
if (typeof value === 'function') {
this._saveFunction = value;
}
break;
case 'multiSelectKey':
switch (value.toString().toLowerCase()) {
case 'ctrl':
case 'ctrlkey':
this._multiSelectKey = "ctrlKey";
break;
case 'shift':
case 'shiftkey':
this._multiSelectKey = "shiftKey";
break;
case 'meta':
case 'alt':
case 'altkey':
case 'metakey':
this._multiSelectKey = "metaKey";
break;
default:
this._multiSelectKey = "ctrlKey";
}
break;
case 'element':
this._parentElement = value;
break;
case 'id':
this._parentElement = document.getElementById(value);
break;
case 'debug':
this._debugging = !!value;
console.log("Debugging", this._debugging);
break;
case 'preview':
if (typeof value === 'function') {
this._previewFunction = value;
}
if (value === false) {
this._previewFunction = false;
} else if (value === true || value == null) {
this._previewFunction = null;
}
if (this._previewButton != null) {
if (this._previewFunction === false) {
this._previewButton.style.display = "none";
} else {
this._previewButton.style.display = "";
}
}
break;
default:
if (this.debugging) {
console.error("fluentReports: unknown setConfig option", option);
}
}
}
/**
* Figures out where the element was dragged to know which section it is now in
* @param offset
* @returns {number}
* @private
*/
_getSectionIn(offset) {
let sec = 0;
const len = this._frSections.length;
for (let i = 0; i < len; i++) {
const top = this._frSections[i].top;
if (offset >= top && offset <= top + this._frSections[i].height) {
sec = i;
break;
}
}
return sec;
}
/**
* Returns a section
* @param id
* @returns {*}
* @private
*/
_getSection(id) {
return this._frSections[id];
}
/**
* Gets the section options
* @param sectionIn
* @returns {{top: string}}
* @private
*/
_getSectionOptions(sectionIn) {
let options = {top: "5px"};
if (sectionIn > 0) {
const section = this._frSections[sectionIn - 1];
// TODO: Change to calculated number 5 is +5 for white space offset
options.top = (parseInt(section._draggable.element.style.top, 10) + 5) + "px";
}
return options;
}
/**
* Detect if we have any formatter functions
* @returns {boolean}
* @private
*/
_hasFormatterFunctions() {
for (let key in this._formatterFunctions) {
if (this._formatterFunctions.hasOwnProperty(key)) {
return true;
}
}
return false;
}
/**
* Handles dealing with switching the paper orientations
* @private
*/
_switchOrientation() {
const temp = this._paperDims[0];
if (this._paperOrientation === 'landscape') {
if (this._paperDims[1] > temp) {
this._paperDims[0] = this._paperDims[1];
this._paperDims[1] = temp;
}
} else {
if (this._paperDims[1] < temp) {
this._paperDims[0] = this._paperDims[1];
this._paperDims[1] = temp;
}
}
}
/**
* Parses the new data file to make sure it is correct
* @param data
* @private
*/
_parseData(data) {
if (!Array.isArray(data)) {
throw new Error("fluentReports: Invalid dataset, should be an array of objects.");
}
if (data.length < 1) {
throw new Error("fluentReports: Invalid dataset, should have at least one record");
}
this._data = data;
this._parsedData = new frReportData(data, null, "primary");
}
/**
* Creates a dummy report based on the data, if you haven't passed in a report.
* @private
*/
newBlankReport(clearData = true) {
const tempReport = {
type: "report",
header: {children: []},
detail: {children: []},
footer: {children: []}
};
if (clearData) {
this.data = [];
}
this._parseReport(tempReport);
}
_createReportOnData() {
let tempReport;
if (this._data.length === 0) {
tempReport = {
type: "report",
header: {children: [{type: "raw", values: ["Sample Header"]}]},
detail: {children: [{type: "print", text: "Welcome to fluentReports"}]},
footer: {children: [{type: "raw", values: ["Sample Footer"]}]}
};
} else {
tempReport = {
type: "report",
header: {children: [{type: "raw", values: ["Sample Header"]}]},
footer: {children: [{type: "raw", values: ["Sample Footer"]}]}
};
if (this.reportFields.childrenIndexed.length === 0) {
tempReport.detail = {children: [{type: "print", text: "Welcome to fluentReports"}]};
} else {
this._createReportOnChildData(tempReport, this.reportFields.childrenIndexed);
}
}
this._parseReport(tempReport);
}
_createReportOnChildData(src, children) {
if (!Array.isArray(src.subReports)) {
src.subReports = [];
}
for (let i = 0; i < children.length; i++) {
const curReport = {type: 'report', dataType: 'parent', data: children[i].name};
src.subReports.push(curReport);
// Check for sub-children
let newChildren = children[i].childrenIndexed;
if (newChildren.length) {
this._createReportOnChildData(curReport, newChildren);
} else {
const parent = children[i].parent;
let name = children[i].name;
if (parent.parent) {
name = parent.name + "/" + name;
}
curReport.detail = {children: [{type: "print", text: "Subreport " + name + " Data"}]};
}
}
}
/**
* Parses a Report
* @param report
*/
_parseReport(report) {
if (report && typeof report.version !== "undefined") {
if (report.version !== 1 && report.version !== 2) {
console.error("This engine only understands version _1 & 2_ reports, please upgrade engine to use this report.");
report = {
type: "report",
detail: {children: [{type: "print", text: "Invalid Report version"}]}
};
}
this._version = report.version;
}
this._reportData = report;
if (this._builtUI) {
this._clearReport();
// Create the Sections
this._subReportCounter = 0;
this._groupCounter = {counter: 0};
this._generateReportLayout(this._reportData, 57, "", [{
type: "report",
index: 0,
datatUUID: this._parsedData.dataUUID
}], this._parsedData.dataUUID);
}
// TODO: Add any missing properties
this._copyProperties(report, this, ["name", "fontSize", "autoPrint", "paperSize", "paperOrientation"]);
if (report.formatterFunctions) {
this.setConfig('formatterFunctions', report.formatterFunctions);
}
// Does report come with its own data?
if (Array.isArray(report.data)) {
this.data = report.data;
delete report.data;
this._includeData = true;
}
if (Array.isArray(report.fonts) && report.fonts.length) {
this._registeredFonts = report.fonts;
}
// Add Margins
if (typeof report.margins !== 'undefined') {
if (isNaN(parseInt(report.margins, 10))) {
this._marginBottom = report.margins.bottom || 72;
this._marginTop = report.margins.top || 72;
this._marginRight = report.margins.right || 72;
this._marginLeft = report.margins.left || 72;
} else {
const margin = parseInt(report.margins, 10);
this._marginBottom = this._marginTop = this._marginRight = this._marginLeft = margin;
}
}
// TODO: Add any missing properties
this._copyProperties(report, this, ["name", "fontSize", "autoPrint", "paperSize", "paperOrientation"]);
if (this._builtUI) {
this._reportSettings();
}
}
/**
* Generate the Data for each of the child reports
* @param dataSet
* @param results
* @private
*/
_generateChildSave(dataSet, results) {
let newResult = {dataUUID: dataSet.dataUUID, dataType: dataSet.dataType, data: dataSet.data, type: "report"};
if (!Array.isArray(results.subReports)) {
results.subReports = [];
}
this._saveTemporaryData.reportTemplates[dataSet.dataUUID] = newResult;
// Loop through the children of this child
let children = dataSet.childrenIndexed;
for (let i = 0; i < children.length; i++) {
this._generateChildSave(children[i], newResult);
}
}
/**
* Starts generating the save data
* @returns {{type: string, dataUUID: number}}
* @private
*/
_generateSave() {
// Setup our temporary data storage
this._saveTemporaryData = {reportTemplates: {}, sectionDestinations: {}};
const results = {type: 'report', dataUUID: this._parsedData.dataUUID, version: 2};
this._copyProperties(this, results, ["fontSize", "autoPrint", "name", "paperSize", "paperOrientation"]);
if (this._marginBottom !== 72 || this._marginTop !== 72 || this._marginLeft !== 72 || this._marginRight !== 72) {
results.margins = {
left: this._marginLeft,
top: this._marginTop,
right: this._marginRight,
bottom: this._marginBottom
};
}
// Add our first level report
this._saveTemporaryData.reportTemplates[this._parsedData.dataUUID] = results;
results.fonts = this.additionalFonts;
results.variables = shallowClone(this.reportVariables);
// This actually generates a Sub-Report info keys
let children = this._parsedData.childrenIndexed;
for (let i = 0; i < children.length; i++) {
this._generateChildSave(children[i], results);
}
// Save the Sections
for (let i = 0; i < this._frSections.length; i++) {
//Sort section's children by Y pos
if (this._frSections[i]._children) {
this._frSections[i]._children.sort((element1, element2) => {
return element1.absoluteY === element2.absoluteY ? (element1.absoluteX - element2.absoluteX) : (element1.absoluteY - element2.absoluteY);
});
}
this._frSections[i]._generateSave(results, this._saveTemporaryData.reportTemplates, this._saveTemporaryData);
}
// Save the Totals..{type: 'report', detail: [], dataUUID: }
this._saveTotals();
//Copy over subreports & groups from the _saveTemporaryData.sectionDestinations
let sectionDestinationKeys = Object.keys(this._saveTemporaryData.sectionDestinations);
sectionDestinationKeys = sectionDestinationKeys.sort((a, b) => {
return a.length - b.length;
});
for (let i = 0; i < sectionDestinationKeys.length; i++) {
let section = this._saveTemporaryData.sectionDestinations[sectionDestinationKeys[i]];
let destination = null;
let quit = false;
for (let j = 0; j < section.treePathing.length; j++) {
let treePath = section.treePathing[j];
if (quit) {
break;
}
switch (treePath.type) {
case "report":
destination = results;
if (j + 1 === section.treePathing.length) {
for (let q in section.container) {
if (section.container.hasOwnProperty(q)) {
results[q] = section.container[q];
}
}
}
break;
case "groupBy":
if (destination) {
if (j + 1 === section.treePathing.length) {
if (!destination.groupBy) {
destination.groupBy = [];
}
destination.groupBy[treePath.index] = section.container;
} else {
if (!destination.groupBy) {
let continueOn = false;
for (let q = 0; q < this._groupBys.length; q++) {
if (this._groupBys[q].dataUUID === treePath.dataUUID) {
continueOn = true;
destination.groupBy = [];
destination.groupBy[treePath.index] = {
type: "group",
groupOn: this._groupBys[q].name
};
destination = destination.groupBy[treePath.index];
break;
}
}
if (!continueOn) {
quit = true;
break;
}
} else {
destination = destination.groupBy[treePath.index];
}
}
}
break;
case "subReports":
case "subReport":
if (destination) {
if (!destination.subReports) { destination.subReports = []; }
if (j + 1 === section.treePathing.length) {
destination.subReports[treePath.index] = section.container;
} else {
if (!destination.subReports[treePath.index]) {
destination.subReports[treePath.index] = {
data: this._parsedData.findByUUID(treePath.dataUUID).data,
dataType: 'parent'
};
}
destination = destination.subReports[treePath.index];
}
}
break;
}
}
}
// TODO: See if this is needed anymore? Seems all data is found
// Update groups data with any Groups that have no actual sections
for (let i = 0; i < this._groupBys.length; i++) {
let found = false;
let curData = this._saveTemporaryData.reportTemplates[this._groupBys[i].dataUUID];
let dataSet = this._parsedData.findByUUID(this._groupBys[i].dataUUID);
if (!curData) {
curData = {
type: 'report',
dataUUID: this._groupBys[i].dataUUID,
dataType: 'parent',
data: dataSet.name,
groupBy: []
};
this._saveTemporaryData.reportTemplates[this._groupBys[i].dataUUID] = curData;
} else if (curData.groupBy) {
for (let j = 0; j < curData.groupBy.length; j++) {
if (curData.groupBy[j].groupOn === this._groupBys[i].name) {
found = true;
j = curData.groupBy.length;
}
}
} else {
curData.groupBy = [];
}
if (!found) {
console.warn("Save; found missing group", this._groupBys[i].name);
curData.groupBy.push({type: "group", groupOn: this._groupBys[i].name});
}
}
// TODO: Is this needed????
// Remove Groups in Report, that no longer exist in the "groupBy" data
for (let key in this._saveTemporaryData.reportTemplates) {
if (!this._saveTemporaryData.reportTemplates.hasOwnProperty(key)) {
continue;
}
let curData = this._saveTemporaryData.reportTemplates[key];
// No Groups; proceed to the next one...
if (!curData.groupBy) {
continue;
}
for (let j = 0; j < curData.groupBy.length; j++) {
let found = false;
for (let k = 0; k < this._groupBys.length; k++) {
// TODO: Now - Need to check to see about DATA UUID match?
if (curData.groupBy[j].groupOn === this._groupBys[k].name) {
found = true;
}
}
if (!found) {
console.log("Deleting from Save", curData.groupBy[j]);
curData.groupBy.splice(j, 1);
j--;
}
}
}
if (this._includeData) {
results.data = this._data;
}
// Copy any json based formatters over to the new report
if (this._reportData.formatterFunctions) {
results.formatterFunctions = this._reportData.formatterFunctions;
}
// Clear our temporary data storage
this._saveTemporaryData = null;
// Strip out all dataUUID from a non-debugged report
if (!this._debugging) {
const cleanSubReport = (subReport) => {
delete subReport.dataUUID;
if (subReport.subReports) {
for (let i = 0; i < subReport.subReports.length; i++) {
cleanSubReport(subReport.subReports[i]);
}
}
};
cleanSubReport(results);
}
return results;
}
/**
* Saves any total information
* @private
*/
_saveTotals() {
const totals = this.reportTotals;
for (let key in totals) {
if (!totals.hasOwnProperty(key)) {
continue;
}
for (let i = 0; i < totals[key].length; i++) {
let dataUUID = 0;
for (let j = 0; j < totals[key][i].treePathing.length; j++) {
if (totals[key][i].treePathing[j].dataUUID) {
dataUUID = totals[key][i].treePathing[j].dataUUID;
}
}
if (!dataUUID) {
continue;
}
const fields = this._parsedData.findByUUID(dataUUID).fields;
if (fields.indexOf(totals[key][i].total) >= 0) {
const destinationStr = this.turnTreePathingIntoString(totals[key][i].treePathing);
if (!this._saveTemporaryData.sectionDestinations[destinationStr]) {
this._saveTemporaryData.sectionDestinations[destinationStr] = {treePathing: totals[key][i].treePathing};
}
if (!this._saveTemporaryData.sectionDestinations[destinationStr].container) {
this._saveTemporaryData.sectionDestinations[destinationStr].container = this._saveTemporaryData.reportTemplates[dataUUID];
}
if (!this._saveTemporaryData.sectionDestinations[destinationStr].container.calcs) {
this._saveTemporaryData.sectionDestinations[destinationStr].container.calcs = {};
}
if (!this._saveTemporaryData.sectionDestinations[destinationStr].container.calcs[key]) {
this._saveTemporaryData.sectionDestinations[destinationStr].container.calcs[key] = [];
}
this._saveTemporaryData.sectionDestinations[destinationStr].container.calcs[key].push(totals[key][i].total);
}
}
}
}
/**
* Saves any Variable information
* @private
*/
_setVariables() {
this.UIBuilder.variableBrowse(this._reportData.variables, (value) => {
this._reportData.variables = value;
});
}
/**
* Edit the Total variables
* @private
*/
_setTotals() {
this.UIBuilder.totalsBrowse(this.reportTotals, this, (value) => {
this._totals = value;
});
}
/**
* Edit the Fonts variables
* @private
*/
_setFonts() {
this.UIBuilder.fontsBrowse(this.additionalFonts, (value) => {
this._registeredFonts = value;
});
}
_setData() {
this.UIBuilder.dataEditor(this._data, this._includeData, (data, include) => {
this._parseData(data);
this._includeData = include;
});
}
/**
* Simple shallow clone of properties to another object
* Used to copy properties from/to save structure
* @param src
* @param dest
* @param props
* @private
*/
_copyProperties(src, dest, props) {
if (src == null) {
return;
}
for (let i = 0; i < props.length; i++) {
if (typeof src[props[i]] !== 'undefined') {
dest[props[i]] = src[props[i]];
}
}
}
/**
* Clears a Report
* @private
*/
_clearReport() {
// Reset Tracking Data
this._includeData = false;
this._totals = {};
this._sectionIn = 0;
this._calculations = [];
this._functions = [];
this._groupBys = [];
this._registeredFonts = [];
this.UIBuilder.clearArea(this._reportLayout);
// Read-add our Section Constrainer
this._reportLayout.appendChild(this._sectionConstrainer);
this.UIBuilder.clearArea(this._propertiesLayout);
this._frElements = [];
this._frSections = [];
}
buildUI(idOrElement) {
if (this._builtUI) {
console.error("fluentReports: Attempting to call build on an already built report.");
return true;
}
// If it hasn't already been assigned in the Constructor...
if (!this._parentElement) {
if (idOrElement != null) {
if (typeof idOrElement === "string") {
this._parentElement = document.getElementById(idOrElement);
if (this._parentElement == null) {
console.error("fluentReports: Unable to find dev element: ", idOrElement);
return false;
}
} else {
this._parentElement = idOrElement;
}
}
if (!this._parentElement) {
console.error("fluentReports: Missing element");
return false;
}
}
this._frame = document.createElement("div");
this._frame.style.position = "relative";
this._frame.style.height = (this._parentElement.clientHeight < 300 ? 300 : this._parentElement.clientHeight) + "px";
// Prefix the entire subtree with our name space for CSS resolution
this._frame.classList.add("fluentReports");
this._parentElement.appendChild(this._frame);
// Keep from running a second time...
this._builtUI = true;
if (this._includeCSS) {
// TODO: Check to see if this file already exists in the head area
let link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css');
link.setAttribute('href', './fr.css');
// Find the ShadowRoot, if it exists
let parent = this._parentElement;
let hasShadow = false;
while (parent != null) {
if (parent instanceof ShadowRoot) {
hasShadow = true;
break;
}
if (parent instanceof HTMLBodyElement || parent instanceof HTMLHeadElement) {
break;
}
parent = parent.parentNode;
}
if (hasShadow && parent != null) {
parent.appendChild(link);
} else {
document.getElementsByTagName('head')[0].appendChild(link);
}
}
let fontSheet = document.createElement('style');
// noinspection SpellCheckingInspection
fontSheet.innerHTML = "@font-face { font-family: 'fr'; font-weight: normal; font-style: normal; " +
"src: url(data:application/font-woff;charset=utf-8;base64,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