UNPKG

fluentreports

Version:

A simple, Fluent API for creating PDF Reports

1,297 lines (1,132 loc) 390 kB
/************************************************************************************** * (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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