@janart19/node-red-fusebox
Version:
A comprehensive collection of custom nodes for interfacing with Fusebox automation controllers - data streams, energy management, and utilities
817 lines (707 loc) • 33.2 kB
HTML
<script type="text/javascript">
RED.nodes.registerType("fusebox-boolean-logic", {
category: "fusebox utils",
color: "#FFDDCC",
// Define the default values for the node configuration
defaults: {
name: { value: "" },
outputTopic: { value: "" },
gateType: { value: "and", required: true },
outputMode: {
value: "",
validate: function (v) {
return ["all", "true", "false"].includes(v);
}
},
formatMode: {
value: "passthrough",
validate: function (v) {
return ["passthrough", "comprehensive"].includes(v);
}
},
rules: { value: [{ topic: "", property: "payload", propertyType: "msg", t: "eq", v: "" }] }
},
// Define the inputs and outputs of the node
inputs: 1,
outputs: 1,
icon: "font-awesome/fa-question-circle",
label: function () {
return this.name || this.gateType + " gate";
},
paletteLabel: function () {
return "logic gate";
},
// Update form fields
oneditprepare: function () {
const node = this;
let _controllers = {};
// Populate form with node values
$("#node-input-name").val(node.name);
$("#node-input-outputTopic").val(node.outputTopic);
$("#node-input-gateType").val(node.gateType);
$("#node-input-outputMode").val(node.outputMode);
$("#node-input-formatMode").val(node.formatMode);
// Adding this type for typed input
const previousValueType = { value: "prev", label: "previous value", hasValue: false };
const operators = [
{ v: "eq", t: "==" },
{ v: "neq", t: "!=" },
{ v: "lt", t: "<" },
{ v: "lte", t: "<=" },
{ v: "gt", t: ">" },
{ v: "gte", t: ">=" },
{ v: "btwn", t: "is between" },
{ v: "cont", t: "contains" },
{ v: "regex", t: "match regex" },
{ v: "true", t: "is true" },
{ v: "false", t: "is false" },
{ v: "null", t: "is null" },
{ v: "nnull", t: "is not null" }
];
const andLabel = "and";
const caseLabel = "ignore case";
// Initialize the form fields
queryControllerConfig();
initializeEditableList();
populateList();
// Define event listeners for form fields
$("#mappings-container, #node-input-gateType, #node-input-outputMode").change(function () {
updateTipText();
});
$("#node-input-formatMode, #node-input-outputMode").change(function () {
checkFields();
});
// Query topics defined in read and write nodes
function queryControllerConfig() {
$.getJSON(`fusebox/controller-config`, function (data) {
_controllers = data;
}).fail(function () {
console.error("Failed to get topics!");
_controllers = {};
});
}
// Initialize the EditableList widget
function initializeEditableList() {
$("#mappings-container").editableList({
removable: true,
sortable: true,
header: $("<div>").append(
$.parseHTML(`
<div style="width: 22px"></div>
<div class="boolean-logic-list" style="text-align: center; white-space: normal; flex: 1">
<div class="topic-row">Topic</div>
<div class="property-row">Property</div>
<div class="operator-row">Operator</div>
<div class="value-row">Value</div>
</div>
<div style="width: 28px"></div>
`)
),
buttons: [
{
label: "delete all",
icon: "fa fa-trash",
title: "delete all entries",
click: function (e) {
$("#mappings-container").editableList("empty");
updateTipText();
}
}
],
addItem: function (container, i, row) {
addRowElements(container, row);
const elements = getRowElements(container);
formatRow(elements, row);
attachRowEvents(elements, row);
// Show elements after initialization due to weird width issues
elements.valueDiv.show();
updateTipText();
},
removeItem: function (data) {
updateTipText();
}
});
}
// Append the row to the container
function addRowElements(container, row) {
const rowHtml = `
<div class="boolean-logic-list">
<div class="topic-row">
<input class="node-input-rule-topic" type="text" placeholder="Topic">
</div>
<div class="property-row">
<input class="node-input-rule-property" type="text">
</div>
<div class="operator-row">
<select class="node-input-rule-select" style="text-align: center;">
${Object.keys(operators)
.map((d) => `<option value="${operators[d].v}">${operators[d].t}</option>`)
.join("")}
</select>
</div>
<div class="value-row">
<input class="node-input-rule-value" type="text" placeholder="value">
<div class="value-row-btwn">
<input class="node-input-rule-btwn-value" type="text">
<span class="node-input-rule-btwn-label"> ${andLabel} </span>
<input class="node-input-rule-btwn-value2" type="text">
</div>
<div class="value-row-regex">
<input class="node-input-rule-regex" type="text">
<div class="checkbox-container">
<input class="node-input-rule-case" type="checkbox">
<label class="node-input-rule-case-label" for="node-input-rule-case">${caseLabel}</label>
</div>
</div>
</div>
</div>
`;
container.append(rowHtml);
}
// Format the newly created / intialized row's elements
function formatRow(elements, row) {
elements.topicField.typedInput({
default: "str",
types: [
{
value: "str",
icon: "red/images/typedInput/az.svg",
autoComplete: function (val) {
const term = val.toLowerCase();
const selection = getTopics();
return selection
.filter((topic) => topic.label.toLowerCase().includes(term) || topic.key.toLowerCase().includes(term))
.map((topic) => ({
value: topic.topic,
label: topic.label
}));
}
}
]
});
elements.propertyField.typedInput({ default: row.propertyType || "msg", types: ["msg", "flow", "global"] });
elements.valueField.typedInput({ default: "str", types: ["msg", "flow", "global", "str", "num", previousValueType] });
elements.btwnValueField.typedInput({ default: "num", types: ["msg", "flow", "global", "num", previousValueType] });
elements.btwnValue2Field.typedInput({ default: "num", types: ["msg", "flow", "global", "num", previousValueType] });
elements.regexField.typedInput({ default: "re", types: ["re"] });
}
// Return a list of topics from the controller configuration
// Structure: [ { key: "ABC", member: 1, topic: test/1, label: ... } ]
function getTopics() {
const result = [];
const controllers = _controllers?.controllers || [];
for (const controller of controllers) {
result.push(...controller.formattedTopics);
}
return result;
}
// Attach events to the row elements
function attachRowEvents(elements = {}, row) {
const { topicField, propertyField, valueField, selectField } = elements;
const { btwnDiv, btwnValueField, btwnValue2Field, btwnValueLabel } = elements;
const { regexDiv, regexField, caseSensitive, caseSensitiveLabel } = elements;
selectField.on("change", function () {
const selected = selectField.val() || "eq";
switch (selected) {
case "btwn": {
valueField.typedInput("hide");
btwnDiv.show();
regexDiv.hide();
break;
}
case "regex": {
valueField.typedInput("hide");
btwnDiv.hide();
regexDiv.show();
break;
}
case "true":
case "false":
case "null":
case "nnull": {
valueField.typedInput("hide");
btwnDiv.hide();
regexDiv.hide();
break;
}
default: {
valueField.typedInput("show");
btwnDiv.hide();
regexDiv.hide();
break;
}
}
});
// Format the topic field based on the property type
propertyField.on("change", function (e, type) {
topicField.typedInput("disable", type !== "msg");
if (type !== "msg") {
topicField.typedInput("value", "");
}
});
// If t isn't defined, set equal as default value
if (!row.hasOwnProperty("t")) row.t = "eq";
// Save existing values to fields
selectField.val(row.t);
topicField.typedInput("value", row.topic);
propertyField.typedInput("value", row.property || "payload");
propertyField.typedInput("type", row.propertyType || "msg");
if (row.t == "btwn") {
btwnValueField.typedInput("value", row.v);
btwnValueField.typedInput("type", row.vt || "num");
btwnValue2Field.typedInput("value", row.v2);
btwnValue2Field.typedInput("type", row.v2t || "num");
} else if (typeof row.v != "undefined") {
if (row.t == "regex") {
regexField.typedInput("value", row.v);
} else {
valueField.typedInput("value", row.v);
valueField.typedInput("type", row.vt || "str");
}
}
caseSensitive.prop("checked", row.case ? true : false);
selectField.change();
}
// Get references to the created elements
function getRowElements(container) {
return {
topicField: container.find(".node-input-rule-topic"),
propertyField: container.find(".node-input-rule-property"),
valueField: container.find(".node-input-rule-value"),
selectField: container.find(".node-input-rule-select"),
propertyDiv: container.find(".property-row"),
operatorDiv: container.find(".opertator-row"),
valueDiv: container.find(".value-row"),
btwnDiv: container.find(".value-row-btwn"),
btwnValueField: container.find(".node-input-rule-btwn-value"),
btwnValue2Field: container.find(".node-input-rule-btwn-value2"),
btwnValueLabel: container.find(".node-input-rule-btwn-label"),
regexDiv: container.find(".value-row-regex"),
regexField: container.find(".node-input-rule-regex"),
caseSensitive: container.find(".node-input-rule-case"),
caseSensitiveLabel: container.find(".node-input-rule-case-label")
};
}
// Populate the form with the node configuration
function populateList() {
for (let i = 0; i < node.rules.length; i++) {
const rule = node.rules[i];
$("#mappings-container").editableList("addItem", rule);
}
}
// Update tip text based on current settings
function updateTipText() {
const rows = getListElements();
const gate = $("#node-input-gateType").val();
const mode = $("#node-input-outputMode").val();
const tipText1 = `Using ${rows.length} rows to evaluate boolean logic.`;
const tipText3 =
mode === "all"
? `The node will always output a message, regardless of the boolean result.`
: `The node will only output a message if the boolean result is true.`;
let tipText2;
switch (gate) {
case "and":
tipText2 = "Boolean gate AND: true if all conditions are true.";
break;
case "or":
tipText2 = "Boolean gate OR: true if at least one condition is true.";
break;
case "nand":
tipText2 = "Boolean gate NAND: true if at least one condition is false.";
break;
case "nor":
tipText2 = "Boolean gate NOR: true if all conditions are false.";
break;
case "xor":
tipText2 = "Boolean gate XOR: true if only one condition is true.";
break;
case "xnor":
tipText2 = "Boolean gate XNOR: true if all conditions are the same.";
break;
}
$("#node-input-tip-text-1").text(tipText1);
$("#node-input-tip-text-2").text(tipText2);
$("#node-input-tip-text-3").text(tipText3);
// Highlight the current gate type in the truth table
$(".truth-table th").removeClass("highlighted-gate");
if (gate) {
$(".truth-table th").each(function () {
if ($(this).text().toLowerCase() === gate.toLowerCase()) {
$(this).addClass("highlighted-gate");
}
});
}
}
// Check the fields, disable or enable them if need be
function checkFields() {
const mode = $("#node-input-outputMode").val();
const format = $("#node-input-formatMode").val();
if (mode === "all") {
$("#node-input-formatMode").val("comprehensive");
$("#node-input-formatMode").prop("disabled", true);
} else {
$("#node-input-formatMode").prop("disabled", false);
}
}
// Iterate over each mapping row and store the elements
function getListElements() {
const result = [];
$("#mappings-container")
.editableList("items")
.each(function () {
const container = $(this);
const elements = getRowElements(container);
result.push(elements);
});
return result;
}
},
// Save the values from the UI to the node configuration
oneditsave: function () {
const node = this;
node.name = $("#node-input-name").val();
node.outputTopic = $("#node-input-outputTopic").val();
node.gateType = $("#node-input-gateType").val();
node.outputMode = $("#node-input-outputMode").val();
node.formatMode = $("#node-input-formatMode").val();
node.rules = getMappings();
// Iterate over each mapping row and store the values
function getMappings() {
const mappings = [];
$("#mappings-container")
.editableList("items")
.each(function () {
const container = $(this);
// Get the selected operator
const type = container.find("select").val();
// format rule
const r = { t: type };
if (!(type === "true" || type === "false" || type === "null" || type === "nnull")) {
if (type === "btwn") {
r.v = container.find(".node-input-rule-btwn-value").typedInput("value");
r.vt = container.find(".node-input-rule-btwn-value").typedInput("type");
r.v2 = container.find(".node-input-rule-btwn-value2").typedInput("value");
r.v2t = container.find(".node-input-rule-btwn-value2").typedInput("type");
} else {
r.v = container.find(".node-input-rule-value").typedInput("value");
r.vt = container.find(".node-input-rule-value").typedInput("type");
}
if (type === "regex") {
r.v = container.find(".node-input-rule-regex").typedInput("value");
r.case = container.find(".node-input-rule-case").prop("checked");
}
}
r.propertyType = container.find(".node-input-rule-property").typedInput("type");
r.property = container.find(".node-input-rule-property").typedInput("value");
if (r.propertyType === "msg") {
// if it's a message, store the message topic
r.topic = container.find(".node-input-rule-topic").typedInput("value");
}
mappings.push(r);
});
return mappings;
}
}
});
</script>
<!-- Define style for the form fields -->
<style type="text/css">
.boolean-logic-div .form-row {
margin-bottom: 10px;
}
.boolean-logic-div .form-row label {
width: 33% ;
vertical-align: middle;
}
.boolean-logic-div .form-row div,
.boolean-logic-div .form-row input,
.boolean-logic-div .form-row select {
max-width: 66% ;
}
.boolean-logic-div .form-row select {
width: 66% ;
}
.boolean-logic-div .form-tips {
max-width: 100% ;
text-align: center;
}
/* Editable list style */
.boolean-logic-div .red-ui-editableList-header {
background-color: #80808014;
font-weight: bold;
display: flex;
}
.boolean-logic-div .red-ui-editableList-container {
min-height: 50px;
}
.boolean-logic-div .red-ui-editableList {
margin-bottom: 10px;
min-width: 600px;
}
.boolean-logic-list {
overflow: hidden;
white-space: nowrap;
display: flex;
align-items: center;
}
/* By default, fields fill their respective columns widths */
.boolean-logic-list select,
.boolean-logic-list .red-ui-typedInput-container {
width: 100% ;
}
.boolean-logic-list select,
.boolean-logic-list .red-ui-typedInput-input,
.boolean-logic-list .node-input-rule-btwn-label,
.boolean-logic-list .red-ui-typedInput-type-label {
font-size: 12px ;
}
.boolean-logic-div .red-ui-autoComplete-container {
width: fit-content ;
}
/* List is split into different columns: topic, arrow, property, operator, value */
.boolean-logic-list .topic-row {
min-width: 100px;
width: 20%;
}
.boolean-logic-list .property-row {
min-width: 100px;
width: 27%;
}
.boolean-logic-list .operator-row {
min-width: 100px;
width: 18%;
}
.boolean-logic-list .value-row {
min-width: 200px;
width: 35%;
}
/* Special style for specific operators */
.value-row-btwn .red-ui-typedInput-container {
width: 45% ;
}
/* Special style for specific operators */
.value-row-regex {
width: 100%;
display: inline-flex;
margin-bottom: -4px;
}
/* Special style for specific operators */
.value-row-regex .red-ui-typedInput-container {
width: 70% ;
}
/* Special style for specific operators */
.value-row-regex .node-input-rule-case {
min-width: 16px;
min-height: 16px;
}
/* Special style for specific operators */
.value-row-regex .node-input-rule-case-label {
font-size: 10px;
line-height: 12px;
text-wrap: wrap;
color: #666;
}
/* Special style for specific operators */
.value-row-regex .checkbox-container {
text-align: center;
min-width: 35px;
width: 30%;
}
/* Truth table styling */
.truth-table-container {
margin: 15px 0;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.truth-table-container h4 {
margin: 0 0 10px 0;
color: #333;
font-size: 14px;
text-align: center;
}
.truth-table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
font-family: monospace;
}
.truth-table th,
.truth-table td {
border: 1px solid #ccc;
padding: 2px 4px;
text-align: center;
}
.truth-table th {
background-color: #e6e6e6;
font-weight: bold;
font-size: 11px;
}
.truth-table td {
background-color: white;
}
.truth-table tr:nth-child(even) td {
background-color: #f5f5f5;
}
.truth-table th.highlighted-gate {
background-color: #4caf50;
color: white;
}
</style>
<!-- Form fields are defined in the template below -->
<script type="text/html" data-template-name="fusebox-boolean-logic">
<div class="boolean-logic-div">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name" />
</div>
<div class="form-row">
<label for="node-input-outputTopic"><i class="fa fa-comment"></i> Output topic</label>
<input type="text" id="node-input-outputTopic" placeholder="Topic" />
</div>
<div class="form-row">
<label for="node-input-gateType"><i class="fa fa-cog"></i> Boolean gate</label>
<select id="node-input-gateType" name="node-input-gateType">
<option value="" disabled>Select gate type...</option>
<option value="and">AND</option>
<option value="or">OR</option>
<option value="nand">NAND</option>
<option value="nor">NOR</option>
<option value="xor">XOR</option>
<option value="xnor">XNOR</option>
</select>
</div>
<div class="form-row">
<label for="node-input-outputMode"><i class="fa fa-sign-out"></i> Output mode</label>
<select id="node-input-outputMode">
<option value="" disabled selected>Select output mode...</option>
<option value="all">Output data every time</option>
<option value="true">Output data only when true</option>
<option value="false">Output data only when false</option>
</select>
</div>
<div class="form-row">
<label for="node-input-formatMode"><i class="fa fa-code"></i> Output format</label>
<select id="node-input-formatMode">
<option value="" disabled selected>Select output format...</option>
<option value="passthrough">Passthrough (output original input message)</option>
<option value="comprehensive">Comprehensive (output both input message and boolean result)</option>
</select>
</div>
<ol id="mappings-container"></ol>
<div class="form-tips" id="node-input-tip-text-1"></div>
<div class="form-tips" id="node-input-tip-text-2"></div>
<div class="form-tips" id="node-input-tip-text-3"></div>
<!-- Truth Table -->
<div class="truth-table-container">
<h4>Truth table</h4>
<table class="truth-table">
<thead>
<tr>
<th>A</th>
<th>B</th>
<th>AND</th>
<th>OR</th>
<th>NAND</th>
<th>NOR</th>
<th>XOR</th>
<th>XNOR</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td>0</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
</div>
<br />
</div>
</script>
<!-- Define node description -->
<script type="text/html" data-help-name="fusebox-boolean-logic">
<p>Perform boolean logic operations, according to user-defined rules.</p>
<p>Each rule is defined by a topic, a property, an operator, and a value.</p>
<p>The node will evaluate the message according to the rules and output a boolean value.</p>
<h3>Parameters</h3>
<dl class="message-properties">
<dt class="optional">name <span class="property-type">string</span></dt>
<dd>User-friendly name for the node.</dd>
<dt class="optional">topic <span class="property-type">string</span></dt>
<dd>The topic of the output message to distinguish it from other messages.</dd>
<dt>gateType <span class="property-type">string</span></dt>
<dd>The type of logic gate to use. Options include: <code>and</code>, <code>or</code>, <code>nand</code>, <code>nor</code>, <code>xor</code>, and <code>xnor</code>.</dd>
<dt>outputMode <span class="property-type">string</span></dt>
<dd>
Defines the output mode of the node. Options include: <code>all</code> (output data every time), <code>true</code> (output data only when true), and
<code>false</code> (output data only when false).
</dd>
<dt>formatMode <span class="property-type">string</span></dt>
<dd>Defines the output format of the node. Options include: <code>passthrough</code> (keep input message) and <code>result</code> (output the boolean result).</dd>
<dt>mappings <span class="property-type">object</span></dt>
<dd>Each row specifies a boolean logic rule. Add a new rule by clicking on the "add" button below the list.</dd>
</dl>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>topic <span class="property-type">string</span></dt>
<dd>The topic of the message. Required if parameter is set to 'msg'.</dd>
<dt>payload <span class="property-type">string | number | object | bool</span></dt>
<dd>The property to evaluate as part of the boolean logic. Allow inputs from 'msg', 'flow', and 'global' types.</dd>
</dl>
<p>Below is an example of the input message object.</p>
<p>
<code style="white-space: pre-line;"> { "topic": "my-topic-1", "payload": 1 } </code>
</p>
<h3>Output</h3>
<p>In the <code>passthrough</code> format mode, the output message will be exactly the same as the input message.</p>
<p>In the <code>result</code> format mode, the output message will only contain the object with the properties seen below:</p>
<dl class="message-properties">
<dt>payload <span class="property-type">boolean</span></dt>
<dd><code>true</code> if the logic gate meets all the conditions, <code>false</code> otherwise.</dd>
<dt class="optional">topic <span class="property-type">string</span></dt>
<dd>The output topic, if defined.</dd>
<dt>trigger <span class="property-type">obj</span></dt>
<dd>Includes info about the message that triggered the node, e.g. input message <code>payload</code> or <code>topic</code>.</dd>
</dl>
<h3>Additional details</h3>
<p>Any other incoming <code>msg</code> properties will be preserved.</p>
</script>