/** * Copyright JS Foundation and other contributors, http://js.foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ var path = require('path'); var mergeTabulator = function(target,source) { if (typeof source === 'object') { Object.keys(source).forEach(element => { if (typeof source[element] !== "object") { target[element] = source[element]; } else { if (!target.hasOwnProperty(element)) { target[element] = (Array.isArray(source[element])) ? [] : {}; } // handle the columns array to merge columns if the field property matches. Otherwise push a new column if (element==='columns' && Array.isArray(source[element])){ source[element].forEach(sourceElement => { let index = target[element].findIndex(targetElement => ( (targetElement.field && sourceElement.field && targetElement.field===sourceElement.field) || // normal column object (targetElement.title && sourceElement.title && targetElement.title===sourceElement.title) // parent object with nested columns )); if (index<0) { // add new column index=target[element].push({})-1; } mergeTabulator(target[element][index],sourceElement); }) } else { mergeTabulator(target[element],source[element]) } } }); } else { target=source; } } module.exports = function (RED) { function checkConfig(node, conf) { if (!conf || !conf.hasOwnProperty('group')) { node.error(RED._('table.error.no-group')); return false; } else { return true; } } function HTML(config,dark) { var configAsJson = JSON.stringify(config, (key, value) => { // exclude the node description if (key === "info") { return undefined; } // replace single quotation mark (apostrophe) by html code in strings if (typeof (value) === "string") { return value.replace(/'/g, "'"); } // all others leave unchanged return value; }); var mid = (dark) ? "_midnight" : ""; var html = String.raw`
`; return html; } function TableNode(config) { var done = null; var node = this; try { RED.nodes.createNode(this, config); if (checkConfig(node, config)) { var ui = RED.require('node-red-dashboard')(RED); var sizes = ui.getSizes(); // var luma = 255; // if (ui.hasOwnProperty("getTheme") && (ui.getTheme() !== undefined)) { // var rgb = parseInt(ui.getTheme()["page-sidebar-backgroundColor"].value.substring(1), 16); // convert rrggbb to decimal // luma = 0.2126 * ((rgb >> 16) & 0xff) + 0.7152 * ((rgb >> 8) & 0xff) + 0.0722 * ((rgb >> 0) & 0xff); // per ITU-R BT.709 // } var html = HTML(config,ui.isDark()); done = ui.addWidget({ node: node, width: config.width, height: (config.height > 2) ? config.height : 2, // min height to 2 so auto will show something format: html, templateScope: 'local', order: config.order || 0, group: config.group, forwardInputMessages: false, storeFrontEndInputAsState: false, // to make msg.ui_control work without msg.payload we have to send msg.payload=null. // we correct this here into undefined to get the last known payload form currentValues[opt.node.id]. convert: function (value) { if (value===null) value=undefined; return value; }, // merge new ui_control messages into config.ui_control // Help needed: use the already build in ui_control mechanism from ui.js beforeEmit: function (msg, value) { // cache ui_control messages for new clients if (msg.hasOwnProperty('ui_control')) { if (!config.hasOwnProperty('ui_control')){ config.ui_control={ "tabulator":{ "columns":config.columns }}; } // use mergeTabulator to correctly merge columns arrays if field property matches mergeTabulator(config.ui_control,msg.ui_control); // delete column definitions by sending a empty columns array (requires page reload) if (msg.ui_control.tabulator && msg.ui_control.tabulator.columns && Array.isArray(msg.ui_control.tabulator.columns) && msg.ui_control.tabulator.columns.length==0) { config.ui_control.tabulator.columns=[]; config.ui_control.tabulator.autoColumns=true; } } return { msg: { payload: value, ui_control: config.ui_control, socketid: msg.socketid }}; }, beforeSend: function (msg, orig) { if (orig) { return orig.msg; } }, initController: function ($scope, events) { $scope.inited = false; $scope.tabledata = []; var tablediv; var mergeObject = function(target,source) { if (typeof source === 'object') { Object.keys(source).forEach(element => { if (typeof source[element] !== "object") { target[element] = source[element]; } else { if (!target.hasOwnProperty(element)) { target[element] = (Array.isArray(source[element])) ? [] : {}; } mergeObject(target[element],source[element]) } }); } else { target = source; } }; var createTable = function(basediv, tabledata, columndata, outputs, ui_control) { // add id field if not already exists if (columndata.length>0 && tabledata.length>0 && tabledata[0] && typeof tabledata[0] === 'object' && !tabledata[0].hasOwnProperty('id')) { tabledata.map((row,index) => row.id = index); } var opts = { data: tabledata, layout: 'fitColumns', columns: columndata, autoColumns: columndata.length == 0, movableColumns: true, } if (!ui_control || !ui_control.tabulator) { var y = (columndata.length === 0) ? 25 : 32; if ($scope.height==2) { // auto height opts.height = (tabledata.length > 0 )? tabledata.length * y + 26 : $scope.height*(sizes.sy+sizes.cy); } else { opts.height = $scope.height*(sizes.sy+sizes.cy); } } else { // configuration via ui_control //as Object.assign is not supported by Internet Explorer //opts = Object.assign(opts, ui_control.tabulator); mergeObject(opts,ui_control.tabulator); var y = (opts.columns && (opts.columns.length > 0)) ? 32 : 25; if (ui_control.customHeight) { opts.height= ui_control.customHeight * y + 26; } else { if ($scope.height==2) { // auto height opts.height= (tabledata.length > 0 )? tabledata.length * y + 26 : $scope.height*(sizes.sy+sizes.cy); } else { opts.height = $scope.height*(sizes.sy+sizes.cy); } } } // end of configuration via ui_control if ((outputs > 0) && !opts.hasOwnProperty('cellClick')) { // default cellClick if not already defined by ui_control opts.cellClick = function(e, cell) { $scope.send({topic:cell.getField(), payload:cell.getData(), row:(cell.getRow()).getPosition()}); }; } //turn autoColumns off if opts.columns is array with length > 0 if (opts.columns && Array.isArray(opts.columns) && opts.columns.length>0) { opts.autoColumns = false; } // console.log("createTabulator",opts); if ($scope.table !== undefined) { $scope.table.destroy(); } $scope.table = new Tabulator(basediv, opts); }; $scope.init = function (config) { $scope.config = config; tablediv = '#ui_table-' + $scope.$eval('$id') var stateCheck = setInterval(function() { if (document.querySelector(tablediv) && $scope.tabledata) { clearInterval(stateCheck); $scope.inited = true; createTable(tablediv,$scope.tabledata,$scope.config.columns,$scope.config.outputs,$scope.config.ui_control); $scope.tabledata = []; } }, 200); // lowest setting on my side ... still fails sometimes ;) }; $scope.$watch('msg', function (msg) { //console.log("ui-table message arrived:",msg); if (msg && msg.hasOwnProperty("ui_control") && msg.ui_control.hasOwnProperty("callback")) return msg; // to avoid loopback from callbacks. No better solution jet. Help needed. //console.log("ui-table msg: ", msg); // configuration via ui_control if (msg && msg.hasOwnProperty("ui_control")) { var addValueOrFunction = function (config,param,value) { if (typeof String.prototype.parseFunction != 'function') { String.prototype.parseFunction = function () { var funcReg = /function *\(([^()]*)\)[ \n\t]*{(.*)}/gmi; var match = funcReg.exec(this.replace(/\n/g, ' ')); if (match) { return new Function(match[1].split(','), match[2]); } return null; }; } var valueFunction; if (typeof value === "string" && (valueFunction = value.parseFunction())) { config[param]=valueFunction.bind($scope); // to enable this.send() for callback functions. } else config[param]= value; } var addObject = function (destinationObject,sourceObject) { for (var element in sourceObject) { if (!destinationObject[element]) destinationObject[element]=(Array.isArray(sourceObject[element]))? [] : {}; if (typeof sourceObject[element] === "object") { addObject(destinationObject[element],sourceObject[element]) } else { addValueOrFunction(destinationObject,element,sourceObject[element]); } } } if (!$scope.config.ui_control) { $scope.config.ui_control={}; } addObject($scope.config.ui_control,msg.ui_control); } // end of configuration via ui_control if (msg && msg.hasOwnProperty("payload")) { if (Array.isArray(msg.payload)) { $scope.tabledata = msg.payload; } // commands to tabulator via msg.payload object if (typeof msg.payload === "object" && msg.payload!==null && !Array.isArray(msg.payload)) { if (msg.payload.hasOwnProperty("command") && $scope.table!==undefined) { if (!msg.payload.hasOwnProperty("arguments") || !Array.isArray(msg.payload.arguments)) { msg.payload.arguments=[]; } if (msg.payload.returnPromise) { $scope.table[msg.payload.command].apply($scope.table,msg.payload.arguments).then(function(...args){ $scope.send({topic:"success", ui_control: {callback:$scope.msg.payload.command}, return:$scope.msg.payload}); }).catch(function(error){ if (Object.keys(error).length>0) { $scope.send({topic:"error", ui_control: {callback:$scope.msg.payload.command}, return:$scope.msg.payload, error: error}); } }); } else { $scope.table[msg.payload.command].apply($scope.table,msg.payload.arguments); } return; } return; } // end of commands to tabulator via msg.payload object } if ($scope.inited == false) { return; } else { createTable(tablediv, $scope.tabledata, $scope.config.columns, $scope.config.outputs, $scope.config.ui_control); } }); } }); } } catch (e) { console.log(e); } node.on('close', function () { if (done) { done(); } }); } RED.nodes.registerType('ui_table', TableNode); var uipath = 'ui'; if (RED?.settings?.ui) { uipath = RED.settings.ui.path; } var fullPath = path.join('/', uipath, '/ui-table/*').replace(/\\/g, '/'); RED.httpNode.get(fullPath, function (req, res) { var options = { root: __dirname + '/lib/', dotfiles: 'deny' }; res.sendFile(req.params[0], options) }); };