/** * 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. **/ /** * @namespace RED.editor */ RED.editor = (function() { var editStack = []; var buildingEditDialog = false; var editing_node = null; var editing_config_node = null; var customEditTypes = {}; var editPanes = {}; var filteredEditPanes = {}; var editTrayWidthCache = {}; /** * Validate a node * @param node - the node being validated * @returns {boolean} whether the node is valid. Sets node.dirty if needed */ function validateNode(node) { var oldValue = node.valid; var oldChanged = node.changed; node.valid = true; var subflow; var isValid; var validationErrors; var hasChanged; if (node.type.indexOf("subflow:")===0) { subflow = RED.nodes.subflow(node.type.substring(8)); isValid = subflow.valid; hasChanged = subflow.changed; if (isValid === undefined) { isValid = validateNode(subflow); hasChanged = subflow.changed; } validationErrors = validateNodeProperties(node, node._def.defaults, node); node.valid = isValid && validationErrors.length === 0; node.changed = node.changed || hasChanged; node.validationErrors = validationErrors; } else if (node._def) { validationErrors = validateNodeProperties(node, node._def.defaults, node); if (node._def._creds) { validationErrors = validationErrors.concat(validateNodeProperties(node, node._def.credentials, node._def._creds)) } node.valid = (validationErrors.length === 0); node.validationErrors = validationErrors; } else if (node.type == "subflow") { var subflowNodes = RED.nodes.filterNodes({z:node.id}); for (var i=0;i 0) { var value = input.val(); if (defaults[property].hasOwnProperty("format") && defaults[property].format !== "" && input[0].nodeName === "DIV") { value = input.text(); } if (!validateNodeProperty(node, defaults, property,value)) { input.addClass("input-error"); } else { input.removeClass("input-error"); } } } /** * Called when the node's properties have changed. * Marks the node as dirty and needing a size check. * Removes any links to non-existant outputs. * @param node - the node that has been updated * @param outputMap - (optional) a map of old->new port numbers if wires should be moved * @returns {array} the links that were removed due to this update */ function updateNodeProperties(node, outputMap) { node.resize = true; node.dirty = true; node.dirtyStatus = true; var removedLinks = []; if (outputMap) { RED.nodes.eachLink(function(l) { if (l.source === node) { if (outputMap.hasOwnProperty(l.sourcePort)) { if (outputMap[l.sourcePort] === "-1") { removedLinks.push(l); } else { l.sourcePort = outputMap[l.sourcePort]; } } } }); } if (node.hasOwnProperty("__outputs")) { if (node.outputs < node.__outputs) { RED.nodes.eachLink(function(l) { if (l.source === node && l.sourcePort >= node.outputs && removedLinks.indexOf(l) === -1) { removedLinks.push(l); } }); } delete node.__outputs; } node.inputs = Math.min(1,Math.max(0,parseInt(node.inputs))); if (isNaN(node.inputs)) { node.inputs = 0; } if (node.inputs === 0) { removedLinks = removedLinks.concat(RED.nodes.filterLinks({target:node})); } for (var l=0;l").css({display:'inline-block',position:'relative'}); var selectWrap = $("
").css({position:'absolute',left:0,right:'40px'}).appendTo(outerWrap); var select = $('').appendTo(selectWrap); outerWrap.width(newWidth).height(input.height()); if (outerWrap.width() === 0) { outerWrap.width("70%"); } input.replaceWith(outerWrap); // set the style attr directly - using width() on FF causes a value of 114%... select.attr('style',"width:100%"); updateConfigNodeSelect(property,type,node[property],prefix); $('') .css({position:'absolute',right:0,top:0}) .appendTo(outerWrap); $('#'+prefix+'-lookup-'+property).on("click", function(e) { showEditConfigNodeDialog(property,type,select.find(":selected").val(),prefix); e.preventDefault(); }); var label = ""; var configNode = RED.nodes.node(node[property]); var node_def = RED.nodes.getType(type); if (configNode) { label = RED.utils.getNodeLabel(configNode,configNode.id); } input.val(label); } /** * Create a config-node button for this property * @param node - the node being edited * @param property - the name of the field * @param type - the type of the config-node */ function prepareConfigNodeButton(node,property,type,prefix) { var input = $("#"+prefix+"-"+property); input.val(node[property]); input.attr("type","hidden"); var button = $("",{id:prefix+"-edit-"+property, class:"red-ui-button"}); input.after(button); if (node[property]) { button.text(RED._("editor.configEdit")); } else { button.text(RED._("editor.configAdd")); } button.on("click", function(e) { showEditConfigNodeDialog(property,type,input.val()||"_ADD_",prefix); e.preventDefault(); }); } /** * Populate the editor dialog input field for this property * @param node - the node being edited * @param property - the name of the field * @param prefix - the prefix to use in the input element ids (node-input|node-config-input) * @param definition - the definition of the field */ function preparePropertyEditor(node,property,prefix,definition) { var input = $("#"+prefix+"-"+property); if (input.length === 0) { return; } if (input.attr('type') === "checkbox") { input.prop('checked',node[property]); } else { var val = node[property]; if (val == null) { val = ""; } if (definition !== undefined && definition[property].hasOwnProperty("format") && definition[property].format !== "" && input[0].nodeName === "DIV") { input.html(RED.text.format.getHtml(val, definition[property].format, {}, false, "en")); RED.text.format.attach(input[0], definition[property].format, {}, false, "en"); } else { input.val(val); if (input[0].nodeName === 'INPUT' || input[0].nodeName === 'TEXTAREA') { RED.text.bidi.prepareInput(input); } } } } /** * Add an on-change handler to revalidate a node field * @param node - the node being edited * @param definition - the definition of the node * @param property - the name of the field * @param prefix - the prefix to use in the input element ids (node-input|node-config-input) */ function attachPropertyChangeHandler(node,definition,property,prefix) { var input = $("#"+prefix+"-"+property); if (definition !== undefined && "format" in definition[property] && definition[property].format !== "" && input[0].nodeName === "DIV") { $("#"+prefix+"-"+property).on('change keyup', function(event) { if (!$(this).attr("skipValidation")) { validateNodeEditor(node,prefix); } }); } else { $("#"+prefix+"-"+property).on("change", function(event) { if (!$(this).attr("skipValidation")) { validateNodeEditor(node,prefix); } }); } } /** * Assign the value to each credential field * @param node * @param credDef * @param credData * @param prefix */ function populateCredentialsInputs(node, credDef, credData, prefix) { var cred; for (cred in credDef) { if (credDef.hasOwnProperty(cred)) { if (credDef[cred].type == 'password') { if (credData[cred]) { $('#' + prefix + '-' + cred).val(credData[cred]); } else if (credData['has_' + cred]) { $('#' + prefix + '-' + cred).val('__PWRD__'); } else { $('#' + prefix + '-' + cred).val(''); } } else { preparePropertyEditor(credData, cred, prefix, credDef); } attachPropertyChangeHandler(node, credDef, cred, prefix); } } } /** * Prepare all of the editor dialog fields * @param trayBody - the tray body to populate * @param nodeEditPanes - array of edit pane ids to add to the dialog * @param node - the node being edited * @param definition - the node definition * @param prefix - the prefix to use in the input element ids (node-input|node-config-input) * @param default - the id of the tab to show by default */ function prepareEditDialog(trayBody, nodeEditPanes, node, definition, prefix, defaultTab, done) { var finishedBuilding = false; var completePrepare = function() { var editorTabEl = $('
    ').appendTo(trayBody); var editorContent = $('
    ').appendTo(trayBody); var editorTabs = RED.tabs.create({ element:editorTabEl, onchange:function(tab) { editorContent.children().hide(); tab.content.show(); if (tab.onchange) { tab.onchange.call(tab); } if (finishedBuilding) { RED.tray.resize(); } }, collapsible: true, menu: false }); var activeEditPanes = []; nodeEditPanes = nodeEditPanes.slice(); for (var i in filteredEditPanes) { if (filteredEditPanes.hasOwnProperty(i)) { if (filteredEditPanes[i](node)) { nodeEditPanes.push(i); } } } nodeEditPanes.forEach(function(id) { try { var editPaneDefinition = editPanes[id]; if (editPaneDefinition) { if (typeof editPaneDefinition === 'function') { editPaneDefinition = editPaneDefinition.call(editPaneDefinition); } var editPaneContent = $('
    ', {class:"red-ui-tray-content"}).appendTo(editorContent).hide(); editPaneDefinition.create.call(editPaneDefinition,editPaneContent,node); var editTab = { id: id, label: editPaneDefinition.label, name: editPaneDefinition.name, iconClass: editPaneDefinition.iconClass, content: editPaneContent, onchange: function() { if (editPaneDefinition.show) { editPaneDefinition.show.call(editPaneDefinition,node) } } } editorTabs.addTab(editTab); activeEditPanes.push(editPaneDefinition); } else { console.warn("Unregisted edit pane:",id) } } catch(err) { console.log(id,err); } }); for (var d in definition.defaults) { if (definition.defaults.hasOwnProperty(d)) { if (definition.defaults[d].type) { if (!definition.defaults[d]._type.array) { var configTypeDef = RED.nodes.getType(definition.defaults[d].type); if (configTypeDef && configTypeDef.category === 'config') { if (configTypeDef.exclusive) { prepareConfigNodeButton(node,d,definition.defaults[d].type,prefix); } else { prepareConfigNodeSelect(node,d,definition.defaults[d].type,prefix); } } else { console.log("Unknown type:", definition.defaults[d].type); preparePropertyEditor(node,d,prefix,definition.defaults); } } } else { preparePropertyEditor(node,d,prefix,definition.defaults); } attachPropertyChangeHandler(node,definition.defaults,d,prefix); } } if (!/^subflow:/.test(definition.type)) { populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); } if (definition.oneditprepare) { try { definition.oneditprepare.call(node); } catch(err) { console.log("oneditprepare",node.id,node.type,err.toString()); console.log(err.stack); } } // Now invoke any change handlers added to the fields - passing true // to prevent full node validation from being triggered each time for (var d in definition.defaults) { if (definition.defaults.hasOwnProperty(d)) { var el = $("#"+prefix+"-"+d); el.attr("skipValidation", true); if (el.data("noderedTypedInput") !== undefined) { el.trigger("change",[el.typedInput('type'),el.typedInput('value')]); } else { el.trigger("change"); } el.removeAttr("skipValidation"); } } if (definition.credentials) { for (d in definition.credentials) { if (definition.credentials.hasOwnProperty(d)) { var el = $("#"+prefix+"-"+d); el.attr("skipValidation", true); if (el.data("noderedTypedInput") !== undefined) { el.trigger("change",[el.typedInput('type'),el.typedInput('value')]); } else { el.trigger("change"); } el.removeAttr("skipValidation"); } } } validateNodeEditor(node,prefix); finishedBuilding = true; if (defaultTab) { editorTabs.activateTab(defaultTab); } if (done) { done(activeEditPanes); } } if (definition.credentials || /^subflow:/.test(definition.type) || node.type === "group" || node.type === "tab") { if (node.credentials) { populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); completePrepare(); } else { var nodeType = node.type; if (/^subflow:/.test(nodeType)) { nodeType = "subflow" } getNodeCredentials(nodeType, node.id, function(data) { if (data) { node.credentials = data; node.credentials._ = $.extend(true,{},data); } completePrepare(); }); } } else { completePrepare(); } } function getEditStackTitle() { var label; for (var i=editStack.length-1;i').appendTo(container); dialogForm.html($("script[data-template-name='"+type+"']").html()); ns = ns||"node-red"; dialogForm.find('[data-i18n]').each(function() { var current = $(this).attr("data-i18n"); var keys = current.split(";"); for (var i=0;i