UNPKG

@janart19/node-red-fusebox

Version:

A collection of Fusebox-specific custom nodes for Node-RED

573 lines (489 loc) 24 kB
<script type="text/javascript"> RED.nodes.registerType("fusebox-boolean-logic", { category: 'fusebox', 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); } }, 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; // 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); // 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 initializeEditableList(); populateList(); // Define event listeners for form fields $("#mappings-container, #node-input-gateType, #node-input-outputMode").change(function () { updateTipText(); }); // 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> `)), 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: ['str'] }); 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'] }); } // 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 conditions.` : `The node will only output a message if all conditions are ${mode}.`; let tipText2; switch (gate) { case "and": tipText2 = "The node will output true if all conditions are true."; break; case "or": tipText2 = "The node will output true if at least one condition is true."; break; case "nand": tipText2 = "The node will output true if at least one condition is false."; break; case "nor": tipText2 = "The node will output true if all conditions are false."; break; case "xor": tipText2 = "The node will output true if only one condition is true."; break; case "xnor": tipText2 = "The node will output 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); } // 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.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; } .red-ui-editableList-header { background-color: #80808014; font-weight: bold; display: flex; } .red-ui-editableList-container { min-height: 50px; } .boolean-logic-div .form-row label { width: 33% !important; vertical-align: middle; } .boolean-logic-div .form-row div, .boolean-logic-div .form-row input, .boolean-logic-div .form-row select { max-width: 66% !important; } .boolean-logic-div .form-row select { width: 66% !important; } .boolean-logic-div .form-tips { max-width: 100% !important; text-align: center; } /* Editable list style */ .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% !important; } .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 !important; } /* 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% !important; } /* 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% !important; } /* 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%; } </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> 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> Type</label> <select id="node-input-gateType" name="node-input-gateType"> <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> <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> <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>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> <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>