@maddytp/node-red-dashboard-2-ui-reef
Version:
Set of UI nodes for aquarium automation based on @flowfuse/node-red-dashboard
728 lines (725 loc) • 35.6 kB
HTML
<script type="text/html" data-template-name="ui-output">
<style>
.func-tabs-row {
margin-bottom: 0;
}
#node-input-libs-container-row .red-ui-editableList-container {
padding: 0px;
}
#node-input-libs-container-row .red-ui-editableList-container li {
padding: 0px;
}
#node-input-libs-container-row .red-ui-editableList-item-remove {
right: 5px;
}
#node-input-libs-container-row .red-ui-editableList-header {
display: flex;
background: var(--red-ui-tertiary-background);
padding-right: 75px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
#node-input-libs-container-row .red-ui-editableList-header>div {
flex-grow: 1;
}
.node-libs-entry {
display: flex;
}
.node-libs-entry .red-ui-typedInput-container {
border-radius: 0;
border: none;
}
.node-libs-entry .red-ui-typedInput-type-select {
border-radius: 0 ;
height: 34px;
}
.node-libs-entry>span>input[type=text] {
border-radius: 0;
border-top-color: var(--red-ui-form-background);
border-bottom-color: var(--red-ui-form-background);
border-right-color: var(--red-ui-form-background);
}
.node-libs-entry>span>input[type=text].input-error {}
.node-libs-entry>span {
flex-grow: 1;
width: 50%;
position: relative;
}
.node-libs-entry span .node-input-libs-var,
.node-libs-entry span .red-ui-typedInput-container {
width: 100%;
}
.node-libs-entry>span>span>i {
display: none;
}
.node-libs-entry>span>span.input-error>i {
display: inline;
}
</style>
<input type="hidden" id="node-input-func">
<input type="hidden" id="node-input-noerr">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name"></div>
</div>
<div class="form-row func-tabs-row">
<ul style="min-width: 400px; margin-bottom: 20px;" id="func-tabs"></ul>
</div>
<div id="func-tabs-content" style="min-height: calc(100% - 220px);">
<div id="func-tab-config" style="display:none">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</span></label>
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-group"></div>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-label"></div>
</div>
<div class="form-row" id="template-row-size">
<label><i class="fa fa-object-group"></i> Size</span></label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-envelope-o"></i> Topic</label>
<input type="text" id="node-input-topic">
</div>
<div class="form-row">
<label for='node-input-useThemeColors'><i class='fa fa-paint-brush'></i> Colors</label>
<input type='checkbox' id='node-input-useThemeColors' style='width: auto; border: none; vertical-align: baseline;'>
<span for='node-input-useThemeColors' style="padding-left: 5px;">Use theme colors?</span>
</div>
<div class="form-row" style="margin-bottom:0;">
<label style="width: 100px"><i class="fa fa-list"></i> Options:</label>
<span id="configError" style="color:#DD0000"><b>All Values must be unique.</b></span>
</div>
<div class="form-row">
<ol id="node-input-opts-container"></ol>
</div>
</div>
<div id="func-tab-body" style="display:none; min-height: calc(100% - 95px);">
<div class="form-row">
<label for="node-input-outputs" style="width: 100px"><i class="fa fa-random"></i> Outputs:</label>
<input id="node-input-outputs" style="width: 50px;" value="1">
</div>
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: calc(100% - 100px); min-height:100px;" class="node-text-editor" id="node-input-func-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 5;"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
<div id="func-tab-module" style="display:none; min-height: calc(100% - 95px);">
<div class="form-row node-input-libs-row hide" id="node-input-libs-container-row">
<ol id="node-input-libs-container"></ol>
</div>
</div>
</div>
</script>
<script type="text/javascript">
(function () {
var invalidModuleVNames = [
'console',
'util',
'Buffer',
'Date',
'RED',
'node',
'__node__',
'context',
'flow',
'global',
'env',
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
'promisify'
]
var knownFunctionNodes = {};
RED.events.on("nodes:add", function (n) {
if (n.type === "ui-output") {
knownFunctionNodes[n.id] = n;
}
})
RED.events.on("nodes:remove", function (n) {
if (n.type === "ui-output") {
delete knownFunctionNodes[n.id];
}
})
var missingModules = [];
var missingModuleReasons = {};
RED.events.on("runtime-state", function (event) {
if (event.error === "missing-modules") {
missingModules = event.modules.map(function (m) { missingModuleReasons[m.module] = m.error; return m.module });
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
RED.editor.validateNode(knownFunctionNodes[id])
}
}
} else if (!event.text) {
missingModuleReasons = {};
missingModules = [];
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
RED.editor.validateNode(knownFunctionNodes[id])
}
}
}
RED.view.redraw();
});
var installAllowList = ['*'];
var installDenyList = [];
var modulesEnabled = true;
if (RED.settings.get('externalModules.modules.allowInstall', true) === false) {
modulesEnabled = false;
}
var settingsAllowList = RED.settings.get("externalModules.modules.allowList")
var settingsDenyList = RED.settings.get("externalModules.modules.denyList")
if (settingsAllowList || settingsDenyList) {
installAllowList = settingsAllowList;
installDenyList = settingsDenyList
}
installAllowList = RED.utils.parseModuleList(installAllowList);
installDenyList = RED.utils.parseModuleList(installDenyList);
var allLibs = [];
function moduleName(module) {
var match = /^([^@]+)@(.+)/.exec(module);
if (match) {
return [match[1], match[2]];
}
return [module, undefined];
}
function getAllUsedModules() {
var moduleSet = new Set();
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id)) {
if (knownFunctionNodes[id].libs) {
for (var i = 0, l = knownFunctionNodes[id].libs.length; i < l; i++) {
if (RED.utils.checkModuleAllowed(knownFunctionNodes[id].libs[i].module, null, installAllowList, installDenyList)) {
moduleSet.add(knownFunctionNodes[id].libs[i].module);
}
}
}
}
}
var modules = Array.from(moduleSet);
modules.sort();
return modules;
}
function prepareLibraryConfig(node) {
$(".node-input-libs-row").show();
var usedModules = getAllUsedModules();
var typedModules = usedModules.map(function (l) {
return { icon: "fa fa-cube", value: l, label: l, hasValue: false }
})
typedModules.push({
value: "_custom_", label: RED._("editor:subflow.licenseOther"), icon: "red/images/typedInput/az.svg"
})
var libList = $("#node-input-libs-container").css('min-height', '100px').css('min-width', '450px').editableList({
header: $('<div><div data-i18n="node-red:function.require.moduleName"></div><div data-i18n="node-red:function.require.importAs"></div></div>'),
addItem: function (container, i, opt) {
var parent = container.parent();
var row0 = $("<div/>").addClass("node-libs-entry").appendTo(container);
var fmoduleSpan = $("<span>").appendTo(row0);
var fmodule = $("<input/>", {
class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"),
type: "text"
}).css({
}).appendTo(fmoduleSpan).typedInput({
types: typedModules,
default: usedModules.indexOf(opt.module) > -1 ? opt.module : "_custom_"
});
if (usedModules.indexOf(opt.module) === -1) {
fmodule.typedInput('value', opt.module);
}
var moduleWarning = $('<span style="position: absolute;right:2px;top:7px; display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(fmoduleSpan);
RED.popover.tooltip(moduleWarning.find("i"), function () {
var val = fmodule.typedInput("type");
if (val === "_custom_") {
val = fmodule.val();
}
var errors = [];
if (!RED.utils.checkModuleAllowed(val, null, installAllowList, installDenyList)) {
return RED._("node-red:function.error.moduleNotAllowed", { module: val });
} else {
return RED._("node-red:function.error.moduleLoadError", { module: val, error: missingModuleReasons[val] });
}
})
var fvarSpan = $("<span>").appendTo(row0);
var fvar = $("<input/>", {
class: "node-input-libs-var red-ui-font-code",
placeholder: RED._("node-red:function.require.var"),
type: "text"
}).css({
}).appendTo(fvarSpan).val(opt.var);
var vnameWarning = $('<span style="position: absolute; right:2px;top:7px;display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(fvarSpan);
RED.popover.tooltip(vnameWarning.find("i"), function () {
var val = fvar.val();
if (invalidModuleVNames.indexOf(val) !== -1) {
return RED._("node-red:function.error.moduleNameReserved", { name: val })
} else {
return RED._("node-red:function.error.moduleNameError", { name: val })
}
})
fvar.on("change keyup paste", function (e) {
var v = $(this).val().trim();
if (v === "" || / /.test(v) || invalidModuleVNames.indexOf(v) !== -1) {
fvar.addClass("input-error");
vnameWarning.addClass("input-error");
} else {
fvar.removeClass("input-error");
vnameWarning.removeClass("input-error");
}
});
fmodule.on("change keyup paste", function (e) {
var val = $(this).typedInput("type");
if (val === "_custom_") {
val = $(this).val();
}
var varName = val.trim().replace(/^@/, "").replace(/@.*$/, "").replace(/[-_/\.].?/g, function (v) { return v[1] ? v[1].toUpperCase() : "" });
fvar.val(varName);
fvar.trigger("change");
if (RED.utils.checkModuleAllowed(val, null, installAllowList, installDenyList) && (missingModules.indexOf(val) === -1)) {
fmodule.removeClass("input-error");
moduleWarning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
moduleWarning.addClass("input-error");
}
});
if (RED.utils.checkModuleAllowed(opt.module, null, installAllowList, installDenyList) && (missingModules.indexOf(opt.module) === -1)) {
fmodule.removeClass("input-error");
moduleWarning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
moduleWarning.addClass("input-error");
}
if (opt.var) {
fvar.trigger("change");
}
},
removable: true
});
var libs = node.libs || [];
for (var i = 0, l = libs.length; i < l; i++) {
libList.editableList('addItem', libs[i])
}
}
function getLibsList() {
var _libs = [];
if (RED.settings.functionExternalModules !== false) {
var libs = $("#node-input-libs-container").editableList("items");
libs.each(function (i) {
var item = $(this);
var v = item.find(".node-input-libs-var").val();
var n = item.find(".node-input-libs-val").typedInput("type");
if (n === "_custom_") {
n = item.find(".node-input-libs-val").val();
}
if ((!v || (v === "")) ||
(!n || (n === ""))) {
return;
}
_libs.push({
var: v,
module: n
});
});
}
return _libs;
}
RED.nodes.registerType('ui-output', {
color: 'rgb( 63, 173, 181)',
category: 'ui reef',
defaults: {
name: { value: '' },
group: { type: 'ui-group', required: true },
order: { value: 0 },
width: {
value: 6,
validate: function (v) {
let valid = true;
const width = v || 0;
const currentGroup = $('#node-input-group').val() || this.group;
const groupNode = RED.nodes.node(currentGroup);
valid = !groupNode || +width <= +groupNode.width;
$('#node-input-size').toggleClass('input-error', !valid);
return valid;
},
},
height: { value: 1 },
label: { value: 'output' },
useThemeColors: { value: true },
options: {
value: [],
validate: function (v) {
const unique = new Set(v.map(function (o) { return o.value; }));
return v.length === unique.size && v.length > 1;
},
},
topic: { value: '' },
func: { value: "\nreturn msg;" },
repeat: {
value: 5,
validate(v) {
return ((v === '') || (RED.validators.number(v) && (v >= 1) && (v <= 2147483)));
},
},
repeatUnit: {
value: '1000',
required: true,
},
outputs: { value: 1 },
timeout: { value: RED.settings.functionTimeout || 0 },
noerr: {
value: 0, required: true,
validate: function (v, opt) {
if (!v) {
return true;
}
return RED._("node-red:function.error.invalid-js");
}
},
initialize: { value: "" },
finalize: { value: "" },
libs: {
value: [], validate: function (v, opt) {
if (!v) { return true; }
for (var i = 0, l = v.length; i < l; i++) {
var m = v[i];
if (!RED.utils.checkModuleAllowed(m.module, null, installAllowList, installDenyList)) {
return RED._("node-red:function.error.moduleNotAllowed", {
module: m.module
});
}
if (m.var === "" || / /.test(m.var)) {
return RED._("node-red:function.error.moduleNameError", {
name: m.var
});
}
if (missingModules.indexOf(m.module) > -1) {
return RED._("node-red:function.error.missing-module", {
module: m.module
});
}
if (invalidModuleVNames.indexOf(m.var) !== -1) {
return RED._("node-red:function.error.moduleNameError", {
name: m.var
});
}
}
return true;
}
}
},
inputs: 1,
outputs: 1,
icon: 'font-awesome/fa-toggle-on',
align: 'right',
paletteLabel: 'output',
label: function () {
return this.name || 'output';
},
labelStyle: function () {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function () {
var that = this;
if (RED.nodes.subflow(this.z)) {
$('#node-input-width').attr('type', 'text')
$('#node-input-height').attr('type', 'text')
$('div.form-row.nr-db-ui-element-sizer-row').hide()
$('div.form-row.nr-db-ui-manual-size-row').show()
} else {
$('div.form-row.nr-db-ui-element-sizer-row').show()
$('div.form-row.nr-db-ui-manual-size-row').hide()
$('#node-input-size').elementSizer({
width: '#node-input-width',
height: '#node-input-height',
group: '#node-input-group'
})
}
$('#node-input-size').elementSizer({
width: '#node-input-width',
group: '#node-input-group',
});
function getColor(idx) {
const colors = ['#009933', '#999999', '#ff6666', '#009999', '#cccc00', '#ff33cc', '#cc6600',
'#99ff66', '#660033',
];
if (idx > colors.length - 1) {
return colors[Math.floor(Math.random() * colors.length)];
}
return colors[idx];
}
const optionsList = $('#node-input-opts-container').css('min-height', '200px').editableList({
header: $('<div>').css({ 'padding-left': '32px', 'padding-right': '32px', gap: '12px', display: 'flex' }).append($.parseHTML(
"<div style='width:40%; display: inline-grid'><b>Label</b></div>"
+ "<div style='width:40%; display: inline-grid'><b>Value</b></div>"
+ "<div style='width:10%; display: inline-grid' class='node-input-option-color'><b>Color</b></div>",
)),
addItem(container, i, option) {
const row = $('<div/>').appendTo(container);
const labelField = $('<input/>', { class: 'node-input-option-label', type: 'text' }).css({ width: '40%', 'margin-left': '5px', 'margin-right': '5px' }).appendTo(row);
labelField.val(option.label || `Option ${i}`);
const valueField = $('<input/>', { class: 'node-input-option-value', type: 'text' }).css({ width: '40%', 'margin-left': '5px', 'margin-right': '5px' }).appendTo(row);
valueField.typedInput({
types: ['str', 'num', 'bool', {
value: 'func', label: 'function', icon: 'fa fa-code', hasValue: false,
}],
});
valueField.typedInput('type', option.valueType || 'str');
valueField.typedInput('value', option.value || `option_${i}`);
valueField.on('change', function (event, type, value) {
if (type === 'func' && value !== 'func') { valueField.typedInput('value', 'func'); }
validate();
});
const colofield = $('#node-input-useThemeColors').prop('checked') ? 'none' : 'inline-block';
const colorField = $('<input/>', { class: 'node-input-option-color', type: 'color' }).css({ width: '10%', 'margin-left': '5px', display: colofield }).appendTo(row);
colorField.val(option.color || getColor(i));
validate();
},
removeItem: function (data) {
validate();
},
removable: true,
sortable: true,
});
if (this.options) {
this.options.forEach(function (option, index) {
optionsList.editableList('addItem', {
index: index, label: option.label, value: option.value, valueType: option.valueType, color: option.color,
});
});
}
$('#node-input-useThemeColors').on('change', function () {
const colorFields = $('.node-input-option-color');
if ($(this)[0].checked) {
colorFields.hide();
} else {
colorFields.show();
}
});
var tabs = RED.tabs.create({
id: "func-tabs",
onchange: function (tab) {
$("#func-tabs-content").children().hide();
$("#" + tab.id).show();
let editor = $("#" + tab.id).find('.monaco-editor').first();
if (editor.length) {
if (that.editor.nodered && that.editor.type == "monaco") {
that.editor.nodered.refreshModuleLibs(getLibsList());
}
RED.tray.resize();
//auto focus editor on tab switch
if (that.editor.getDomNode() == editor[0]) {
that.editor.focus();
}
}
}
});
tabs.addTab({
id: "func-tab-config",
iconClass: "fa fa-cog",
label: 'Setup'
});
tabs.addTab({
id: "func-tab-body",
iconClass: "fa fa-sign-in",
label: 'On Message'
});
tabs.addTab({
id: "func-tab-module",
iconClass: "fa fa-cubes",
label: ' Modules'
});
tabs.activateTab("func-tab-config");
$("#node-input-outputs").spinner({
min: 0,
max: 500,
change: function (event, ui) {
var value = parseInt(this.value);
value = isNaN(value) ? 1 : value;
value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
if (value !== this.value) { $(this).spinner("value", value); }
}
});
const buildEditor = function (id, stateId, focus, value, defaultValue, extraLibs, offset) {
const editor = RED.editor.createEditor({
id: id,
mode: 'ace/mode/nrjavascript',
value: value || defaultValue || "",
stateId: stateId,
focus: true,
globals: {
msg: true,
context: true,
RED: true,
util: true,
flow: true,
global: true,
console: true,
Buffer: true,
setTimeout: true,
clearTimeout: true,
setInterval: true,
clearInterval: true
},
extraLibs: extraLibs
});
if (defaultValue && value === "") {
editor.moveCursorTo(defaultValue.split("\n").length + offset, 0);
}
editor.__stateId = stateId;
return editor;
}
this.editor = buildEditor('node-input-func-editor', this.id + "/" + "editor", true, $("#node-input-func").val(), undefined, that.libs || [], undefined, -1);
RED.library.create({
url: "functions",
type: "ui-output",
editor: this.editor,
mode: "ace/mode/nrjavascript",
fields: [
'name', 'outputs', 'timeout',
{
name: 'info',
get: function () {
return that.infoEditor.getValue();
},
set: function (v) {
that.infoEditor.setValue(v || "", -1);
}
}
],
ext: "js"
});
var expandButtonClickHandler = function (editor) {
return function (e) {
e.preventDefault();
var value = editor.getValue();
editor.saveView(`inside function-expandButtonClickHandler ${editor.__stateId}`);
var extraLibs = that.libs || [];
RED.editor.editJavaScript({
value: value,
width: "Infinity",
stateId: editor.__stateId,
mode: "ace/mode/nrjavascript",
focus: true,
cancel: function () {
setTimeout(function () {
editor.focus();
}, 250);
},
complete: function (v, cursor) {
editor.setValue(v, -1);
setTimeout(function () {
editor.restoreView();
editor.focus();
}, 250);
},
extraLibs: extraLibs
});
}
}
$("#node-function-expand-js").on("click", expandButtonClickHandler(this.editor));
RED.popover.tooltip($("#node-function-expand-js"), RED._("node-red:common.label.expand"));
if (RED.settings.functionExternalModules !== false) {
prepareLibraryConfig(that);
}
function validate() {
const that = this;
this.options = [];
const optionsList = $('#node-input-opts-container').editableList('items');
optionsList.each(function (i) {
const option = $(this);
const index = i;
const label = option.find('.node-input-option-label').val();
const valueType = option.find('.node-input-option-value').typedInput('type');
const value = option.find('.node-input-option-value').typedInput('value');
const color = option.find('.node-input-option-color').val();
that.options.push({
index: index, label: label, value: value, valueType: valueType, color: color,
});
});
const un = new Set(this.options.map(function (o) {
return o.value;
}));
if (this.options.length !== un.size) {
$('#configError').text('Values must be unique');
$('#configError').show();
} else if (this.options.length < 2) {
$('#configError').text('Configure at least two options');
$('#configError').show();
} else {
$('#configError').hide();
}
}
if (this.options) {
validate();
}
},
oneditsave: function () {
var node = this;
node.options = [];
const optionsList = $('#node-input-opts-container').editableList('items');
optionsList.each(function (i) {
const option = $(this);
const index = i;
const label = option.find('.node-input-option-label').val();
const valueType = option.find('.node-input-option-value').typedInput('type');
const value = option.find('.node-input-option-value').typedInput('value');
const color = option.find('.node-input-option-color').val();
node.options.push({
index, label, value, valueType, color,
});
});
var noerr = 0;
$("#node-input-noerr").val(0);
var disposeEditor = function (editorName, targetName, defaultValue) {
var editor = node[editorName];
var annot = editor.getSession().getAnnotations();
for (var k = 0; k < annot.length; k++) {
if (annot[k].type === "error") {
noerr += annot.length;
break;
}
}
var val = editor.getValue();
if (defaultValue) {
if (val.trim() == defaultValue.trim()) {
val = "";
}
}
editor.destroy();
delete node[editorName];
$("#" + targetName).val(val);
}
disposeEditor("editor", "node-input-func");
$("#node-input-noerr").val(noerr);
this.noerr = noerr;
node.libs = getLibsList();
},
oneditcancel: function () {
var node = this;
node.editor.destroy();
delete node.editor;
},
oneditresize: function (size) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i = 0; i < rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom")));
$("#dialog-form .node-text-editor").css("height", height + "px");
var height = size.height;
$("#node-input-func-editor").css("height", (height - 125) + "px");
this.editor.resize();
$("#node-input-libs-container").css("height", (height - 125) + "px");
}
});
})();
</script>