/** * A view that contains and displays data. * Provides functions to convert the data to CSV for exports. * To be enhanced by more table-related functions in the future. */ class DataTable2 extends View { /** * Returns an object with the default column information that will apply if no specific rules are set for a column in the column information object. */ get defaultColumnInformation() { return { displayName: "unnamed", type: "string" }; } build(element) { this.table = document.createElement("table"); element.appendChild(this.table); this.head = document.createElement("thead"); this.table.appendChild(this.head); this.body = document.createElement("tbody"); this.table.appendChild(this.body); this.foot = document.createElement("tfoot"); this.table.appendChild(this.foot); this.aggregationRow = document.createElement("tr"); this.foot.appendChild(this.aggregationRow); this.summaryRow = document.createElement("tr"); this.summaryRow.className = "summary"; this.summaryRow.style.display = "none"; this.foot.appendChild(this.summaryRow); const rowCountTd = document.createElement("th"); this.summaryRow.appendChild(rowCountTd); this.rowCountLabel = document.createElement("span"); rowCountTd.appendChild(this.rowCountLabel); const rowCountTitleLabel = document.createElement("span"); rowCountTitleLabel.innerText = " results"; rowCountTd.appendChild(rowCountTitleLabel); return element; } setData(data) { this.data = data; this.update(); } setColumnInformation(columnInformation) { this.columnInformation = columnInformation; } update() { //Make sure we have data. if (!this.data) { return; } //Prepare and perform the aggregations. this.sums = [ ]; this.hasAggregation = false; this.aggregations = [ ]; let aggregationLabelIndex = -1; for (let i = 0; i < this.data.length; i++) { const row = this.data[i]; for (let j = 0; j < row.length; j++) { let value = row[j]; const columnInformation = this.getColumnInformation(this.data[0][j]); if (i == 0) { //Prepare the aggregations. //Add the sum counter for each column. this.sums.push(0); if (columnInformation.aggregation) { this.hasAggregation = true; this.aggregations.push(columnInformation.aggregation); } else { this.aggregations.push(null); } } else { //Perform the aggregation only if the cell is not null. if (columnInformation.aggregation == "sum" && value) { //Calculate cell value if calculation function is available. if (columnInformation.getCellValue) { value = columnInformation.getCellValue(value, row, i); } //Same for array cells. if (columnInformation.getArrayCellValue) { const keys = this.getArrayCellKeys(value); value = columnInformation.getArrayCellValue(keys, row, i); } //Skip if value is not a number. if (isNaN(value)) { continue; } this.sums[j] += value; } } } } //Clear table. this.head.innerHTML = ""; this.body.innerHTML = ""; this.aggregationRow.innerHTML = ""; //Count visible columns for summary colspan. let visibleColumns = 0; //Build header. if (this.data.length > 0) { const rowData = this.data[0]; const row = document.createElement("tr"); this.head.appendChild(row); for (let i = 0; i < rowData.length; i++) { const columnKey = rowData[i]; const columnInformation = this.getColumnInformation(columnKey); if (!columnInformation.hidden) { const cell = document.createElement("th"); this.buildTitleCell(columnKey, columnInformation, cell); row.appendChild(cell); //Use first visible column as aggregation label column. if (aggregationLabelIndex == -1) { aggregationLabelIndex = i; } visibleColumns++; } } } //Build data rows. for (let i = 1; i < this.data.length; i++) { const rowData = this.data[i]; const row = document.createElement("tr"); this.body.appendChild(row); for (let j = 0; j < rowData.length; j++) { const value = rowData[j]; const columnKey = j < this.data[0].length ? this.data[0][j] : "unknown"; const columnInformation = this.getColumnInformation(columnKey); if (!columnInformation.hidden) { const cell = document.createElement("td"); this.buildCell(columnKey, value, columnInformation, cell, i); row.appendChild(cell); } } } //Build the new aggregation row. this.aggregationRow.style.display = this.hasAggregation ? "" : "none"; if (this.hasAggregation) { for (let i = 0; i < this.data[0].length; i++) { const columnKey = this.data[0][i]; const columnInformation = this.getColumnInformation(columnKey); if (columnInformation.hidden) { continue; } let value = null; if (this.aggregations[i] == "sum") { value = this.sums[i]; } else if (i == aggregationLabelIndex) { value = this.columnInformation.aggregationLabel; } const cell = document.createElement("th"); this.buildCell(columnKey, value, columnInformation, cell, -1); this.aggregationRow.appendChild(cell); } } //Show summary information. this.rowCountLabel.innerText = "" + (this.data.length - 1); this.rowCountLabel.parentNode.colSpan = visibleColumns; this.summaryRow.style.display = visibleColumns > 0 ? "" : "none"; } /** * Constructs the table title cell with the given column name and column information. * The element that is passed as the third parameter is the cell element that may be filled to the developer's heart's content. */ buildTitleCell(columnKey, columnInformation, cell) { cell.style.textAlign = columnInformation.textAlign ?? this.getTextAlignForColumnType(columnInformation.type); // if (this.getTextOrientationForColumn(columnKey, columnInformation) == "vertical") { // cell.style.textAlign = "center"; // var labelDiv = document.createElement("div"); // labelDiv.innerText = columnInformation.displayName; // labelDiv.style.writingMode = "vertical-rl"; // labelDiv.style.lineBreak = "strict"; // labelDiv.style.marginLeft = "-0.3em"; // labelDiv.style.display = "inline-block"; // labelDiv.style.verticalAlign = "text-bottom"; // cell.appendChild(labelDiv); // } // else { cell.innerText = columnInformation.displayName; // } } /** * Constructs the table cell with the given column name, value and column information. * The element that is passed as the fourth parameter is the cell element that may be filled to the developer's heart's content. * The index parameter is the current row index of the cell. Notice that 0 is the header row and 1 is the actual first row. -1 is the aggregation row. */ buildCell(columnKey, value, columnInformation, cell, index) { let type = columnInformation.type; if (index != -1 && columnInformation.getCellValue) { value = columnInformation.getCellValue(value, this.data[index], index); } if (index != -1 && columnInformation.getArrayCellValue) { const keys = this.getArrayCellKeys(value); value = columnInformation.getArrayCellValue(keys, this.data[index], index); //If only one value is present, use single type. if (keys.length <= 1) { type = columnInformation.singleType ?? type; } } if (type == "currency" && value !== null) { //Round the value if it is a currency field. value = (typeof value === "number") ? value.toFixed(2) : value; } if ((type == "number" || type == "difference") && value !== null) { //Round the value if a rounding is demanded. if (columnInformation.round) { value = (typeof value === "number") ? parseFloat(value.toFixed(columnInformation.round)) : value; } //Add "+" in front of value in case of a difference value. if (type == "difference") { value = (value > 0 ? "+" : "") + value; } } if (type == "boolean") { value = value !== null && value !== "" ? (value === true || value === 1 || value.toUpperCase() == "TRUE" ? "✓" : "✗") : ""; } cell.innerText = value; cell.style.textAlign = columnInformation.textAlign ?? this.getTextAlignForColumnType(type); } getArrayCellKeys(value) { return value?.replaceAll(", ", ",").split(",") ?? []; } /** * Generates a text representation of an HTML cell (th or td) with the given column key and column information. * Returns innerText by default. This behaviour may be overridden by other reports. * DO NOT add quotes to the string nor do any escaping. This is done somewhere else. */ convertCellToCsv(cell) { return cell.innerText; } /** * Returns the appropriate text alignment for the given column type as a string ("left", "right", "center"). * The default alignment is "left". */ getTextAlignForColumnType(type) { if (type == "boolean") { return "center"; } else if ([ "number", "difference", "currency", "time" ].indexOf(type) != -1) { return "right"; } return "left"; } // /** // * Returns either "horizontal" or "vertical", depending on how the given column should be displayed. // */ // getTextOrientationForColumn(columnKey, columnInformation) { // return columnInformation.orientation ? columnInformation.orientation : "horizontal"; // } /** * Returns the column information for the column with the given column name. */ getColumnInformation(columnKey) { //Get the default column information that is used when no specific rules are defined. var columnInformation = this.defaultColumnInformation; //Use the raw column name as the default display name. columnInformation.displayName = columnKey; //Override the default column information with the custom rules, if applicable. if (this.columnInformation?.fields && (columnKey in this.columnInformation.fields)) { var customColumnInformation = this.columnInformation.fields[columnKey]; for (var property in customColumnInformation) { columnInformation[property] = customColumnInformation[property]; } } return columnInformation; } /** * Returns a CSV string with the table's data. */ toCsvString() { if (!this.data || this.data.length == 0) { return ""; } let csvString = ""; const rows = [...this.head.querySelectorAll("tr"), ...this.body.querySelectorAll("tr"), this.aggregationRow]; for (const row of rows) { if (csvString != "") { csvString += "\r\n"; } for (let j = 0; j < row.children.length; j++) { const cellElement = row.children[j]; let cell = this.convertCellToCsv(cellElement); //Replace all quotes by double quotes. cell = cell.replaceAll("\"", "\"\""); //Enclose cell into quotes if it contains commas or line breaks. if (cell.indexOf(",") != -1 || cell.indexOf("\r") != -1 || cell.indexOf("\n") != -1 || cell.indexOf("\r\n") != -1) { cell = "\"" + cell + "\""; } if (j > 0) { csvString += ","; } csvString += cell; //Add empty cells if there is a colspan. for (let k = 1; k < cellElement.colSpan; k++) { csvString += ","; } } } return csvString; } } UIKit.registerViewType(DataTable2, "data-table-2");