(function() {
    var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;

    if (isIE11) {
        // IE11 DOMTokenList.toggle does not support the two-argument variety
        window.DOMTokenList.prototype.toggle = function(cl,bo) {
            if (arguments.length === 1) {
                bo = !this.contains(cl);
            }
            this[!!bo?"add":"remove"](cl);
        }

        // IE11 does not provide classList on SVGElements
        if (! ("classList" in SVGElement.prototype)) {
            Object.defineProperty(SVGElement.prototype, 'classList', Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'classList'));
        }

        // IE11 does not provide children on SVGElements
        if (! ("children" in SVGElement.prototype)) {
            Object.defineProperty(SVGElement.prototype, 'children', Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'children'));
        }

        Array.from = function() {
            if (arguments.length > 1) {
                throw new Error("Node-RED's IE11 Array.from polyfill doesn't support multiple arguments");
            }
            var arrayLike = arguments[0]
            var result = [];
            if (arrayLike.forEach) {
                arrayLike.forEach(function(i) {
                    result.push(i);
                })
            } else {
                for (var i=0;i<arrayLike.length;i++) {
                    result.push(arrayList[i]);
                }
            }
            return result;
        }

        if (new Set([0]).size === 0) {
            // IE does not support passing an iterable to Set constructor
            var _Set = Set;
            /*global Set:true */
            Set = function Set(iterable) {
                var set = new _Set();
                if (iterable) {
                    iterable.forEach(set.add, set);
                }
                return set;
            };
            Set.prototype = _Set.prototype;
            Set.prototype.constructor = Set;
        }
    }
})();
;/**
 * 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.
 **/


/**
 * Trigger enabled/disabled events when element.prop("disabled",false/true) is
 * called.
 * Used by RED.popover to hide a popover when the trigger element is disabled
 * as a disabled element doesn't emit mouseleave
 */
jQuery.propHooks.disabled = {
    set: function (element, value) {
        if (element.disabled !== value) {
            element.disabled = value;
            if (value) {
                $(element).trigger('disabled');
            } else {
                $(element).trigger('enabled');
            }
        }
    }
};
;/**
 * 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 RED = (function() {


    function loadPluginList() {
        loader.reportProgress(RED._("event.loadPlugins"), 10)
        $.ajax({
            headers: {
                "Accept":"application/json"
            },
            cache: false,
            url: 'plugins',
            success: function(data) {
                RED.plugins.setPluginList(data);
                loader.reportProgress(RED._("event.loadPlugins"), 13)
                RED.i18n.loadPluginCatalogs(function() {
                    loadPlugins(function() {
                        loadNodeList();
                    });
                });
            }
        });
    }
    function loadPlugins(done) {
        loader.reportProgress(RED._("event.loadPlugins",{count:""}), 17)
        var lang = localStorage.getItem("editor-language")||RED.i18n.detectLanguage();

        $.ajax({
            headers: {
                "Accept":"text/html",
                "Accept-Language": lang
            },
            cache: false,
            url: 'plugins',
            success: function(data) {
                var configs = data.trim().split(/(?=<!-- --- \[red-plugin:\S+\] --- -->)/);
                var totalCount = configs.length;
                var stepConfig = function() {
                    // loader.reportProgress(RED._("event.loadNodes",{count:(totalCount-configs.length)+"/"+totalCount}), 30 + ((totalCount-configs.length)/totalCount)*40 )
                    if (configs.length === 0) {
                        done();
                    } else {
                        var config = configs.shift();
                        appendPluginConfig(config,stepConfig);
                    }
                }
                stepConfig();
            }
        });
    }

    function appendConfig(config, moduleIdMatch, targetContainer, done) {
        done = done || function(){};
        var moduleId;
        if (moduleIdMatch) {
            moduleId = moduleIdMatch[1];
            RED._loadingModule = moduleId;
        } else {
            moduleId = "unknown";
        }
        try {
            var hasDeferred = false;
            var nodeConfigEls = $("<div>"+config+"</div>");
            var scripts = nodeConfigEls.find("script");
            var scriptCount = scripts.length;
            scripts.each(function(i,el) {
                var srcUrl = $(el).attr('src');
                if (srcUrl && !/^\s*(https?:|\/|\.)/.test(srcUrl)) {
                    $(el).remove();
                    var newScript = document.createElement("script");
                    newScript.onload = function() {
                        scriptCount--;
                        if (scriptCount === 0) {
                            $(targetContainer).append(nodeConfigEls);
                            delete RED._loadingModule;
                            done()
                        }
                    }
                    if ($(el).attr('type') === "module") {
                        newScript.type = "module";
                    }
                    $(targetContainer).append(newScript);
                    newScript.src = RED.settings.apiRootUrl+srcUrl;
                    hasDeferred = true;
                } else {
                    if (/\/ace.js$/.test(srcUrl) || /\/ext-language_tools.js$/.test(srcUrl)) {
                        // Block any attempts to load ace.js from a CDN - this will
                        // break the version of ace included in the editor.
                        // At the time of commit, the contrib-python nodes did this.
                        // This is a crude fix until the python nodes are fixed.
                        console.warn("Blocked attempt to load",srcUrl,"by",moduleId)
                        $(el).remove();
                    }
                    scriptCount--;
                }
            })
            if (!hasDeferred) {
                $(targetContainer).append(nodeConfigEls);
                delete RED._loadingModule;
                done();
            }
        } catch(err) {
            RED.notify(RED._("notification.errors.failedToAppendNode",{module:moduleId, error:err.toString()}),{
                type: "error",
                timeout: 10000
            });
            console.log("["+moduleId+"] "+err.toString());
            delete RED._loadingModule;
            done();
        }
    }
    function appendPluginConfig(pluginConfig,done) {
        appendConfig(
            pluginConfig,
            /<!-- --- \[red-plugin:(\S+)\] --- -->/.exec(pluginConfig.trim()),
            "#red-ui-editor-plugin-configs",
            done
        );
    }

    function appendNodeConfig(nodeConfig,done) {
        appendConfig(
            nodeConfig,
            /<!-- --- \[red-module:(\S+)\] --- -->/.exec(nodeConfig.trim()),
            "#red-ui-editor-node-configs",
            done
        );
    }

    function loadNodeList() {
        loader.reportProgress(RED._("event.loadPalette"), 20)
        $.ajax({
            headers: {
                "Accept":"application/json"
            },
            cache: false,
            url: 'nodes',
            success: function(data) {
                RED.nodes.setNodeList(data);
                loader.reportProgress(RED._("event.loadNodeCatalogs"), 25)
                RED.i18n.loadNodeCatalogs(function() {
                    loadIconList(loadNodes);
                });
            }
        });
    }

    function loadIconList(done) {
        $.ajax({
            headers: {
                "Accept":"application/json"
            },
            cache: false,
            url: 'icons',
            success: function(data) {
                RED.nodes.setIconSets(data);
                if (done) {
                    done();
                }
            }
        });
    }

    function loadNodes() {
        loader.reportProgress(RED._("event.loadNodes",{count:""}), 30)
        var lang = localStorage.getItem("editor-language")||RED.i18n.detectLanguage();

        $.ajax({
            headers: {
                "Accept":"text/html",
                "Accept-Language": lang
            },
            cache: false,
            url: 'nodes',
            success: function(data) {
                var configs = data.trim().split(/(?=<!-- --- \[red-module:\S+\] --- -->)/);
                var totalCount = configs.length;

                var stepConfig = function() {
                    loader.reportProgress(RED._("event.loadNodes",{count:(totalCount-configs.length)+"/"+totalCount}), 30 + ((totalCount-configs.length)/totalCount)*40 )

                    if (configs.length === 0) {
                        $("#red-ui-editor").i18n();
                        $("#red-ui-palette > .red-ui-palette-spinner").hide();
                        $(".red-ui-palette-scroll").removeClass("hide");
                        $("#red-ui-palette-search").removeClass("hide");
                        if (RED.settings.theme("projects.enabled",false)) {
                            RED.projects.refresh(function(activeProject) {
                                loadFlows(function() {
                                    RED.sidebar.info.refresh()
                                    var showProjectWelcome = false;
                                    if (!activeProject) {
                                        // Projects enabled but no active project
                                        RED.menu.setDisabled('menu-item-projects-open',true);
                                        RED.menu.setDisabled('menu-item-projects-settings',true);
                                        if (activeProject === false) {
                                            // User previously decline the migration to projects.
                                        } else { // null/undefined
                                            showProjectWelcome = true;
                                        }
                                    }
                                    completeLoad(showProjectWelcome);
                                });
                            });
                        } else {
                            loadFlows(function() {
                                // Projects disabled by the user
                                RED.sidebar.info.refresh()
                                completeLoad();
                            });
                        }
                    } else {
                        var config = configs.shift();
                        appendNodeConfig(config,stepConfig);
                    }
                }
                stepConfig();
            }
        });
    }

    function loadFlows(done) {
        loader.reportProgress(RED._("event.loadFlows"),80 )
        $.ajax({
            headers: {
                "Accept":"application/json",
            },
            cache: false,
            url: 'flows',
            success: function(nodes) {
                if (nodes) {
                    var currentHash = window.location.hash;
                    RED.nodes.version(nodes.rev);
                    loader.reportProgress(RED._("event.importFlows"),90 )
                    try {
                        RED.nodes.import(nodes.flows);
                        RED.nodes.dirty(false);
                        RED.view.redraw(true);
                        if (/^#(flow|node|group)\/.+$/.test(currentHash)) {
                            const hashParts = currentHash.split('/')
                            const showEditDialog = hashParts.length > 2 && hashParts[2] === 'edit'
                            if (hashParts[0] === '#flow') {
                                RED.workspaces.show(hashParts[1], true);
                                if (showEditDialog) {
                                    RED.workspaces.edit()
                                }
                            } else if (hashParts[0] === '#node') {
                                const nodeToShow = RED.nodes.node(hashParts[1])
                                if (nodeToShow) {
                                    setTimeout(() => {
                                        RED.view.reveal(nodeToShow.id)
                                        window.location.hash = currentHash
                                        RED.view.select(nodeToShow.id)
                                        if (showEditDialog) {
                                            RED.editor.edit(nodeToShow)
                                        }
                                    }, 50)
                                }
                            } else if (hashParts[0] === '#group') {
                                const nodeToShow = RED.nodes.group(hashParts[1])
                                if (nodeToShow) {
                                    RED.view.reveal(nodeToShow.id)
                                    window.location.hash = currentHash
                                    RED.view.select(nodeToShow.id)
                                    if (showEditDialog) {
                                        RED.editor.editGroup(nodeToShow)
                                    }
                                }
                            }
                        }
                        if (RED.workspaces.count() > 0) {
                            const hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}");
                            const workspaces = RED.nodes.getWorkspaceOrder();
                            if (RED.workspaces.active() === 0) {
                                for (let index = 0; index < workspaces.length; index++) {
                                    const ws = workspaces[index];
                                    if (!hiddenTabs[ws]) {
                                        RED.workspaces.show(ws);
                                        break;
                                    }
                                }
                            }
                            if (RED.workspaces.active() === 0) {
                                RED.workspaces.show(workspaces[0]);
                            }
                        }
                        RED.events.emit('flows:loaded')
                    } catch(err) {
                        console.warn(err);
                        RED.notify(
                            RED._("event.importError", {message: err.message}),
                            {
                                fixed: true,
                                type: 'error'
                            }
                        );
                    }
                }
                done();
            }
        });
    }

    function completeLoad(showProjectWelcome) {
        var persistentNotifications = {};
        RED.comms.subscribe("notification/#",function(topic,msg) {
            var parts = topic.split("/");
            var notificationId = parts[1];
            if (notificationId === "runtime-deploy") {
                // handled in ui/deploy.js
                return;
            }
            if (notificationId === "node") {
                // handled below
                return;
            }
            if (notificationId === "flows-run-state") {
                // handled in editor-client/src/js/runtime.js
                return;
            }
            if (notificationId === "project-update") {
                loader.start(RED._("event.loadingProject"), 0);
                RED.nodes.clear();
                RED.history.clear();
                RED.view.redraw(true);
                RED.projects.refresh(function() {
                    loadFlows(function() {
                        var project = RED.projects.getActiveProject();
                        var message = {
                            "change-branch": RED._("notification.project.change-branch", {project: project.git.branches.local}),
                            "merge-abort": RED._("notification.project.merge-abort"),
                            "loaded": RED._("notification.project.loaded", {project: msg.project}),
                            "updated": RED._("notification.project.updated", {project: msg.project}),
                            "pull": RED._("notification.project.pull", {project: msg.project}),
                            "revert": RED._("notification.project.revert", {project: msg.project}),
                            "merge-complete": RED._("notification.project.merge-complete")
                        }[msg.action];
                        loader.end()
                        RED.notify($("<p>").text(message));
                        RED.sidebar.info.refresh()
                        RED.menu.setDisabled('menu-item-projects-open',false);
                        RED.menu.setDisabled('menu-item-projects-settings',false);
                    });
                });
                return;
            }

            if (msg.text) {
                msg.default = msg.text;
                var text = RED._(msg.text,msg);
                var options = {
                    type: msg.type,
                    fixed: msg.timeout === undefined,
                    timeout: msg.timeout,
                    id: notificationId
                }
                if (notificationId === "runtime-state") {
                    if (msg.error === "safe-mode") {
                        options.buttons = [
                            {
                                text: RED._("common.label.close"),
                                click: function() {
                                    persistentNotifications[notificationId].hideNotification();
                                }
                            }
                        ]
                    } else if (msg.error === "missing-types") {
                        text+="<ul><li>"+msg.types.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
                        if (!!RED.projects.getActiveProject()) {
                            options.buttons = [
                                {
                                    text: RED._("notification.label.manage-project-dep"),
                                    click: function() {
                                        persistentNotifications[notificationId].hideNotification();
                                        RED.projects.settings.show('deps');
                                    }
                                }
                            ]
                            // } else if (RED.settings.get('externalModules.palette.allowInstall', true) !== false) {
                        } else {
                            options.buttons = [
                                {
                                    text: RED._("notification.label.unknownNodesButton"),
                                    class: "pull-left",
                                    click: function() {
                                        RED.actions.invoke("core:search", "type:unknown ");
                                    }
                                },
                                {
                                    class: "primary",
                                    text: RED._("common.label.close"),
                                    click: function() {
                                        persistentNotifications[notificationId].hideNotification();
                                    }
                                }
                            ]
                        }
                    } else if (msg.error === "missing-modules") {
                        text+="<ul><li>"+msg.modules.map(function(m) { return RED.utils.sanitize(m.module)+(m.error?(" - <small>"+RED.utils.sanitize(""+m.error)+"</small>"):"")}).join("</li><li>")+"</li></ul>";
                        options.buttons = [
                            {
                                text: RED._("common.label.close"),
                                click: function() {
                                    persistentNotifications[notificationId].hideNotification();
                                }
                            }
                        ]
                    } else if (msg.error === "credentials_load_failed") {
                        if (RED.settings.theme("projects.enabled",false)) {
                            // projects enabled
                            if (RED.user.hasPermission("projects.write")) {
                                options.buttons = [
                                    {
                                        text: RED._("notification.project.setupCredentials"),
                                        click: function() {
                                            persistentNotifications[notificationId].hideNotification();
                                            RED.projects.showCredentialsPrompt();
                                        }
                                    }
                                ]
                            }
                        } else {
                            options.buttons = [
                                {
                                    text: RED._("common.label.close"),
                                    click: function() {
                                        persistentNotifications[notificationId].hideNotification();
                                    }
                                }
                            ]
                        }
                    } else if (msg.error === "missing_flow_file") {
                        if (RED.user.hasPermission("projects.write")) {
                            options.buttons = [
                                {
                                    text: RED._("notification.project.setupProjectFiles"),
                                    click: function() {
                                        persistentNotifications[notificationId].hideNotification();
                                        RED.projects.showFilesPrompt();
                                    }
                                }
                            ]
                        }
                    } else if (msg.error === "missing_package_file") {
                        if (RED.user.hasPermission("projects.write")) {
                            options.buttons = [
                                {
                                    text: RED._("notification.project.setupProjectFiles"),
                                    click: function() {
                                        persistentNotifications[notificationId].hideNotification();
                                        RED.projects.showFilesPrompt();
                                    }
                                }
                            ]
                        }
                    } else if (msg.error === "project_empty") {
                        if (RED.user.hasPermission("projects.write")) {
                            options.buttons = [
                                {
                                    text: RED._("notification.project.no"),
                                    click: function() {
                                        persistentNotifications[notificationId].hideNotification();
                                    }
                                },
                                {
                                    text: RED._("notification.project.createDefault"),
                                    click: function() {
                                        persistentNotifications[notificationId].hideNotification();
                                        RED.projects.createDefaultFileSet();
                                    }
                                }
                            ]
                        }
                    } else if (msg.error === "git_merge_conflict") {
                        RED.nodes.clear();
                        RED.sidebar.versionControl.refresh(true);
                        if (RED.user.hasPermission("projects.write")) {
                            options.buttons = [
                                {
                                    text: RED._("notification.project.mergeConflict"),
                                    click: function() {
                                        persistentNotifications[notificationId].hideNotification();
                                        RED.sidebar.versionControl.showLocalChanges();
                                    }
                                }
                            ]
                        }
                    }
                } else if (notificationId === 'restart-required') {
                    options.buttons = [
                        {
                            text: RED._("common.label.close"),
                            click: function() {
                                persistentNotifications[notificationId].hideNotification();
                            }
                        }
                    ]
                }
                if (!persistentNotifications.hasOwnProperty(notificationId)) {
                    persistentNotifications[notificationId] = RED.notify(text,options);
                } else {
                    persistentNotifications[notificationId].update(text,options);
                }
            } else if (persistentNotifications.hasOwnProperty(notificationId)) {
                persistentNotifications[notificationId].close();
                delete persistentNotifications[notificationId];
            }
            if (notificationId === 'runtime-state') {
                RED.events.emit("runtime-state",msg);
            }
        });
        RED.comms.subscribe("status/#",function(topic,msg) {
            var parts = topic.split("/");
            var node = RED.nodes.node(parts[1]);
            if (node) {
                if (msg.hasOwnProperty("text") && msg.text !== null && /^[@a-zA-Z]/.test(msg.text)) {
                    msg.text = node._(msg.text.toString(),{defaultValue:msg.text.toString()});
                }
                node.status = msg;
                node.dirtyStatus = true;
                node.dirty = true;
                RED.view.redrawStatus(node);
            }
        });
        RED.comms.subscribe("notification/plugin/#",function(topic,msg) {
            if (topic == "notification/plugin/added") {
                RED.settings.refreshSettings(function(err, data) {
                    let addedPlugins = [];
                    msg.forEach(function(m) {
                        let id = m.id;
                        RED.plugins.addPlugin(m);

                        m.plugins.forEach((p) => {
                            addedPlugins.push(p.id);
                        })

                        RED.i18n.loadNodeCatalog(id, function() {
                            var lang = localStorage.getItem("editor-language")||RED.i18n.detectLanguage();
                            $.ajax({
                                headers: {
                                    "Accept":"text/html",
                                    "Accept-Language": lang
                                },
                                cache: false,
                                url: 'plugins/'+id,
                                success: function(data) {
                                    appendPluginConfig(data);
                                }
                            });
                        });
                    });
                    if (addedPlugins.length) {
                        let pluginList = "<ul><li>"+addedPlugins.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
                        // ToDo: Adapt notification (node -> plugin)
                        RED.notify(RED._("palette.event.nodeAdded", {count:addedPlugins.length})+pluginList,"success");
                    }
                })
            }
        });

        let pendingNodeRemovedNotifications = []
        let pendingNodeRemovedTimeout

        RED.comms.subscribe("notification/node/#",function(topic,msg) {
            var i,m;
            var typeList;
            var info;
            if (topic == "notification/node/added") {
                RED.settings.refreshSettings(function(err, data) {
                    var addedTypes = [];
                    msg.forEach(function(m) {
                        var id = m.id;
                        RED.nodes.addNodeSet(m);
                        addedTypes = addedTypes.concat(m.types);
                        RED.i18n.loadNodeCatalog(id, function() {
                            var lang = localStorage.getItem("editor-language")||RED.i18n.detectLanguage();
                            $.ajax({
                                headers: {
                                    "Accept":"text/html",
                                    "Accept-Language": lang
                                },
                                cache: false,
                                url: 'nodes/'+id,
                                success: function(data) {
                                    appendNodeConfig(data);
                                }
                            });
                        });
                    });
                    if (addedTypes.length) {
                        typeList = "<ul><li>"+addedTypes.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
                        RED.notify(RED._("palette.event.nodeAdded", {count:addedTypes.length})+typeList,"success");
                    }
                    loadIconList();
                })
            } else if (topic == "notification/node/removed") {
                for (i=0;i<msg.length;i++) {
                    m = msg[i];
                    info = RED.nodes.removeNodeSet(m.id);
                    if (info.added) {
                        pendingNodeRemovedNotifications = pendingNodeRemovedNotifications.concat(m.types.map(RED.utils.sanitize))
                        if (pendingNodeRemovedTimeout) {
                            clearTimeout(pendingNodeRemovedTimeout)
                        }
                        pendingNodeRemovedTimeout = setTimeout(function () {
                            typeList = "<ul><li>"+pendingNodeRemovedNotifications.join("</li><li>")+"</li></ul>";
                            RED.notify(RED._("palette.event.nodeRemoved", {count:pendingNodeRemovedNotifications.length})+typeList,"success");
                            pendingNodeRemovedNotifications = []
                        }, 200)
                    }
                }
                loadIconList();
            } else if (topic == "notification/node/enabled") {
                if (msg.types) {
                    RED.settings.refreshSettings(function(err, data) {
                        info = RED.nodes.getNodeSet(msg.id);
                        if (info.added) {
                            RED.nodes.enableNodeSet(msg.id);
                            typeList = "<ul><li>"+msg.types.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
                            RED.notify(RED._("palette.event.nodeEnabled", {count:msg.types.length})+typeList,"success");
                        } else {
                            var lang = localStorage.getItem("editor-language")||RED.i18n.detectLanguage();
                            $.ajax({
                                headers: {
                                    "Accept":"text/html",
                                    "Accept-Language": lang
                                },
                                cache: false,
                                url: 'nodes/'+msg.id,
                                success: function(data) {
                                    appendNodeConfig(data);
                                    typeList = "<ul><li>"+msg.types.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
                                    RED.notify(RED._("palette.event.nodeAdded", {count:msg.types.length})+typeList,"success");
                                }
                            });
                        }
                    });
                }
            } else if (topic == "notification/node/disabled") {
                if (msg.types) {
                    RED.nodes.disableNodeSet(msg.id);
                    typeList = "<ul><li>"+msg.types.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
                    RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success");
                }
            } else if (topic == "notification/node/upgraded") {
                RED.notify(RED._("palette.event.nodeUpgraded", {module:msg.module,version:msg.version}),"success");
                RED.nodes.registry.setModulePendingUpdated(msg.module,msg.version);
            }
        });
        RED.comms.subscribe("event-log/#", function(topic,payload) {
            var id = topic.substring(9);
            RED.eventLog.log(id,payload);
        });

        $(".red-ui-header-toolbar").show();

        RED.sidebar.show(":first", true);

        setTimeout(function() {
            loader.end();
            checkFirstRun(function() {
                if (showProjectWelcome) {
                    RED.projects.showStartup();
                }
            });
        },100);
    }

    function checkFirstRun(done) {
        if (RED.settings.theme("tours") === false) {
            done();
            return;
        }
        if (!RED.settings.get("editor.view.view-show-welcome-tours", true)) {
            done();
            return;
        }
        RED.actions.invoke("core:show-welcome-tour", RED.settings.get("editor.tours.welcome"), done);
    }

    function buildMainMenu() {
        var menuOptions = [];
        if (RED.settings.theme("projects.enabled",false)) {
            menuOptions.push({id:"menu-item-projects-menu",label:RED._("menu.label.projects"),options:[
                {id:"menu-item-projects-new",label:RED._("menu.label.projects-new"),disabled:false,onselect:"core:new-project"},
                {id:"menu-item-projects-open",label:RED._("menu.label.projects-open"),disabled:false,onselect:"core:open-project"},
                {id:"menu-item-projects-settings",label:RED._("menu.label.projects-settings"),disabled:false,onselect:"core:show-project-settings"}
            ]});
        }
        menuOptions.push({id:"menu-item-edit-menu", label:RED._("menu.label.edit"), options: [
            {id: "menu-item-edit-undo", label:RED._("keyboard.undoChange"), disabled: true, onselect: "core:undo"},
            {id: "menu-item-edit-redo", label:RED._("keyboard.redoChange"), disabled: true, onselect: "core:redo"},
            null,
            {id: "menu-item-edit-cut", label:RED._("keyboard.cutNode"), onselect: "core:cut-selection-to-internal-clipboard"},
            {id: "menu-item-edit-copy", label:RED._("keyboard.copyNode"), onselect: "core:copy-selection-to-internal-clipboard"},
            {id: "menu-item-edit-paste", label:RED._("keyboard.pasteNode"), disabled: true, onselect: "core:paste-from-internal-clipboard"},
            null,
            {id: "menu-item-edit-copy-group-style", label:RED._("keyboard.copyGroupStyle"), onselect: "core:copy-group-style"},
            {id: "menu-item-edit-paste-group-style", label:RED._("keyboard.pasteGroupStyle"), disabled: true, onselect: "core:paste-group-style"},
            null,
            {id: "menu-item-edit-select-all", label:RED._("keyboard.selectAll"), onselect: "core:select-all-nodes"},
            {id: "menu-item-edit-select-connected", label:RED._("keyboard.selectAllConnected"), onselect: "core:select-connected-nodes"},
            {id: "menu-item-edit-select-none", label:RED._("keyboard.selectNone"), onselect: "core:select-none"},
            null,
            {id: "menu-item-edit-split-wire-with-links", label:RED._("keyboard.splitWireWithLinks"), onselect: "core:split-wire-with-link-nodes"},

        ]});

        menuOptions.push({id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[
            {id:"menu-item-palette",label:RED._("menu.label.palette.show"),toggle:true,onselect:"core:toggle-palette", selected: true},
            {id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:"core:toggle-sidebar", selected: true},
            {id:"menu-item-event-log",label:RED._("eventLog.title"),onselect:"core:show-event-log"},
            {id:"menu-item-action-list",label:RED._("keyboard.actionList"),onselect:"core:show-action-list"},
            null
        ]});

        menuOptions.push({id:"menu-item-arrange-menu", label:RED._("menu.label.arrange"), options: [
            {id: "menu-item-view-tools-align-left", label:RED._("menu.label.alignLeft"), disabled: true, onselect: "core:align-selection-to-left"},
            {id: "menu-item-view-tools-align-center", label:RED._("menu.label.alignCenter"), disabled: true, onselect: "core:align-selection-to-center"},
            {id: "menu-item-view-tools-align-right", label:RED._("menu.label.alignRight"), disabled: true, onselect: "core:align-selection-to-right"},
            null,
            {id: "menu-item-view-tools-align-top", label:RED._("menu.label.alignTop"), disabled: true, onselect: "core:align-selection-to-top"},
            {id: "menu-item-view-tools-align-middle", label:RED._("menu.label.alignMiddle"), disabled: true, onselect: "core:align-selection-to-middle"},
            {id: "menu-item-view-tools-align-bottom", label:RED._("menu.label.alignBottom"), disabled: true, onselect: "core:align-selection-to-bottom"},
            null,
            {id: "menu-item-view-tools-distribute-horizontally", label:RED._("menu.label.distributeHorizontally"), disabled: true, onselect: "core:distribute-selection-horizontally"},
            {id: "menu-item-view-tools-distribute-veritcally", label:RED._("menu.label.distributeVertically"), disabled: true, onselect: "core:distribute-selection-vertically"},
            null,
            {id: "menu-item-view-tools-move-to-back", label:RED._("menu.label.moveToBack"), disabled: true, onselect: "core:move-selection-to-back"},
            {id: "menu-item-view-tools-move-to-front", label:RED._("menu.label.moveToFront"), disabled: true, onselect: "core:move-selection-to-front"},
            {id: "menu-item-view-tools-move-backwards", label:RED._("menu.label.moveBackwards"), disabled: true, onselect: "core:move-selection-backwards"},
            {id: "menu-item-view-tools-move-forwards", label:RED._("menu.label.moveForwards"), disabled: true, onselect: "core:move-selection-forwards"}
        ]});

        menuOptions.push(null);
        if (RED.settings.theme("menu.menu-item-import-library", true)) {
            menuOptions.push({id: "menu-item-import", label: RED._("menu.label.import"), onselect: "core:show-import-dialog"});
        }
        if (RED.settings.theme("menu.menu-item-export-library", true)) {
            menuOptions.push({id: "menu-item-export", label: RED._("menu.label.export"), onselect: "core:show-export-dialog"});
        }
        menuOptions.push(null);
        menuOptions.push({id:"menu-item-search",label:RED._("menu.label.search"),onselect:"core:search"});
        menuOptions.push(null);
        menuOptions.push({id:"menu-item-config-nodes",label:RED._("menu.label.displayConfig"),onselect:"core:show-config-tab"});
        menuOptions.push({id:"menu-item-workspace",label:RED._("menu.label.flows"),options:[
            {id:"menu-item-workspace-add",label:RED._("menu.label.add"),onselect:"core:add-flow"},
            {id:"menu-item-workspace-edit",label:RED._("menu.label.edit"),onselect:"core:edit-flow"},
            {id:"menu-item-workspace-delete",label:RED._("menu.label.delete"),onselect:"core:remove-flow"}
        ]});
        menuOptions.push({id:"menu-item-subflow",label:RED._("menu.label.subflows"), options: [
            {id:"menu-item-subflow-create",label:RED._("menu.label.createSubflow"),onselect:"core:create-subflow"},
            {id:"menu-item-subflow-convert",label:RED._("menu.label.selectionToSubflow"),disabled:true,onselect:"core:convert-to-subflow"},
        ]});
        menuOptions.push({id:"menu-item-group",label:RED._("menu.label.groups"), options: [
            {id:"menu-item-group-group",label:RED._("menu.label.groupSelection"),disabled:true,onselect:"core:group-selection"},
            {id:"menu-item-group-ungroup",label:RED._("menu.label.ungroupSelection"),disabled:true,onselect:"core:ungroup-selection"},
            null,
            {id:"menu-item-group-merge",label:RED._("menu.label.groupMergeSelection"),disabled:true,onselect:"core:merge-selection-to-group"},
            {id:"menu-item-group-remove",label:RED._("menu.label.groupRemoveSelection"),disabled:true,onselect:"core:remove-selection-from-group"}
        ]});

        menuOptions.push(null);
        if (RED.settings.get('externalModules.palette.allowInstall', true) !== false) {
            menuOptions.push({id:"menu-item-edit-palette",label:RED._("menu.label.editPalette"),onselect:"core:manage-palette"});
            menuOptions.push(null);
        }

        menuOptions.push({id:"menu-item-user-settings",label:RED._("menu.label.settings"),onselect:"core:show-user-settings"});
        menuOptions.push(null);

        if (RED.settings.theme("menu.menu-item-keyboard-shortcuts", true)) {
            menuOptions.push({id: "menu-item-keyboard-shortcuts", label: RED._("menu.label.keyboardShortcuts"), onselect: "core:show-help"});
        }
        menuOptions.push({id:"menu-item-help",
            label: RED.settings.theme("menu.menu-item-help.label",RED._("menu.label.help")),
            href: RED.settings.theme("menu.menu-item-help.url","https://nodered.org/docs")
        });
        menuOptions.push({id:"menu-item-node-red-version", label:"v"+RED.settings.version, onselect: "core:show-about" });


        $('<li><a id="red-ui-header-button-sidemenu" class="button" href="#"><i class="fa fa-bars"></i></a></li>').appendTo(".red-ui-header-toolbar")
        RED.menu.init({id:"red-ui-header-button-sidemenu",options: menuOptions});

    }

    function loadEditor() {
        RED.workspaces.init();
        RED.statusBar.init();
        RED.view.init();
        RED.userSettings.init();
        RED.user.init();
        RED.notifications.init();
        RED.library.init();
        RED.palette.init();
        RED.eventLog.init();

        if (RED.settings.get('externalModules.palette.allowInstall', true) !== false) {
            RED.palette.editor.init();
        } else {
            console.log("Palette editor disabled");
        }

        RED.sidebar.init();

        if (RED.settings.theme("projects.enabled",false)) {
            RED.projects.init();
        } else {
            console.log("Projects disabled");
        }

        RED.subflow.init();
        RED.group.init();
        RED.clipboard.init();
        RED.search.init();
        RED.actionList.init();
        RED.editor.init();
        RED.diagnostics.init();
        RED.diff.init();


        RED.deploy.init(RED.settings.theme("deployButton",null));

        RED.keyboard.init(buildMainMenu);
        RED.envVar.init();

        RED.nodes.init();
        RED.runtime.init()

        if (RED.settings.theme("multiplayer.enabled",false)) {
            RED.multiplayer.init()
        }
        RED.comms.connect();

        $("#red-ui-main-container").show();

        loadPluginList();
    }


    function buildEditor(options) {
        var header = $('<div id="red-ui-header"></div>').appendTo(options.target);
        var logo = $('<span class="red-ui-header-logo"></span>').appendTo(header);
        $('<ul class="red-ui-header-toolbar hide"></ul>').appendTo(header);
        $('<div id="red-ui-header-shade" class="hide"></div>').appendTo(header);
        $('<div id="red-ui-main-container" class="red-ui-sidebar-closed hide">'+
            '<div id="red-ui-workspace"></div>'+
            '<div id="red-ui-editor-stack" tabindex="-1"></div>'+
            '<div id="red-ui-palette"></div>'+
            '<div id="red-ui-sidebar"></div>'+
            '<div id="red-ui-sidebar-separator"></div>'+
        '</div>').appendTo(options.target);
        $('<div id="red-ui-editor-plugin-configs"></div>').appendTo(options.target);
        $('<div id="red-ui-editor-node-configs"></div>').appendTo(options.target);
        $('<div id="red-ui-full-shade" class="hide"></div>').appendTo(options.target);

        loader.init().appendTo("#red-ui-main-container");
        loader.start("...",0);

        $.getJSON(options.apiRootUrl+"theme", function(theme) {
            if (theme.header) {
                if (theme.header.url) {
                    logo = $("<a>",{href:theme.header.url}).appendTo(logo);
                }
                if (theme.header.image) {
                    $('<img>',{src:theme.header.image}).appendTo(logo);
                }
                if (theme.header.title) {
                    $('<span>').html(theme.header.title).appendTo(logo);
                }
            }
            if (theme.themes) {
                knownThemes = theme.themes;
            }
        });
    }
    var knownThemes = null;
    var initialised = false;

    function init(options) {
        if (initialised) {
            throw new Error("RED already initialised");
        }
        initialised = true;
        if(window.ace) { window.ace.require("ace/ext/language_tools"); }
        options = options || {};
        options.apiRootUrl = options.apiRootUrl || "";
        if (options.apiRootUrl && !/\/$/.test(options.apiRootUrl)) {
            options.apiRootUrl = options.apiRootUrl+"/";
        }
        options.target = $("#red-ui-editor");
        options.target.addClass("red-ui-editor");

        buildEditor(options);

        RED.i18n.init(options, function() {
            RED.settings.init(options, function() {
                if (knownThemes) {
                    RED.settings.editorTheme = RED.settings.editorTheme || {};
                    RED.settings.editorTheme.themes = knownThemes;
                }
                loadEditor();
            });
        })
    }

    var loader = {
        init: function() {
            var wrapper = $('<div id="red-ui-loading-progress"></div>').hide();
            var container = $('<div>').appendTo(wrapper);
            var label = $('<div>',{class:"red-ui-loading-bar-label"}).appendTo(container);
            var bar = $('<div>',{class:"red-ui-loading-bar"}).appendTo(container);
            var fill =$('<span>').appendTo(bar);
            return wrapper;
        },
        start: function(text, prcnt) {
            if (text) {
                loader.reportProgress(text,prcnt)
            }
            $("#red-ui-loading-progress").show();
        },
        reportProgress: function(text, prcnt) {
            $(".red-ui-loading-bar-label").text(text);
            $(".red-ui-loading-bar span").width(prcnt+"%")
        },
        end: function() {
            $("#red-ui-loading-progress").hide();
            loader.reportProgress("",0);
        }
    }

    return {
        init: init,
        loader: loader
    }
})();
;/**
 * 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.
 **/

 RED.events = (function() {
     var handlers = {};

     function on(evt,func) {
         handlers[evt] = handlers[evt]||[];
         handlers[evt].push(func);
     }
     function off(evt,func) {
         var handler = handlers[evt];
         if (handler) {
             for (var i=0;i<handler.length;i++) {
                 if (handler[i] === func) {
                     handler.splice(i,1);
                     return;
                 }
             }
         }
     }
     function emit() {
         var evt = arguments[0]
         var args = Array.prototype.slice.call(arguments,1);
         if (RED.events.DEBUG) {
             console.warn(evt,args);
         }
         if (handlers[evt]) {
             let cpyHandlers = [...handlers[evt]];
          
             for (var i=0;i<cpyHandlers.length;i++) {
                 try {
                     cpyHandlers[i].apply(null, args);
                 } catch(err) {
                     console.warn("RED.events.emit error: ["+evt+"] "+(err.toString()));
                     console.warn(err);
                 }
             }
         }
     }
     return {
         on: on,
         off: off,
         emit: emit
     }
 })();
;RED.hooks = (function() {

    var VALID_HOOKS = [

    ]

    var hooks = { }
    var labelledHooks = { }

    function add(hookId, callback) {
        var parts = hookId.split(".");
        var id = parts[0], label = parts[1];

        // if (VALID_HOOKS.indexOf(id) === -1) {
        //     throw new Error("Invalid hook '"+id+"'");
        // }
        if (label && labelledHooks[label] && labelledHooks[label][id]) {
            throw new Error("Hook "+hookId+" already registered")
        }
        var hookItem = {cb:callback, previousHook: null, nextHook: null }

        var tailItem = hooks[id];
        if (tailItem === undefined) {
            hooks[id] = hookItem;
        } else {
            while(tailItem.nextHook !== null) {
                tailItem = tailItem.nextHook
            }
            tailItem.nextHook = hookItem;
            hookItem.previousHook = tailItem;
        }

        if (label) {
            labelledHooks[label] = labelledHooks[label]||{};
            labelledHooks[label][id] = hookItem;
        }
    }
    function remove(hookId) {
        var parts = hookId.split(".");
        var id = parts[0], label = parts[1];
        if ( !label) {
            throw new Error("Cannot remove hook without label: "+hookId)
        }
        if (labelledHooks[label]) {
            if (id === "*") {
                // Remove all hooks for this label
                var hookList = Object.keys(labelledHooks[label]);
                for (var i=0;i<hookList.length;i++) {
                    removeHook(hookList[i],labelledHooks[label][hookList[i]])
                }
                delete labelledHooks[label];
            } else if (labelledHooks[label][id]) {
                removeHook(id,labelledHooks[label][id])
                delete labelledHooks[label][id];
                if (Object.keys(labelledHooks[label]).length === 0){
                    delete labelledHooks[label];
                }
            }
        }
    }

    function removeHook(id,hookItem) {
        var previousHook = hookItem.previousHook;
        var nextHook = hookItem.nextHook;

        if (previousHook) {
            previousHook.nextHook = nextHook;
        } else {
            hooks[id] = nextHook;
        }
        if (nextHook) {
            nextHook.previousHook = previousHook;
        }
        hookItem.removed = true;
        if (!previousHook && !nextHook) {
            delete hooks[id];
        }
    }

    function trigger(hookId, payload, done) {
        var hookItem = hooks[hookId];
        if (!hookItem) {
            if (done) {
                done();
            }
            return;
        }
        function callNextHook(err) {
            if (!hookItem || err) {
                if (done) { done(err) }
                return err;
            }
            if (hookItem.removed) {
                hookItem = hookItem.nextHook;
                return callNextHook();
            }
            var callback = hookItem.cb;
            if (callback.length === 1) {
                try {
                    let result = callback(payload);
                    if (result === false) {
                        // Halting the flow
                        if (done) { done(false) }
                        return result;
                    }
                    hookItem = hookItem.nextHook;
                    return callNextHook();
                } catch(e) {
                    console.warn(e);
                    if (done) { done(e);}
                    return e;
                }
            } else {
                // There is a done callback
                try {
                    callback(payload,function(result) {
                        if (result === undefined) {
                            hookItem = hookItem.nextHook;
                            callNextHook();
                        } else {
                            if (done) { done(result)}
                        }
                    })
                } catch(e) {
                    console.warn(e);
                    if (done) { done(e) }
                    return e;
                }
            }
        }

        return callNextHook();
    }

    function clear() {
        hooks = {}
        labelledHooks = {}
    }

    function has(hookId) {
        var parts = hookId.split(".");
        var id = parts[0], label = parts[1];
        if (label) {
            return !!(labelledHooks[label] && labelledHooks[label][id])
        }
        return !!hooks[id]
    }

    return {
        has: has,
        clear: clear,
        add: add,
        remove: remove,
        trigger: trigger
    }
})();
;/**
 * 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.
 **/

RED.i18n = (function() {

    var apiRootUrl;

    function detectLanguage() {
        return navigator.language
    }

    return {
        init: function(options, done) {
            apiRootUrl = options.apiRootUrl||"";
            var preferredLanguage = localStorage.getItem("editor-language") || detectLanguage();
            var opts = {
                compatibilityJSON: 'v3',
                backend: {
                    loadPath: apiRootUrl+'locales/__ns__?lng=__lng__',
                },
                lng: 'en-US',
                // debug: true,
                preload:['en-US'],
                ns: ["editor","node-red","jsonata","infotips"],
                defaultNS: "editor",
                fallbackLng: ['en-US'],
                returnObjects: true,
                keySeparator: ".",
                nsSeparator: ":",
                interpolation: {
                    unescapeSuffix: 'HTML',
                    escapeValue: false,
                    prefix: '__',
                    suffix: '__'
                }
            };
            if (preferredLanguage) {
                opts.lng = preferredLanguage;
            }

            i18next.use(i18nextHttpBackend).init(opts,function() {
                done();
            });
            jqueryI18next.init(i18next, $, { handleName: 'i18n' });


            RED["_"] = function() {
                var v = i18next.t.apply(i18next,arguments);
                if (typeof v === 'string') {
                    return v;
                } else {
                    return arguments[0];
                }
            }
        },
        lang: function() {
            // Gets the active message catalog language. This is based on what
            // locale the editor is using and what languages are available.
            //
            var preferredLangs = [localStorage.getItem("editor-language")|| detectLanguage()].concat(i18next.languages);
            var knownLangs = RED.settings.theme("languages")||["en-US"];
            for (var i=0;i<preferredLangs.length;i++) {
                if (knownLangs.indexOf(preferredLangs[i]) > -1) {
                    return preferredLangs[i]
                }
            }
            return 'en-US'
        },
        loadNodeCatalog: function(namespace,done) {
            var languageList = [localStorage.getItem("editor-language")|| detectLanguage()].concat(i18next.languages);
            var toLoad = languageList.length;
            languageList.forEach(function(lang) {
                $.ajax({
                    headers: {
                        "Accept":"application/json"
                    },
                    cache: false,
                    url: apiRootUrl+'nodes/'+namespace+'/messages?lng='+lang,
                    success: function(data) {
                        i18next.addResourceBundle(lang,namespace,data);
                        toLoad--;
                        if (toLoad === 0) {
                            done();
                        }
                    }
                });
            })

        },

        loadNodeCatalogs: function(done) {
            var languageList = [localStorage.getItem("editor-language")|| detectLanguage()].concat(i18next.languages);
            var toLoad = languageList.length;

            languageList.forEach(function(lang) {
                $.ajax({
                    headers: {
                        "Accept":"application/json"
                    },
                    cache: false,
                    url: apiRootUrl+'nodes/messages?lng='+lang,
                    success: function(data) {
                        var namespaces = Object.keys(data);
                        namespaces.forEach(function(ns) {
                            i18next.addResourceBundle(lang,ns,data[ns]);
                        });
                        toLoad--;
                        if (toLoad === 0) {
                            done();
                        }
                    }
                });
            })
        },

        loadPluginCatalogs: function(done) {
            var languageList = [localStorage.getItem("editor-language")|| detectLanguage()].concat(i18next.languages);
            var toLoad = languageList.length;

            languageList.forEach(function(lang) {
                $.ajax({
                    headers: {
                        "Accept":"application/json"
                    },
                    cache: false,
                    url: apiRootUrl+'plugins/messages?lng='+lang,
                    success: function(data) {
                        var namespaces = Object.keys(data);
                        namespaces.forEach(function(ns) {
                            i18next.addResourceBundle(lang,ns,data[ns]);
                        });
                        toLoad--;
                        if (toLoad === 0) {
                            done();
                        }
                    }
                });
            })
        },
        detectLanguage: detectLanguage
    }
})();
;/**
 * 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.
 **/


RED.settings = (function () {

    var loadedSettings = {};
    var userSettings = {};
    var pendingSave;

    var hasLocalStorage = function () {
        try {
            return 'localStorage' in window && window['localStorage'] !== null;
        } catch (e) {
            return false;
        }
    };

    var set = function (key, value) {
        if (!hasLocalStorage()) {
            return;
        }
        if (key.startsWith("auth-tokens")) {
            localStorage.setItem(key+this.authTokensSuffix, JSON.stringify(value));
        } else {
            RED.utils.setMessageProperty(userSettings,key,value);
            saveUserSettings();
        }
    };

    /**
     * If the key is not set in the localStorage it returns <i>undefined</i>
     * Else return the JSON parsed value
     * @param key
     * @param defaultIfUndefined
     * @returns {*}
     */
    var get = function (key,defaultIfUndefined) {
        if (!hasLocalStorage()) {
            return undefined;
        }
        if (key.startsWith("auth-tokens")) {
            return JSON.parse(localStorage.getItem(key+this.authTokensSuffix));
        } else {
            var v;
            try { v = RED.utils.getMessageProperty(userSettings,key); } catch(err) {}
            if (v === undefined) {
                try { v = RED.utils.getMessageProperty(RED.settings,key); } catch(err) {}
            }
            if (v === undefined) {
                v = defaultIfUndefined;
            }
            return v;
        }
    };

    var remove = function (key) {
        if (!hasLocalStorage()) {
            return;
        }
        if (key.startsWith("auth-tokens")) {
            localStorage.removeItem(key+this.authTokensSuffix);
        } else {
            delete userSettings[key];
            saveUserSettings();
        }
    };

    var setProperties = function(data) {
        for (var prop in loadedSettings) {
            if (loadedSettings.hasOwnProperty(prop) && RED.settings.hasOwnProperty(prop)) {
                delete RED.settings[prop];
            }
        }
        for (prop in data) {
            if (data.hasOwnProperty(prop)) {
                RED.settings[prop] = data[prop];
            }
        }
        loadedSettings = data;
    };

    var setUserSettings = function(data) {
        userSettings = data;
    }

    var init = function (options, done) {
        var accessTokenMatch = /[?&]access_token=(.*?)(?:$|&)/.exec(window.location.search);
        var path=window.location.pathname.slice(0,-1);
        RED.settings.authTokensSuffix=path.replace(/\//g, '-');
        if (accessTokenMatch) {
            var accessToken = accessTokenMatch[1];
            RED.settings.set("auth-tokens",{access_token: accessToken});
            window.location.search = "";
        }
        RED.settings.apiRootUrl = options.apiRootUrl;

        $.ajaxSetup({
            beforeSend: function(jqXHR,settings) {
                // Only attach auth header for requests to relative paths
                if (!/^\s*(https?:|\/|\.)/.test(settings.url)) {
                    if (options.apiRootUrl) {
                        settings.url = options.apiRootUrl+settings.url;
                    }
                    var auth_tokens = RED.settings.get("auth-tokens");
                    if (auth_tokens) {
                        jqXHR.setRequestHeader("Authorization","Bearer "+auth_tokens.access_token);
                    }
                    jqXHR.setRequestHeader("Node-RED-API-Version","v2");
                }
            }
        });

        load(done);
    }

    var refreshSettings = function(done) {
        $.ajax({
            headers: {
                "Accept": "application/json"
            },
            dataType: "json",
            cache: false,
            url: 'settings',
            success: function (data) {
                setProperties(data);
                done(null, data);
            },
            error: function(jqXHR,textStatus,errorThrown) {
                if (jqXHR.status === 401) {
                    if (/[?&]access_token=(.*?)(?:$|&)/.test(window.location.search)) {
                        window.location.search = "";
                    }
                    RED.user.login(function() { refreshSettings(done); });
                } else {
                    console.log("Unexpected error loading settings:",jqXHR.status,textStatus);
                }
            }
        });
    }
    var load = function(done) {
        refreshSettings(function(err, data) {
            if (!err) {
                if (!RED.settings.user || RED.settings.user.anonymous) {
                    RED.settings.remove("auth-tokens");
                }
                console.log("Node-RED: " + data.version);
                console.groupCollapsed("Versions");
                console.log("jQuery",$().jquery)
                console.log("jQuery UI",$.ui.version);
                if(window.ace) { console.log("ACE",ace.version); }
                if(window.monaco) { console.log("MONACO",monaco.version || "unknown"); }
                console.log("D3",d3.version);
                console.groupEnd();
                loadUserSettings(done);
            }
        })
    };

    function loadUserSettings(done) {
        $.ajax({
            headers: {
                "Accept": "application/json"
            },
            dataType: "json",
            cache: false,
            url: 'settings/user',
            success: function (data) {
                setUserSettings(data);
                done();
            },
            error: function(jqXHR,textStatus,errorThrown) {
                console.log("Unexpected error loading user settings:",jqXHR.status,textStatus);
            }
        });
    }

    function saveUserSettings() {
        if (RED.user.hasPermission("settings.write")) {
            if (pendingSave) {
                clearTimeout(pendingSave);
            }
            pendingSave = setTimeout(function() {
                pendingSave = null;
                $.ajax({
                    method: 'POST',
                    contentType: 'application/json',
                    url: 'settings/user',
                    data: JSON.stringify(userSettings),
                    success: function (data) {
                    },
                    error: function(jqXHR,textStatus,errorThrown) {
                        console.log("Unexpected error saving user settings:",jqXHR.status,textStatus);
                    }
                });
            },300);
        }
    }

    function theme(property,defaultValue) {
        if (!RED.settings.editorTheme) {
            return defaultValue;
        }
        var parts = property.split(".");
        var v = RED.settings.editorTheme;
        try {
            for (var i=0;i<parts.length;i++) {
                v = v[parts[i]];
            }
            if (v === undefined) {
                return defaultValue;
            }
            return v;
        } catch(err) {
            return defaultValue;
        }
    }
    function getLocal(key) {
        return localStorage.getItem(key)
    }
    function setLocal(key, value) {
        localStorage.setItem(key, value);
    }
    function removeLocal(key) {
        localStorage.removeItem(key)
    }


    return {
        init: init,
        load: load,
        loadUserSettings: loadUserSettings,
        refreshSettings: refreshSettings,
        set: set,
        get: get,
        remove: remove,
        theme: theme,
        setLocal: setLocal,
        getLocal: getLocal,
        removeLocal: removeLocal
    }
})();
;/**
 * 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.
 **/
RED.user = (function() {

    function login(opts,done) {
        if (typeof opts == 'function') {
            done = opts;
            opts = {};
        }

        var dialog = $('<div id="node-dialog-login" class="hide" style="display: flex; align-items: flex-end;">'+
                       '<div style="width: 250px; flex-grow: 0;"><img id="node-dialog-login-image" src=""/></div>'+
                       '<div style="flex-grow: 1;">'+
                            '<form id="node-dialog-login-fields" class="form-horizontal" style="margin-bottom: 0px; margin-left:20px;"></form>'+
                       '</div>'+
                       '</div>');

        dialog.dialog({
            autoOpen: false,
            classes: {
                "ui-dialog": "red-ui-editor-dialog",
                "ui-dialog-titlebar-close": "hide",
                "ui-widget-overlay": "red-ui-editor-dialog"
            },
            modal: true,
            closeOnEscape: !!opts.cancelable,
            width: 600,
            resizable: false,
            draggable: false,
            close: function( event, ui ) {
                $("#node-dialog-login").dialog('destroy').remove();
                RED.keyboard.enable()
            }
        });

        $("#node-dialog-login-fields").empty();
        $.ajax({
            dataType: "json",
            url: "auth/login",
            success: function(data) {
                var i=0;

                if (data.type == "credentials") {

                    for (;i<data.prompts.length;i++) {
                        var field = data.prompts[i];
                        var row = $("<div/>",{class:"form-row"});
                        $('<label for="node-dialog-login-'+field.id+'">'+RED._(field.label)+':</label><br/>').appendTo(row);
                        var input = $('<input style="width: 100%" id="node-dialog-login-'+field.id+'" type="'+field.type+'" tabIndex="'+(i+1)+'"/>').appendTo(row);

                        if (i<data.prompts.length-1) {
                            input.keypress(
                                (function() {
                                    var r = row;
                                    return function(event) {
                                        if (event.keyCode == 13) {
                                            r.next("div").find("input").trigger("focus");
                                            event.preventDefault();
                                        }
                                    }
                                })()
                            );
                        }
                        row.appendTo("#node-dialog-login-fields");
                    }
                    $('<div class="form-row" style="text-align: right; margin-top: 10px;"><span id="node-dialog-login-failed" style="line-height: 2em;float:left;color:var(--red-ui-text-color-error);" class="hide">'+RED._("user.loginFailed")+'</span><img src="red/images/spin.svg" style="height: 30px; margin-right: 10px; " class="login-spinner hide"/>'+
                        (opts.cancelable?'<a href="#" id="node-dialog-login-cancel" class="red-ui-button" style="margin-right: 20px;" tabIndex="'+(i+1)+'">'+RED._("common.label.cancel")+'</a>':'')+
                        '<input type="submit" id="node-dialog-login-submit" class="red-ui-button" style="width: auto;" tabIndex="'+(i+2)+'" value="'+RED._("user.login")+'"></div>').appendTo("#node-dialog-login-fields");


                    $("#node-dialog-login-submit").button();
                    $("#node-dialog-login-fields").on("submit", function(event) {
                        $("#node-dialog-login-submit").button("option","disabled",true);
                        $("#node-dialog-login-failed").hide();
                        $(".login-spinner").show();

                        var body = {
                            client_id: "node-red-editor",
                            grant_type: "password",
                            scope:""
                        }
                        for (var i=0;i<data.prompts.length;i++) {
                            var field = data.prompts[i];
                            body[field.id] = $("#node-dialog-login-"+field.id).val();
                        }
                        $.ajax({
                            url:"auth/token",
                            type: "POST",
                            data: body
                        }).done(function(data,textStatus,xhr) {
                            RED.settings.set("auth-tokens",data);
                            if (opts.updateMenu) {
                                updateUserMenu();
                            }
                            $("#node-dialog-login").dialog("close");
                            done();
                        }).fail(function(jqXHR,textStatus,errorThrown) {
                            RED.settings.remove("auth-tokens");
                            $("#node-dialog-login-failed").show();
                        }).always(function() {
                            $("#node-dialog-login-submit").button("option","disabled",false);
                            $(".login-spinner").hide();
                        });
                        event.preventDefault();
                    });

                } else if (data.type == "strategy") {
                    var sessionMessage = /[?&]session_message=(.*?)(?:$|&)/.exec(window.location.search);
                    RED.sessionMessages = RED.sessionMessages || [];
                    if (sessionMessage) {
                        RED.sessionMessages.push(decodeURIComponent(sessionMessage[1]));
                        if (history.pushState) {
                            var newurl = window.location.protocol+"//"+window.location.host+window.location.pathname
                            window.history.replaceState({ path: newurl }, "", newurl);
                        } else {
                            window.location.search = "";
                        }
                    }

                    if (RED.sessionMessages.length === 0 && data.autoLogin) {
                        document.location = data.loginRedirect
                        return
                    }

                    i = 0;
                    for (;i<data.prompts.length;i++) {
                        var field = data.prompts[i];
                        if (RED.sessionMessages) {
                            var sessionMessages = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
                            RED.sessionMessages.forEach(function (msg) {
                                $('<div>').css("color","var(--red-ui-text-color-error)").text(msg).appendTo(sessionMessages);
                            });
                            delete RED.sessionMessages;
                        }
                        var row = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");

                        var loginButton = $('<a href="#" class="red-ui-button"></a>',{style: "padding: 10px"}).appendTo(row).on("click", function() {
                            document.location = field.url;
                        });
                        if (field.image) {
                            $("<img>",{src:field.image}).appendTo(loginButton);
                        } else if (field.label) {
                            var label = $('<span></span>').text(field.label);
                            if (field.icon) {
                                $('<i></i>',{class: "fa fa-2x "+field.icon, style:"vertical-align: middle"}).appendTo(loginButton);
                                label.css({
                                    "verticalAlign":"middle",
                                    "marginLeft":"8px"
                                });

                            }
                            label.appendTo(loginButton);
                        }
                        loginButton.button();
                    }


                } else {
                    if (data.prompts) {
                        if (data.loginMessage) {
                            const sessionMessages = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
                            $('<div>').text(data.loginMessage).appendTo(sessionMessages);
                        }

                        i = 0;
                        for (;i<data.prompts.length;i++) {
                            var field = data.prompts[i];
                            var row = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
                            var loginButton = $('<a href="#" class="red-ui-button"></a>',{style: "padding: 10px"}).appendTo(row).on("click", function() {
                                document.location = field.url;
                            });
                            if (field.image) {
                                $("<img>",{src:field.image}).appendTo(loginButton);
                            } else if (field.label) {
                                var label = $('<span></span>').text(field.label);
                                if (field.icon) {
                                    $('<i></i>',{class: "fa fa-2x "+field.icon, style:"vertical-align: middle"}).appendTo(loginButton);
                                    label.css({
                                        "verticalAlign":"middle",
                                        "marginLeft":"8px"
                                    });

                                }
                                label.appendTo(loginButton);
                            }
                            loginButton.button();
                        }
                    }
                }
                if (opts.cancelable) {
                    $("#node-dialog-login-cancel").button().on("click", function( event ) {
                        $("#node-dialog-login").dialog('close');

                    });
                }

                var loginImageSrc = data.image || "red/images/node-red-256.svg";

                $("#node-dialog-login-image").load(function() {
                    dialog.dialog("open");
                }).attr("src",loginImageSrc);
                RED.keyboard.disable();
            }
        });
    }

    function logout() {
        RED.events.emit('logout')
        var tokens = RED.settings.get("auth-tokens");
        var token = tokens?tokens.access_token:"";
        $.ajax({
            url: "auth/revoke",
            type: "POST",
            data: {token:token}
        }).done(function(data,textStatus,xhr) {
            RED.settings.remove("auth-tokens");
            if (data && data.redirect) {
                document.location.href = data.redirect;
            } else {
                document.location.reload(true);
            }
        }).fail(function(jqXHR,textStatus,errorThrown) {
            if (jqXHR.status === 401) {
                document.location.reload(true);
            } else {
                console.log(textStatus);
            }
        })
    }

    function updateUserMenu() {
        $("#red-ui-header-button-user-submenu li").remove();
        const userMenu = $("#red-ui-header-button-user")
        userMenu.empty()
        if (RED.settings.user.anonymous) {
            RED.menu.addItem("red-ui-header-button-user",{
                id:"usermenu-item-login",
                label:RED._("menu.label.login"),
                onselect: function() {
                    RED.user.login({cancelable:true},function() {
                        RED.settings.load(function() {
                            RED.notify(RED._("user.loggedInAs",{name:RED.settings.user.username}),"success");
                            updateUserMenu();
                            RED.events.emit("login",RED.settings.user.username);
                        });
                    });
                }
            });
        } else {
            RED.menu.addItem("red-ui-header-button-user",{
                id:"usermenu-item-username",
                label:"<b>"+RED.settings.user.username+"</b>"
            });
            RED.menu.addItem("red-ui-header-button-user",{
                id:"usermenu-item-logout",
                label:RED._("menu.label.logout"),
                onselect: function() {
                    RED.user.logout();
                }
            });
        }
        const userIcon = generateUserIcon(RED.settings.user)
        userIcon.appendTo(userMenu);
    }

    function init() {
        if (RED.settings.user) {
            if (!RED.settings.editorTheme || !RED.settings.editorTheme.hasOwnProperty("userMenu") || RED.settings.editorTheme.userMenu) {

                var userMenu = $('<li><a id="red-ui-header-button-user" class="button hide" href="#"></a></li>')
                    .prependTo(".red-ui-header-toolbar");
                RED.menu.init({id:"red-ui-header-button-user",
                    options: []
                });
                updateUserMenu();
            }
        }

    }

    var readRE = /^((.+)\.)?read$/
    var writeRE = /^((.+)\.)?write$/

    function hasPermission(permission) {
        if (permission === "") {
            return true;
        }
        if (!RED.settings.user) {
            return true;
        }
        return checkPermission(RED.settings.user.permissions||"",permission);
    }
    function checkPermission(userScope,permission) {
        if (permission === "") {
            return true;
        }
        var i;

        if (Array.isArray(permission)) {
            // Multiple permissions requested - check each one
            for (i=0;i<permission.length;i++) {
                if (!checkPermission(userScope,permission[i])) {
                    return false;
                }
            }
            // All permissions check out
            return true;
        }

        if (Array.isArray(userScope)) {
            if (userScope.length === 0) {
                return false;
            }
            for (i=0;i<userScope.length;i++) {
                if (checkPermission(userScope[i],permission)) {
                    return true;
                }
            }
            return false;
        }

        if (userScope === "*" || userScope === permission) {
            return true;
        }

        if (userScope === "read" || userScope === "*.read") {
            return readRE.test(permission);
        } else if (userScope === "write" || userScope === "*.write") {
            return writeRE.test(permission);
        }
        return false;
    }

    function generateUserIcon(user) {
        const userIcon = $('<span class="red-ui-user-profile"></span>')
        if (user.image) {
            userIcon.addClass('has_profile_image')
            userIcon.css({
                backgroundImage: "url("+user.image+")",
            })
        } else if (user.anonymous || (!user.username && !user.email)) {
            $('<i class="fa fa-user"></i>').appendTo(userIcon);
        } else {
            $('<span>').text((user.username || user.email).substring(0,2)).appendTo(userIcon);
        }
        if (user.profileColor !== undefined) {
            userIcon.addClass('red-ui-user-profile-color-' + user.profileColor)
        }
        return userIcon
    }

    return {
        init: init,
        login: login,
        logout: logout,
        hasPermission: hasPermission,
        generateUserIcon
    }

})();
;/**
 * 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.
 **/

RED.comms = (function() {

    var errornotification = null;
    var clearErrorTimer = null;
    var connectCountdownTimer = null;
    var connectCountdown = 10;
    var subscriptions = {};
    var ws;
    var pendingAuth = false;
    var reconnectAttempts = 0;
    var active = false;

    RED.events.on('login', function(username) {
        // User has logged in
        // Need to upgrade the connection to be authenticated
        if (ws && ws.readyState == 1) {
            const auth_tokens = RED.settings.get("auth-tokens");
            ws.send(JSON.stringify({auth:auth_tokens.access_token}))
        }
    })

    function connectWS() {
        active = true;
        var wspath;

        if (RED.settings.apiRootUrl) {
            var m = /^(https?):\/\/(.*)$/.exec(RED.settings.apiRootUrl);
            if (m) {
                console.log(m);
                wspath = "ws"+(m[1]==="https"?"s":"")+"://"+m[2]+"comms";
            }
        } else {
            var path = location.hostname;
            var port = location.port;
            if (port.length !== 0) {
                path = path+":"+port;
            }
            path = path+document.location.pathname;
            path = path+(path.slice(-1) == "/"?"":"/")+"comms";
            wspath = "ws"+(document.location.protocol=="https:"?"s":"")+"://"+path;
        }

        var auth_tokens = RED.settings.get("auth-tokens");
        pendingAuth = (auth_tokens!=null);

        function completeConnection() {
            for (var t in subscriptions) {
                if (subscriptions.hasOwnProperty(t)) {
                    ws.send(JSON.stringify({subscribe:t}));
                }
            }
            emit('connect')
        }

        ws = new WebSocket(wspath);
        ws.onopen = function() {
            reconnectAttempts = 0;
            if (errornotification) {
                clearErrorTimer = setTimeout(function() {
                    errornotification.close();
                    errornotification = null;
                },1000);
            }
            if (pendingAuth) {
                ws.send(JSON.stringify({auth:auth_tokens.access_token}));
            } else {
                completeConnection();
            }
        }
        ws.onmessage = function(event) {
            var message = JSON.parse(event.data);
            if (message.auth) {
                if (pendingAuth) {
                    if (message.auth === "ok") {
                        pendingAuth = false;
                        completeConnection();
                    } else if (message.auth === "fail") {
                        // anything else is an error...
                        active = false;
                        RED.user.login({updateMenu:true},function() {
                            connectWS();
                        })
                    }
                } else if (message.auth === "fail") {
                    // Our current session has expired
                    active = false;
                    RED.user.login({updateMenu:true},function() {
                        connectWS();
                    })
                }
            } else {
                // Otherwise, 'message' is an array of actual comms messages
                for (var m = 0; m < message.length; m++) {
                    var msg = message[m];
                    if (msg.topic) {
                        for (var t in subscriptions) {
                            if (subscriptions.hasOwnProperty(t)) {
                                var re = new RegExp("^"+t.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g,"\\$1").replace(/\+/g,"[^/]+").replace(/\/#$/,"(\/.*)?")+"$");
                                if (re.test(msg.topic)) {
                                    var subscribers = subscriptions[t];
                                    if (subscribers) {
                                        for (var i=0;i<subscribers.length;i++) {
                                            subscribers[i](msg.topic,msg.data);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        };
        ws.onclose = function() {
            if (!active) {
                return;
            }
            if (clearErrorTimer) {
                clearTimeout(clearErrorTimer);
                clearErrorTimer = null;
            }
            reconnectAttempts++;
            if (reconnectAttempts < 10) {
                setTimeout(connectWS,1000);
                if (reconnectAttempts > 5 && errornotification == null) {
                    errornotification = RED.notify(RED._("notification.errors.lostConnection"),"error",true);
                }
            } else if (reconnectAttempts < 20) {
                setTimeout(connectWS,2000);
            } else {
                connectCountdown = 60;
                connectCountdownTimer = setInterval(function() {
                    connectCountdown--;
                    if (connectCountdown === 0) {
                        errornotification.update(RED._("notification.errors.lostConnection"));
                        clearInterval(connectCountdownTimer);
                        connectWS();
                    } else {
                        var msg = RED._("notification.errors.lostConnectionReconnect",{time: connectCountdown})+' <a href="#">'+ RED._("notification.errors.lostConnectionTry")+'</a>';
                        errornotification.update(msg,{silent:true});
                        $(errornotification).find("a").on("click", function(e) {
                            e.preventDefault();
                            errornotification.update(RED._("notification.errors.lostConnection"),{silent:true});
                            clearInterval(connectCountdownTimer);
                            connectWS();
                        })
                    }
                },1000);
            }

        }
    }

    function subscribe(topic,callback) {
        if (subscriptions[topic] == null) {
            subscriptions[topic] = [];
        }
        subscriptions[topic].push(callback);
        if (ws && ws.readyState == 1) {
            ws.send(JSON.stringify({subscribe:topic}));
        }
    }

    function unsubscribe(topic,callback) {
        if (subscriptions[topic]) {
            for (var i=0;i<subscriptions[topic].length;i++) {
                if (subscriptions[topic][i] === callback) {
                    subscriptions[topic].splice(i,1);
                    break;
                }
            }
            if (subscriptions[topic].length === 0) {
                delete subscriptions[topic];
            }
        }
    }

    function send(topic, msg) {
        if (ws && ws.readyState == 1) {
            ws.send(JSON.stringify({
                topic,
                data: msg
            }))
        }
    }

    const eventHandlers = {};
    function on(evt,func) {
        eventHandlers[evt] = eventHandlers[evt]||[];
        eventHandlers[evt].push(func);
    }
    function off(evt,func) {
        const handler = eventHandlers[evt];
        if (handler) {
            for (let i=0;i<handler.length;i++) {
                if (handler[i] === func) {
                    handler.splice(i,1);
                    return;
                }
            }
        }
    }
    function emit() {
        const evt = arguments[0]
        const args = Array.prototype.slice.call(arguments,1);
        if (eventHandlers[evt]) {
            let cpyHandlers = [...eventHandlers[evt]];
            for (let i=0;i<cpyHandlers.length;i++) {
                try {
                    cpyHandlers[i].apply(null, args);
                } catch(err) {
                    console.warn("RED.comms.emit error: ["+evt+"] "+(err.toString()));
                    console.warn(err);
                }
            }
        }
    }

    return {
        connect: connectWS,
        subscribe: subscribe,
        unsubscribe:unsubscribe,
        on,
        off,
        send
    }
})();
;RED.runtime = (function() {
    let state = ""
    let settings = { ui: false, enabled: false };
    const STOPPED = "stop"
    const STARTED = "start"
    const SAFE = "safe"

    return {
        init: function() {
            // refresh the current runtime status from server
            settings = Object.assign({}, settings, RED.settings.runtimeState);
            RED.events.on("runtime-state", function(msg) {
                if (msg.state) {
                    const currentState = state
                    state = msg.state
                    $(".red-ui-flow-node-button").toggleClass("red-ui-flow-node-button-stopped", state !== STARTED)
                    if(settings.enabled === true && settings.ui === true) {
                        RED.menu.setVisible("deploymenu-item-runtime-stop", state === STARTED)
                        RED.menu.setVisible("deploymenu-item-runtime-start", state !== STARTED)
                    }
                    // Do not notify the user about this event if:
                    // - This is the very first event we've received after loading the editor (currentState = '')
                    // - The state matches what we already thought was the case (state === currentState)
                    // - The event was triggered by a deploy (msg.deploy === true)
                    // - The event is a safe mode event - that gets notified separately
                    if (currentState !== '' && state !== currentState && !msg.deploy && state !== SAFE) {
                        RED.notify(RED._("notification.state.flows"+(state === STOPPED?'Stopped':'Started'), msg), "success")
                    }
                }
            });
        },
        get started() {
            return state === STARTED
        }
    }
})()
;RED.multiplayer = (function () {

    // activeSessionId - used to identify sessions across websocket reconnects
    let activeSessionId

    let headerWidget
    // Map of session id to { session:'', user:{}, location:{}}
    let sessions = {}
    // Map of username to { user:{}, sessions:[] }
    let users = {}

    function addUserSession (session) {
        if (sessions[session.session]) {
            // This is an existing connection that has been authenticated
            const existingSession = sessions[session.session]
            if (existingSession.user.username !== session.user.username) {
                removeUserHeaderButton(users[existingSession.user.username])
            }
        }
        sessions[session.session] = session
        const user = users[session.user.username] = users[session.user.username] || {
            user: session.user,
            sessions: []
        }
        if (session.user.profileColor === undefined) {
            session.user.profileColor = (1 + Math.floor(Math.random() * 5))
        }
        session.location = session.location || {}
        user.sessions.push(session)

        if (session.session === activeSessionId) {
            // This is the current user session - do not add a extra button for them
        } else {
            if (user.sessions.length === 1) {
                if (user.button) {
                    clearTimeout(user.inactiveTimeout)
                    clearTimeout(user.removeTimeout)
                    user.button.removeClass('inactive')
                } else {
                    addUserHeaderButton(user)
                }
            }
            sessions[session.session].location = session.location
            updateUserLocation(session.session)
        }
    }

    function removeUserSession (sessionId, isDisconnected) {
        removeUserLocation(sessionId)
        const session = sessions[sessionId]
        delete sessions[sessionId]
        const user = users[session.user.username]
        const i = user.sessions.indexOf(session)
        user.sessions.splice(i, 1)
        if (isDisconnected) {
            removeUserHeaderButton(user)
        } else {
            if (user.sessions.length === 0) {
                // Give the user 5s to reconnect before marking inactive
                user.inactiveTimeout = setTimeout(() => {
                    user.button.addClass('inactive')
                    // Give the user further 20 seconds to reconnect before removing them
                    // from the user toolbar entirely
                    user.removeTimeout = setTimeout(() => {
                        removeUserHeaderButton(user)
                    }, 20000)
                }, 5000)
            }
        }
    }

    function addUserHeaderButton (user) {
        user.button = $('<li class="red-ui-multiplayer-user"><button type="button" class="red-ui-multiplayer-user-icon"></button></li>')
            .attr('data-username', user.user.username)
            .prependTo("#red-ui-multiplayer-user-list");
        var button = user.button.find("button")
        RED.popover.tooltip(button, user.user.username)
        button.on('click', function () {
            const location = user.sessions[0].location
            revealUser(location)
        })

        const userProfile = RED.user.generateUserIcon(user.user)
        userProfile.appendTo(button)
    }

    function removeUserHeaderButton (user) {
        user.button.remove()
        delete user.button
    }

    function getLocation () {
        const location = {
            workspace: RED.workspaces.active()
        }
        const editStack = RED.editor.getEditStack()
        for (let i = editStack.length - 1; i >= 0; i--) {
            if (editStack[i].id) {
                location.node = editStack[i].id
                break
            }
        }
        if (isInWorkspace) {
            const chart = $('#red-ui-workspace-chart')
            const chartOffset = chart.offset()
            const scaleFactor = RED.view.scale()
            location.cursor = {
                x: (lastPosition[0] - chartOffset.left + chart.scrollLeft()) / scaleFactor,
                y: (lastPosition[1] - chartOffset.top + chart.scrollTop()) / scaleFactor
            }
        }
        return location
    }

    let publishLocationTimeout
    let lastPosition = [0,0]
    let isInWorkspace = false

    function publishLocation () {
        if (!publishLocationTimeout) {
            publishLocationTimeout = setTimeout(() => {
                const location = getLocation()
                if (location.workspace !== 0) {
                    log('send', 'multiplayer/location', location)
                    RED.comms.send('multiplayer/location', location)
                }
                publishLocationTimeout = null
            }, 100)
        }
    }


    function revealUser(location, skipWorkspace) {
        if (location.node) {
            // Need to check if this is a known node, so we can fall back to revealing
            // the workspace instead
            const node = RED.nodes.node(location.node)
            if (node) {
                RED.view.reveal(location.node)
            } else if (!skipWorkspace && location.workspace) {
                RED.view.reveal(location.workspace)
            }
        } else if (!skipWorkspace && location.workspace) {
            RED.view.reveal(location.workspace)
        }
    }

    const workspaceTrays = {}
    function getWorkspaceTray(workspaceId) {
        // console.log('get tray for',workspaceId)
        if (!workspaceTrays[workspaceId]) {
            const tray = $('<div class="red-ui-multiplayer-users-tray"></div>')
            const users = []
            const userIcons = {}

            const userCountIcon = $(`<div class="red-ui-multiplayer-user-location"><span class="red-ui-user-profile red-ui-multiplayer-user-count"><span></span></span></div>`)
            const userCountSpan = userCountIcon.find('span span')
            userCountIcon.hide()
            userCountSpan.text('')
            userCountIcon.appendTo(tray)
            const userCountTooltip = RED.popover.tooltip(userCountIcon, function () {
                    const content = $('<div>')
                    users.forEach(sessionId => {
                        $('<div>').append($('<a href="#">').text(sessions[sessionId].user.username).on('click', function (evt) {
                            evt.preventDefault()
                            revealUser(sessions[sessionId].location, true)
                            userCountTooltip.close()
                        })).appendTo(content)
                    })
                    return content
                },
                null,
                true
            )

            const updateUserCount = function () {
                const maxShown = 2
                const children = tray.children()
                children.each(function (index, element) {
                    const i = users.length - index
                    if (i > maxShown) {
                        $(this).hide()
                    } else if (i >= 0) {
                        $(this).show()
                    }
                })
                if (users.length < maxShown + 1) { 
                    userCountIcon.hide()
                } else {
                    userCountSpan.text('+'+(users.length - maxShown))
                    userCountIcon.show()
                }
            }
            workspaceTrays[workspaceId] = {
                attached: false,
                tray,
                users,
                userIcons,
                addUser: function (sessionId) {
                    if (users.indexOf(sessionId) === -1) {
                        // console.log(`addUser ws:${workspaceId} session:${sessionId}`)
                        users.push(sessionId)
                        const userLocationId = `red-ui-multiplayer-user-location-${sessionId}`
                        const userLocationIcon = $(`<div class="red-ui-multiplayer-user-location" id="${userLocationId}"></div>`)
                        RED.user.generateUserIcon(sessions[sessionId].user).appendTo(userLocationIcon)
                        userLocationIcon.prependTo(tray)
                        RED.popover.tooltip(userLocationIcon, sessions[sessionId].user.username)
                        userIcons[sessionId] = userLocationIcon
                        updateUserCount()
                    }
                },
                removeUser: function (sessionId) {
                    // console.log(`removeUser ws:${workspaceId} session:${sessionId}`)
                    const userLocationId = `red-ui-multiplayer-user-location-${sessionId}`
                    const index = users.indexOf(sessionId)
                    if (index > -1) {
                        users.splice(index, 1)
                        userIcons[sessionId].remove()
                        delete userIcons[sessionId]
                    }
                    updateUserCount()
                },
                updateUserCount
            }
        }
        const trayDef = workspaceTrays[workspaceId]
        if (!trayDef.attached) {
            const workspaceTab = $(`#red-ui-tab-${workspaceId}`)
            if (workspaceTab.length > 0) {
                trayDef.attached = true
                trayDef.tray.appendTo(workspaceTab)
                trayDef.users.forEach(sessionId => {
                    trayDef.userIcons[sessionId].on('click', function (evt) {
                        revealUser(sessions[sessionId].location, true)
                    })
                })
            }
        }
        return workspaceTrays[workspaceId]
    }
    function attachWorkspaceTrays () {
        let viewTouched = false
        for (let sessionId of Object.keys(sessions)) {
            const location = sessions[sessionId].location
            if (location) {
                if (location.workspace) {
                    getWorkspaceTray(location.workspace).updateUserCount()
                }
                if (location.node) {
                    addUserToNode(sessionId, location.node)
                    viewTouched = true
                }
            }
        }
        if (viewTouched) {
            RED.view.redraw()
        }
    }

    function addUserToNode(sessionId, nodeId) {
        const node = RED.nodes.node(nodeId)
        if (node) {
            if (!node._multiplayer) {
                node._multiplayer = {
                    users: [sessionId]
                }
                node._multiplayer_refresh = true
            } else {
                if (node._multiplayer.users.indexOf(sessionId) === -1) {
                    node._multiplayer.users.push(sessionId)
                    node._multiplayer_refresh = true
                }
            }
        }
    }
    function removeUserFromNode(sessionId, nodeId) {
        const node = RED.nodes.node(nodeId)
        if (node && node._multiplayer) {
            const i = node._multiplayer.users.indexOf(sessionId)
            if (i > -1) {
                node._multiplayer.users.splice(i, 1)
            }
            if (node._multiplayer.users.length === 0) {
                delete node._multiplayer
            } else {
                node._multiplayer_refresh = true
            }
        }

    }

    function removeUserLocation (sessionId) {
        updateUserLocation(sessionId, {})
        removeUserCursor(sessionId)
    }
    function removeUserCursor (sessionId) {
        // return
        if (sessions[sessionId]?.cursor) {
            sessions[sessionId].cursor.parentNode.removeChild(sessions[sessionId].cursor)
            delete sessions[sessionId].cursor
        }
    }

    function updateUserLocation (sessionId, location) {
        let viewTouched = false
        const oldLocation = sessions[sessionId].location
        if (location) {
            if (oldLocation.workspace !== location.workspace) {
                // console.log('removing', sessionId, oldLocation.workspace)
                workspaceTrays[oldLocation.workspace]?.removeUser(sessionId)
            }
            if (oldLocation.node !== location.node) {
                removeUserFromNode(sessionId, oldLocation.node)
                viewTouched = true
            }
            sessions[sessionId].location = location
        } else {
            location = sessions[sessionId].location
        }
        // console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`)
        if (location.workspace) {
            getWorkspaceTray(location.workspace).addUser(sessionId)
            if (location.cursor && location.workspace === RED.workspaces.active()) {
                if (!sessions[sessionId].cursor) {
                    const user = sessions[sessionId].user
                    const cursorIcon = document.createElementNS("http://www.w3.org/2000/svg","g");
                    cursorIcon.setAttribute("class", "red-ui-multiplayer-annotation")
                    cursorIcon.appendChild(createAnnotationUser(user, true))
                    $(cursorIcon).css({
                        transform: `translate( ${location.cursor.x}px, ${location.cursor.y}px)`,
                        transition: 'transform 0.1s linear'
                    })
                    $("#red-ui-workspace-chart svg").append(cursorIcon)
                    sessions[sessionId].cursor = cursorIcon
                } else {
                    const cursorIcon = sessions[sessionId].cursor
                    $(cursorIcon).css({
                        transform: `translate( ${location.cursor.x}px, ${location.cursor.y}px)`
                    })
    
                }
            } else if (sessions[sessionId].cursor) {
                removeUserCursor(sessionId)
            }
        }
        if (location.node) {
            addUserToNode(sessionId, location.node)
            viewTouched = true
        }
        if (viewTouched) {
            RED.view.redraw()
        }
    }

    // function refreshUserLocations () {
    //     for (const session of Object.keys(sessions)) {
    //         if (session !== activeSessionId) {
    //             updateUserLocation(session)
    //         }
    //     }
    // }


    function createAnnotationUser(user, pointer = false) {
        const radius = 20
        const halfRadius = radius/2
        const group = document.createElementNS("http://www.w3.org/2000/svg","g");
        const badge = document.createElementNS("http://www.w3.org/2000/svg","path");
        let shapePath
        if (!pointer) {
            shapePath = `M 0 ${halfRadius} a ${halfRadius} ${halfRadius} 0 1 1 ${radius} 0 a ${halfRadius} ${halfRadius} 0 1 1 -${radius} 0 z`
        } else {
            shapePath = `M 0 0 h ${halfRadius} a ${halfRadius} ${halfRadius} 0 1 1 -${halfRadius} ${halfRadius} z`
        }
        badge.setAttribute('d', shapePath)
        badge.setAttribute("class", "red-ui-multiplayer-annotation-background")
        group.appendChild(badge)
        if (user && user.profileColor !== undefined) {
            badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor)
        }
        if (user && user.image) {
            const image = document.createElementNS("http://www.w3.org/2000/svg","image");
            image.setAttribute("width", radius)
            image.setAttribute("height", radius)
            image.setAttribute("href", user.image)
            image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")")
            group.appendChild(image)
        } else if (user && user.anonymous) {
            const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle");
            anonIconHead.setAttribute("cx", radius/2)
            anonIconHead.setAttribute("cy", radius/2 - 2)
            anonIconHead.setAttribute("r", 2.4)
            anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label");
            group.appendChild(anonIconHead)
            const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path");
            anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label");
            // anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`);
            anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5  2.5 5.5  0 4.5  z`);
            group.appendChild(anonIconBody)
        } else {
            const label = document.createElementNS("http://www.w3.org/2000/svg","text");
            if (user.username || user.email) {
                label.setAttribute("class","red-ui-multiplayer-annotation-label");
                label.textContent = (user.username || user.email).substring(0,2)
            } else {
                label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count")
                label.textContent = 'nr'
            }
            label.setAttribute("text-anchor", "middle")
            label.setAttribute("x",radius/2);
            label.setAttribute("y",radius/2 + 3);
            group.appendChild(label)
        }
        const border = document.createElementNS("http://www.w3.org/2000/svg","path");
        border.setAttribute('d', shapePath)
        border.setAttribute("class", "red-ui-multiplayer-annotation-border")
        group.appendChild(border)
        return group
    }

    return {
        init: function () {

            
            
            RED.view.annotations.register("red-ui-multiplayer",{
                type: 'badge',
                align: 'left',
                class: "red-ui-multiplayer-annotation",
                show: "_multiplayer",
                refresh: "_multiplayer_refresh",
                element: function(node) {
                    const containerGroup = document.createElementNS("http://www.w3.org/2000/svg","g");
                    containerGroup.setAttribute("transform","translate(0,-4)")
                    if (node._multiplayer) {
                        let y = 0
                        for (let i = Math.min(1, node._multiplayer.users.length - 1); i >= 0; i--) {
                            const user = sessions[node._multiplayer.users[i]].user
                            const group = createAnnotationUser(user)
                            group.setAttribute("transform","translate("+y+",0)")
                            y += 15
                            containerGroup.appendChild(group)
                        }
                        if (node._multiplayer.users.length > 2) {
                            const group = createAnnotationUser('+'+(node._multiplayer.users.length - 2))
                            group.setAttribute("transform","translate("+y+",0)")
                            y += 12
                            containerGroup.appendChild(group)
                        }

                    }
                    return containerGroup;
                },
                tooltip: node => { return node._multiplayer.users.map(u => sessions[u].user.username).join('\n') }
            });


            // activeSessionId = RED.settings.getLocal('multiplayer:sessionId')
            // if (!activeSessionId) {
                activeSessionId = RED.nodes.id()
            //     RED.settings.setLocal('multiplayer:sessionId', activeSessionId)
            //     log('Session ID (new)', activeSessionId)
            // } else {
                log('Session ID', activeSessionId)
            // }
            
            headerWidget = $('<li><ul id="red-ui-multiplayer-user-list"></ul></li>').prependTo('.red-ui-header-toolbar')

            RED.comms.on('connect', () => {
                const location = getLocation()
                const connectInfo = {
                    session: activeSessionId
                }
                if (location.workspace !== 0) {
                    connectInfo.location = location
                }
                RED.comms.send('multiplayer/connect', connectInfo)
            })
            RED.comms.subscribe('multiplayer/#', (topic, msg) => {
                log('recv', topic, msg)
                if (topic === 'multiplayer/init') {
                    // We have just reconnected, runtime has sent state to
                    // initialise the world
                    sessions = {}
                    users = {}
                    $('#red-ui-multiplayer-user-list').empty()

                    msg.sessions.forEach(session => {
                        addUserSession(session)
                    })
                } else if (topic === 'multiplayer/connection-added') {
                    addUserSession(msg)
                } else if (topic === 'multiplayer/connection-removed') {
                    removeUserSession(msg.session, msg.disconnected)
                } else if (topic === 'multiplayer/location') {
                    const session = msg.session
                    delete msg.session
                    updateUserLocation(session, msg)
                }
            })

            RED.events.on('workspace:change', (event) => {
                getWorkspaceTray(event.workspace)
                publishLocation()
            })
            RED.events.on('editor:open', () => {
                publishLocation()
            })
            RED.events.on('editor:close', () => {
                publishLocation()
            })
            RED.events.on('editor:change', () => {
                publishLocation()
            })
            RED.events.on('login', () => {
                publishLocation()
            })
            RED.events.on('flows:loaded', () => {
                attachWorkspaceTrays()
            })
            RED.events.on('workspace:close', (event) => {
                // A subflow tab has been closed. Need to mark its tray as detached
                if (workspaceTrays[event.workspace]) {
                    workspaceTrays[event.workspace].attached = false
                }
            })
            RED.events.on('logout', () => {
                const disconnectInfo = {
                    session: activeSessionId
                }
                RED.comms.send('multiplayer/disconnect', disconnectInfo)
                RED.settings.removeLocal('multiplayer:sessionId')
            })
            
            const chart = $('#red-ui-workspace-chart')
            chart.on('mousemove', function (evt) {
                lastPosition[0] = evt.clientX
                lastPosition[1] = evt.clientY
                publishLocation()
            })
            chart.on('scroll', function (evt) {
                publishLocation()
            })
            chart.on('mouseenter', function () {
                isInWorkspace = true
                publishLocation()
            })
            chart.on('mouseleave', function () {
                isInWorkspace = false
                publishLocation()
            })
        }
    }

    function log() {
        if (RED.multiplayer.DEBUG) {
            console.log('[multiplayer]', ...arguments)
        }
    }
})();
;/**
 * 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.
 **/
RED.text = {};
RED.text.bidi = (function() {
    var textDir = "";
    var LRE = "\u202A",
        RLE = "\u202B",
        PDF = "\u202C";

    function isRTLValue(stringValue) {
        var length = stringValue.length;
        for (var i=0;i<length;i++) {
            if (isBidiChar(stringValue.charCodeAt(i))) {
                return true;
            }
            else if(isLatinChar(stringValue.charCodeAt(i))) {
                return false;
            }
         }
         return false;
    }

    function isBidiChar(c)  {
        return (c >= 0x05d0 && c <= 0x05ff)||
               (c >= 0x0600 && c <= 0x065f)||
               (c >= 0x066a && c <= 0x06ef)||
               (c >= 0x06fa && c <= 0x07ff)||
               (c >= 0xfb1d && c <= 0xfdff)||
               (c >= 0xfe70 && c <= 0xfefc);
    }

    function isLatinChar(c){
        return (c > 64 && c < 91)||(c > 96 && c < 123)
    }

    /**
     * Determines the text direction of a given string.
     * @param value - the string
     */
    function resolveBaseTextDir(value) {
        if (textDir == "auto") {
            if (isRTLValue(value)) {
                return "rtl";
            } else {
                return "ltr";
            }
        }
        else {
            return textDir;
        }
    }

    function onInputChange() {
        $(this).attr("dir", resolveBaseTextDir($(this).val()));
    }

    /**
     * Adds event listeners to the Input to ensure its text-direction attribute
     * is properly set based on its content.
     * @param input - the input field
     */
    function prepareInput(input) {
        input.on("keyup",onInputChange).on("paste",onInputChange).on("cut",onInputChange);
        // Set the initial text direction
        onInputChange.call(input);
    }

    /**
     * Enforces the text direction of a given string by adding
     * UCC (Unicode Control Characters)
     * @param value - the string
     */
    function enforceTextDirectionWithUCC(value) {
        if (value) {
            var dir = resolveBaseTextDir(value);
            if (dir == "ltr") {
               return LRE + value + PDF;
            }
            else if (dir == "rtl") {
               return RLE + value + PDF;
            }
        }
        return value;
    }

    /**
     * Enforces the text direction for all the spans with style red-ui-text-bidi-aware under
     * workspace or sidebar div
     */
    function enforceTextDirectionOnPage() {
        $("#red-ui-workspace").find('span.red-ui-text-bidi-aware').each(function() {
            $(this).attr("dir", resolveBaseTextDir($(this).html()));
        });
        $("#red-ui-sidebar").find('span.red-ui-text-bidi-aware').each(function() {
            $(this).attr("dir", resolveBaseTextDir($(this).text()));
        });
    }

    /**
     * Sets the text direction preference
     * @param dir - the text direction preference
     */
    function setTextDirection(dir) {
        textDir = dir;
        RED.nodes.eachNode(function(n) { n.dirty = true;});
        RED.view.redraw();
        RED.palette.refresh();
        enforceTextDirectionOnPage();
    }

    return {
        setTextDirection: setTextDirection,
        enforceTextDirectionWithUCC: enforceTextDirectionWithUCC,
        resolveBaseTextDir: resolveBaseTextDir,
        prepareInput: prepareInput
    }
})();
;/**
 * 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.
 **/
RED.text.format = (function() {

    var TextSegment = (function() {
        var TextSegment = function (obj) {
            this.content = "";
            this.actual = "";
            this.textDirection = "";
            this.localGui = "";
            this.isVisible = true;
            this.isSeparator = false;
            this.isParsed = false;
            this.keep = false;
            this.inBounds = false;
            this.inPoints = false;
            var prop = "";
            for (prop in obj) {
                if (obj.hasOwnProperty(prop)) {
                    this[prop] = obj[prop];
                }
            }
        };
        return TextSegment;
    })();

    var tools = (function() {
        function initBounds(bounds) {
            if (!bounds) {
                return false;
            }
            if (typeof(bounds.start) === "undefined") {
                bounds.start = "";
            }
            if (typeof(bounds.end) === "undefined") {
                bounds.end = "";
            }
            if (typeof(bounds.startAfter) !== "undefined") {
                bounds.start = bounds.startAfter;
                bounds.after = true;
            } else {
                bounds.after = false;
            }
            if (typeof(bounds.endBefore) !== "undefined") {
                bounds.end = bounds.endBefore;
                bounds.before = true;
            } else {
                bounds.before = false;
            }
            var startPos = parseInt(bounds.startPos, 10);
            if (!isNaN(startPos)) {
                bounds.usePos = true;
            } else {
                bounds.usePos = false;
            }
            var bLength = parseInt(bounds.length, 10);
            if (!isNaN(bLength)) {
                bounds.useLength = true;
            } else {
                bounds.useLength = false;
            }
            bounds.loops = typeof(bounds.loops) !== "undefined" ? !!bounds.loops : true;
            return true;
        }

        function getBounds(segment, src) {
            var bounds = {};
            for (var prop in src) {
                if (src.hasOwnProperty(prop)) {
                    bounds[prop] = src[prop];
                }
            }
            var content = segment.content;
            var usePos = bounds.usePos && bounds.startPos < content.length;
            if (usePos) {
                bounds.start = "";
                bounds.loops = false;
            }
            bounds.bStart = usePos ? bounds.startPos : bounds.start.length > 0 ? content.indexOf(bounds.start) : 0;
            var useLength = bounds.useLength && bounds.length > 0 && bounds.bStart + bounds.length < content.length;
            if (useLength) {
                bounds.end = "";
            }
            bounds.bEnd = useLength ? bounds.bStart + bounds.length : bounds.end.length > 0 ?
                    content.indexOf(bounds.end, bounds.bStart + bounds.start.length) + 1 : content.length;
            if (!bounds.after) {
                bounds.start = "";
            }
            if (!bounds.before) {
                bounds.end = "";
            }
            return bounds;
        }

        return {
            handleSubcontents: function (segments, args, subs, origContent, locale) { // jshint unused: false
                if (!subs.content || typeof(subs.content) !== "string" || subs.content.length === 0) {
                    return segments;
                }
                var sLoops = true;
                if (typeof(subs.loops) !== "undefined") {
                    sLoops = !!subs.loops;
                }
                for (var j = 0; true; j++) {
                    if (j >= segments.length) {
                        break;
                    }
                    if (segments[j].isParsed || segments.keep || segments[j].isSeparator) {
                        continue;
                    }
                    var content = segments[j].content;
                    var start = content.indexOf(subs.content);
                    if (start < 0) {
                        continue;
                    }
                    var end;
                    var length = 0;
                    if (subs.continued) {
                        do {
                            length++;
                            end = content.indexOf(subs.content, start + length * subs.content.length);
                        } while (end === 0);
                    } else {
                        length = 1;
                    }
                    end = start + length * subs.content.length;
                    segments.splice(j, 1);
                    if (start > 0) {
                        segments.splice(j, 0, new TextSegment({
                            content: content.substring(0, start),
                            localGui: args.dir,
                            keep: true
                        }));
                        j++;
                    }
                    segments.splice(j, 0, new TextSegment({
                        content: content.substring(start, end),
                        textDirection: subs.subDir,
                        localGui: args.dir
                    }));
                    if (end < content.length) {
                        segments.splice(j + 1, 0, new TextSegment({
                            content: content.substring(end, content.length),
                            localGui: args.dir,
                            keep: true
                        }));
                    }
                    if (!sLoops) {
                        break;
                    }
                }
            },

            handleBounds: function (segments, args, aBounds, origContent, locale) {
                for (var i = 0; i < aBounds.length; i++) {
                    if (!initBounds(aBounds[i])) {
                        continue;
                    }
                    for (var j = 0; true; j++) {
                        if (j >= segments.length) {
                            break;
                        }
                        if (segments[j].isParsed || segments[j].inBounds || segments.keep || segments[j].isSeparator) {
                            continue;
                        }
                        var bounds = getBounds(segments[j], aBounds[i]);
                        var start = bounds.bStart;
                        var end = bounds.bEnd;
                        if (start < 0 || end < 0) {
                            continue;
                        }
                        var content = segments[j].content;

                        segments.splice(j, 1);
                        if (start > 0) {
                            segments.splice(j, 0, new TextSegment({
                                content: content.substring(0, start),
                                localGui: args.dir,
                                keep: true
                            }));
                            j++;
                        }
                        if (bounds.start) {
                            segments.splice(j, 0, new TextSegment({
                                content: bounds.start,
                                localGui: args.dir,
                                isSeparator: true
                            }));
                            j++;
                        }
                        segments.splice(j, 0, new TextSegment({
                            content: content.substring(start + bounds.start.length, end - bounds.end.length),
                            textDirection: bounds.subDir,
                            localGui: args.dir,
                            inBounds: true
                        }));
                        if (bounds.end) {
                            j++;
                            segments.splice(j, 0, new TextSegment({
                                content: bounds.end,
                                localGui: args.dir,
                                isSeparator: true
                            }));
                        }
                        if (end + bounds.end.length < content.length) {
                            segments.splice(j + 1, 0, new TextSegment({
                                content: content.substring(end + bounds.end.length, content.length),
                                localGui: args.dir,
                                keep: true
                            }));
                        }
                        if (!bounds.loops) {
                            break;
                        }
                    }
                }
                for (i = 0; i < segments.length; i++) {
                    segments[i].inBounds = false;
                }
                return segments;
            },

            handleCases: function (segments, args, cases, origContent, locale) {
                if (cases.length === 0) {
                    return segments;
                }
                var hArgs = {};
                for (var prop in args) {
                    if (args.hasOwnProperty(prop)) {
                        hArgs[prop] = args[prop];
                    }
                }
                for (var i =  0; i < cases.length; i++) {
                    if (!cases[i].handler || typeof(cases[i].handler.handle) !== "function") {
                        cases[i].handler = args.commonHandler;
                    }
                    if (cases[i].args) {
                        hArgs.cases = cases[i].args.cases;
                        hArgs.points = cases[i].args.points;
                        hArgs.bounds = cases[i].args.bounds;
                        hArgs.subs = cases[i].args.subs;
                    } else {
                        hArgs.cases = [];
                        hArgs.points = [];
                        hArgs.bounds = [];
                        hArgs.subs = {};
                    }
                    cases[i].handler.handle(origContent, segments, hArgs, locale);
                }
                return segments;
            },

            handlePoints: function (segments, args, points, origContent, locale) { //jshint unused: false
                for (var i = 0; i < points.length; i++) {
                    for (var j = 0; true; j++) {
                        if (j >= segments.length) {
                            break;
                        }
                        if (segments[j].isParsed || segments[j].keep || segments[j].isSeparator) {
                            continue;
                        }
                        var content = segments[j].content;
                        var pos = content.indexOf(points[i]);
                        if (pos >= 0) {
                            segments.splice(j, 1);
                            if (pos > 0) {
                                segments.splice(j, 0, new TextSegment({
                                    content: content.substring(0, pos),
                                    textDirection: args.subDir,
                                    localGui: args.dir,
                                    inPoints: true
                                }));
                                j++;
                            }
                            segments.splice(j, 0, new TextSegment({
                                content: points[i],
                                localGui: args.dir,
                                isSeparator: true
                            }));
                            if (pos + points[i].length + 1 <= content.length) {
                                segments.splice(j + 1, 0, new TextSegment({
                                    content: content.substring(pos + points[i].length),
                                    textDirection: args.subDir,
                                    localGui: args.dir,
                                    inPoints: true
                                }));
                            }
                        }
                    }
                }
                for (i = 0; i < segments.length; i++) {
                    if (segments[i].keep) {
                        segments[i].keep = false;
                    } else if(segments[i].inPoints){
                        segments[i].isParsed = true;
                        segments[i].inPoints = false;
                    }
                }
                return segments;
            }
        };
    })();

    var common = (function() {
        return {
            handle: function (content, segments, args, locale) {
                var cases = [];
                if (Array.isArray(args.cases)) {
                    cases = args.cases;
                }
                var points = [];
                if (typeof(args.points) !== "undefined") {
                    if (Array.isArray(args.points)) {
                        points = args.points;
                    } else if (typeof(args.points) === "string") {
                        points = args.points.split("");
                    }
                }
                var subs = {};
                if (typeof(args.subs) === "object") {
                    subs = args.subs;
                }
                var aBounds = [];
                if (Array.isArray(args.bounds)) {
                    aBounds = args.bounds;
                }

                tools.handleBounds(segments, args, aBounds, content, locale);
                tools.handleSubcontents(segments, args, subs, content, locale);
                tools.handleCases(segments, args, cases, content, locale);
                tools.handlePoints(segments, args, points, content, locale);
                return segments;
            }
        };
    })();

    var misc = (function() {
        var isBidiLocale = function (locale) {
            var lang = !locale ? "" : locale.split("-")[0];
            if (!lang || lang.length < 2) {
                return false;
            }
            return ["iw", "he", "ar", "fa", "ur"].some(function (bidiLang) {
                return bidiLang === lang;
            });
        };
        var LRE = "\u202A";
        var RLE = "\u202B";
        var PDF = "\u202C";
        var LRM = "\u200E";
        var RLM = "\u200F";
        var LRO = "\u202D";
        var RLO = "\u202E";

        return {
            LRE: LRE,
            RLE: RLE,
            PDF: PDF,
            LRM: LRM,
            RLM: RLM,
            LRO: LRO,
            RLO: RLO,

            getLocaleDetails: function (locale) {
                if (!locale) {
                    locale = typeof navigator === "undefined" ? "" :
                        (navigator.language ||
                        navigator.userLanguage ||
                        "");
                }
                locale = locale.toLowerCase();
                if (isBidiLocale(locale)) {
                    var full = locale.split("-");
                    return {lang: full[0], country: full[1] ? full[1] : ""};
                }
                return {lang: "not-bidi"};
            },

            removeUcc: function (text) {
                if (text) {
                    return text.replace(/[\u200E\u200F\u202A-\u202E]/g, "");
                }
                return text;
            },

            removeTags: function (text) {
                if (text) {
                    return text.replace(/<[^<]*>/g, "");
                }
                return text;
            },

            getDirection: function (text, dir, guiDir, checkEnd) {
                if (dir !== "auto" && (/^(rtl|ltr)$/i).test(dir)) {
                    return dir;
                }
                guiDir = (/^(rtl|ltr)$/i).test(guiDir) ? guiDir : "ltr";
                var txt = !checkEnd ? text : text.split("").reverse().join("");
                var fdc = /[A-Za-z\u05d0-\u065f\u066a-\u06ef\u06fa-\u07ff\ufb1d-\ufdff\ufe70-\ufefc]/.exec(txt);
                return fdc ? (fdc[0] <= "z" ? "ltr" : "rtl") : guiDir;
            },

            hasArabicChar: function (text) {
                var fdc = /[\u0600-\u065f\u066a-\u06ef\u06fa-\u07ff\ufb1d-\ufdff\ufe70-\ufefc]/.exec(text);
                return !!fdc;
            },

            showMarks: function (text, guiDir) {
                var result = "";
                for (var i = 0; i < text.length; i++) {
                    var c = "" + text.charAt(i);
                    switch (c) {
                    case LRM:
                        result += "<LRM>";
                        break;
                    case RLM:
                        result += "<RLM>";
                        break;
                    case LRE:
                        result += "<LRE>";
                        break;
                    case RLE:
                        result += "<RLE>";
                        break;
                    case LRO:
                        result += "<LRO>";
                        break;
                    case RLO:
                        result += "<RLO>";
                        break;
                    case PDF:
                        result += "<PDF>";
                        break;
                    default:
                        result += c;
                    }
                }
                var mark = typeof(guiDir) === "undefined" || !((/^(rtl|ltr)$/i).test(guiDir)) ? "" :
                    guiDir === "rtl" ? RLO : LRO;
                return mark + result + (mark === "" ? "" : PDF);
            },

            hideMarks: function (text) {
                var txt = text.replace(/<LRM>/g, this.LRM).replace(/<RLM>/g, this.RLM).replace(/<LRE>/g, this.LRE);
                return txt.replace(/<RLE>/g, this.RLE).replace(/<LRO>/g, this.LRO).replace(/<RLO>/g, this.RLO).replace(/<PDF>/g, this.PDF);
            },

            showTags: function (text) {
                return "<xmp>" + text + "</xmp>";
            },

            hideTags: function (text) {
                return text.replace(/<xmp>/g,"").replace(/<\/xmp>/g,"");
            }
        };
    })();

    var stext = (function() {
        var stt = {};

        // args
        //   handler: main handler (default - dbidi/stt/handlers/common)
        //   guiDir: GUI direction (default - "ltr")
        //   dir: main stt direction (default - guiDir)
        //   subDir: direction of subsegments
        //   points: array of delimiters (default - [])
        //   bounds: array of definitions of bounds in which handler works
        //   subs: object defines special handling for some substring if found
        //   cases: array of additional modules with their args for handling special cases (default - [])
        function parseAndDisplayStructure(content, fArgs, isHtml, locale) {
            if (!content || !fArgs) {
                return content;
            }
            return displayStructure(parseStructure(content, fArgs, locale), fArgs, isHtml);
        }

        function checkArguments(fArgs, fullCheck) {
            var args = Array.isArray(fArgs)? fArgs[0] : fArgs;
            if (!args.guiDir) {
                args.guiDir = "ltr";
            }
            if (!args.dir) {
                args.dir = args.guiDir;
            }
            if (!fullCheck) {
                return args;
            }
            if (typeof(args.points) === "undefined") {
                args.points = [];
            }
            if (!args.cases) {
                args.cases = [];
            }
            if (!args.bounds) {
                args.bounds = [];
            }
            args.commonHandler = common;
            return args;
        }

        function parseStructure(content, fArgs, locale) {
            if (!content || !fArgs) {
                return new TextSegment({content: ""});
            }
            var args = checkArguments(fArgs, true);
            var segments = [new TextSegment(
                {
                    content: content,
                    actual: content,
                    localGui: args.dir
                })];
            var parse = common.handle;
            if (args.handler && typeof(args.handler) === "function") {
                parse = args.handler.handle;
            }
            parse(content, segments, args, locale);
            return segments;
        }

        function displayStructure(segments, fArgs, isHtml) {
            var args = checkArguments(fArgs, false);
            if (isHtml) {
                return getResultWithHtml(segments, args);
            }
            else {
                return getResultWithUcc(segments, args);
            }
        }

        function getResultWithUcc(segments, args, isHtml) {
            var result = "";
            var checkedDir = "";
            var prevDir = "";
            var stop = false;
            for (var i = 0; i < segments.length; i++) {
                if (segments[i].isVisible) {
                    var dir = segments[i].textDirection;
                    var lDir = segments[i].localGui;
                    if (lDir !== "" && prevDir === "") {
                        result += (lDir === "rtl" ? misc.RLE : misc.LRE);
                    }
                    else if(prevDir !== "" && (lDir === "" || lDir !== prevDir || stop)) {
                        result += misc.PDF + (i == segments.length - 1 && lDir !== ""? "" : args.dir === "rtl" ? misc.RLM : misc.LRM);
                        if (lDir !== "") {
                            result += (lDir === "rtl" ? misc.RLE : misc.LRE);
                        }
                    }
                    if (dir === "auto") {
                        dir = misc.getDirection(segments[i].content, dir, args.guiDir);
                    }
                    if ((/^(rtl|ltr)$/i).test(dir)) {
                        result += (dir === "rtl" ? misc.RLE : misc.LRE) + segments[i].content + misc.PDF;
                        checkedDir = dir;
                    }
                    else {
                        result += segments[i].content;
                        checkedDir = misc.getDirection(segments[i].content, dir, args.guiDir, true);
                    }
                    if (i < segments.length - 1) {
                        var locDir = lDir && segments[i+1].localGui? lDir : args.dir;
                        result += locDir === "rtl" ? misc.RLM : misc.LRM;
                    }
                    else if(prevDir !== "") {
                        result += misc.PDF;
                    }
                    prevDir = lDir;
                    stop = false;
                }
                else {
                    stop = true;
                }
            }
            var sttDir = args.dir === "auto" ? misc.getDirection(segments[0].actual, args.dir, args.guiDir) : args.dir;
            if (sttDir !== args.guiDir) {
                result = (sttDir === "rtl" ? misc.RLE : misc.LRE) + result + misc.PDF;
            }
            return result;
        }

        function getResultWithHtml(segments, args, isHtml) {
            var result = "";
            var checkedDir = "";
            var prevDir = "";
            for (var i = 0; i < segments.length; i++) {
                if (segments[i].isVisible) {
                    var dir = segments[i].textDirection;
                    var lDir = segments[i].localGui;
                    if (lDir !== "" && prevDir === "") {
                        result += "<bdi dir='" + (lDir === "rtl" ? "rtl" : "ltr") + "'>";
                    }
                    else if(prevDir !== "" && (lDir === "" || lDir !== prevDir || stop)) {
                        result += "</bdi>" + (i == segments.length - 1 && lDir !== ""? "" : "<span style='unicode-bidi: embed; direction: " + (args.dir === "rtl" ? "rtl" : "ltr") + ";'></span>");
                        if (lDir !== "") {
                            result += "<bdi dir='" + (lDir === "rtl" ? "rtl" : "ltr") + "'>";
                        }
                    }

                    if (dir === "auto") {
                        dir = misc.getDirection(segments[i].content, dir, args.guiDir);
                    }
                    if ((/^(rtl|ltr)$/i).test(dir)) {
                        //result += "<span style='unicode-bidi: embed; direction: " + (dir === "rtl" ? "rtl" : "ltr") + ";'>" + segments[i].content + "</span>";
                        result += "<bdi dir='" + (dir === "rtl" ? "rtl" : "ltr") + "'>" + segments[i].content + "</bdi>";
                        checkedDir = dir;
                    }
                    else {
                        result += segments[i].content;
                        checkedDir = misc.getDirection(segments[i].content, dir, args.guiDir, true);
                    }
                    if (i < segments.length - 1) {
                        var locDir = lDir && segments[i+1].localGui? lDir : args.dir;
                        result += "<span style='unicode-bidi: embed; direction: " + (locDir === "rtl" ? "rtl" : "ltr") + ";'></span>";
                    }
                    else if(prevDir !== "") {
                        result += "</bdi>";
                    }
                    prevDir = lDir;
                    stop = false;
                }
                else {
                    stop = true;
                }
            }
            var sttDir = args.dir === "auto" ? misc.getDirection(segments[0].actual, args.dir, args.guiDir) : args.dir;
            if (sttDir !== args.guiDir) {
                result = "<bdi dir='" + (sttDir === "rtl" ? "rtl" : "ltr") + "'>" + result + "</bdi>";
            }
            return result;
        }

        //TBD ?
        function restore(text, isHtml) {
            return text;
        }

        stt.parseAndDisplayStructure = parseAndDisplayStructure;
        stt.parseStructure = parseStructure;
        stt.displayStructure = displayStructure;
        stt.restore = restore;

        return stt;
    })();

    var breadcrumb = (function() {
        return {
            format: function (text, args, isRtl, isHtml, locale, parseOnly) {
                var fArgs =
                {
                        guiDir: isRtl ? "rtl" : "ltr",
                        dir: args.dir ? args.dir : isRtl ? "rtl" : "ltr",
                        subs: {
                            content: ">",
                            continued: true,
                            subDir: isRtl ? "rtl" : "ltr"
                        },
                        cases: [{
                            args: {
                                subs: {
                                    content: "<",
                                    continued: true,
                                    subDir: isRtl ? "ltr" : "rtl"
                                }
                            }
                        }]
                };

                if (!parseOnly) {
                    return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale);
                }
                else {
                    return stext.parseStructure(text, fArgs, !!isHtml, locale);
                }
            }
        };
    })();

    var comma = (function() {
        return {
            format: function (text, args, isRtl, isHtml, locale, parseOnly) {
                var fArgs =
                {
                        guiDir: isRtl ? "rtl" : "ltr",
                        dir: "ltr",
                        points: ","
                };
                if (!parseOnly) {
                    return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale);
                }
                else {
                    return stext.parseStructure(text, fArgs, !!isHtml, locale);
                }
            }
        };
    })();

    var email = (function() {
        function getDir(text, locale) {
            if (misc.getLocaleDetails(locale).lang !== "ar") {
                return "ltr";
            }
            var ind = text.indexOf("@");
            if (ind > 0 && ind < text.length - 1) {
                return misc.hasArabicChar(text.substring(ind + 1)) ? "rtl" : "ltr";
            }
            return "ltr";
        }

        return {
            format: function (text, args, isRtl, isHtml, locale, parseOnly) {
                var fArgs =
                {
                        guiDir: isRtl ? "rtl" : "ltr",
                        dir: getDir(text, locale),
                        points: "<>.:,;@",
                        cases: [{
                            handler: common,
                            args: {
                                bounds: [{
                                    startAfter: "\"",
                                    endBefore: "\""
                                },
                                {
                                    startAfter: "(",
                                    endBefore: ")"
                                }
                                ],
                                points: ""
                            }
                        }]
                };
                if (!parseOnly) {
                    return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale);
                }
                else {
                    return stext.parseStructure(text, fArgs, !!isHtml, locale);
                }
            }
        };
    })();

    var filepath = (function() {
        return {
            format: function (text, args, isRtl, isHtml, locale, parseOnly) {
                var fArgs =
                {
                        guiDir: isRtl ? "rtl" : "ltr",
                        dir: "ltr",
                        points: "/\\:."
                };
                if (!parseOnly) {
                    return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale);
                }
                else {
                    return stext.parseStructure(text, fArgs, !!isHtml, locale);
                }
            }
        };
    })();

    var formula = (function() {
        return {
            format: function (text, args, isRtl, isHtml, locale, parseOnly) {
                var fArgs =
                {
                        guiDir: isRtl ? "rtl" : "ltr",
                        dir: "ltr",
                        points: " /%^&[]<>=!?~:.,|()+-*{}",
                };
                if (!parseOnly) {
                    return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale);
                }
                else {
                    return stext.parseStructure(text, fArgs, !!isHtml, locale);
                }
            }
        };
    })();


    var sql = (function() {
        return {
            format: function (text, args, isRtl, isHtml, locale, parseOnly) {
                var fArgs =
                {
                        guiDir: isRtl ? "rtl" : "ltr",
                        dir: "ltr",
                        points: "\t!#%&()*+,-./:;<=>?|[]{}",
                        cases: [{
                            handler: common,
                            args: {
                                bounds: [{
                                    startAfter: "/*",
                                    endBefore: "*/"
                                },
                                {
                                    startAfter: "--",
                                    end: "\n"
                                },
                                {
                                    startAfter: "--"
                                }
                                ]
                            }
                        },
                        {
                            handler: common,
                            args: {
                                subs: {
                                    content: " ",
                                    continued: true
                                }
                            }
                        },
                        {
                            handler: common,
                            args: {
                                bounds: [{
                                    startAfter: "'",
                                    endBefore: "'"
                                },
                                {
                                    startAfter: "\"",
                                    endBefore: "\""
                                }
                                ]
                            }
                        }
                        ]
                };
                if (!parseOnly) {
                    return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale);
                }
                else {
                    return stext.parseStructure(text, fArgs, !!isHtml, locale);
                }
            }
        };
    })();

    var underscore = (function() {
        return {
            format: function (text, args, isRtl, isHtml, locale, parseOnly) {
                var fArgs =
                {
                        guiDir: isRtl ? "rtl" : "ltr",
                        dir: "ltr",
                        points: "_"
                };
                if (!parseOnly) {
                    return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale);
                }
                else {
                    return stext.parseStructure(text, fArgs, !!isHtml, locale);
                }
            }
        };
    })();

    var url = (function() {
        return {
            format: function (text, args, isRtl, isHtml, locale, parseOnly) {
                var fArgs =
                {
                        guiDir: isRtl ? "rtl" : "ltr",
                        dir: "ltr",
                        points: ":?#/@.[]="
                };
                if (!parseOnly) {
                    return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale);
                }
                else {
                    return stext.parseStructure(text, fArgs, !!isHtml, locale);
                }
            }
        };
    })();

    var word = (function() {
        return {
            format: function (text, args, isRtl, isHtml, locale, parseOnly) {
                var fArgs =
                {
                        guiDir: isRtl ? "rtl" : "ltr",
                        dir: args.dir ? args.dir : isRtl ? "rtl" : "ltr",
                        points: " ,.!?;:",
                };
                if (!parseOnly) {
                    return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale);
                }
                else {
                    return stext.parseStructure(text, fArgs, !!isHtml, locale);
                }
            }
        };
    })();

    var xpath = (function() {
        return {
            format: function (text, args, isRtl, isHtml, locale, parseOnly) {
                var fArgs =
                {
                        guiDir: isRtl ? "rtl" : "ltr",
                        dir: "ltr",
                        points: " /[]<>=!:@.|()+-*",
                        cases: [{
                            handler: common,
                            args: {
                                bounds: [{
                                    startAfter: "\"",
                                    endBefore: "\""
                                },
                                {
                                    startAfter: "'",
                                    endBefore: "'"
                                }
                                ],
                                points: ""
                            }
                        }
                        ]
                };
                if (!parseOnly) {
                    return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale);
                }
                else {
                    return stext.parseStructure(text, fArgs, !!isHtml, locale);
                }
            }
        };
    })();

    var custom = (function() {
        return {
            format: function (text, args, isRtl, isHtml, locale, parseOnly) {
                var hArgs = {};
                var prop = "";
                var sArgs = Array.isArray(args)? args[0] : args;
                for (prop in sArgs) {
                    if (sArgs.hasOwnProperty(prop)) {
                        hArgs[prop] = sArgs[prop];
                    }
                }
                hArgs.guiDir = isRtl ? "rtl" : "ltr";
                hArgs.dir = hArgs.dir ? hArgs.dir : hArgs.guiDir;
                if (!parseOnly) {
                    return stext.parseAndDisplayStructure(text, hArgs, !!isHtml, locale);
                }
                else {
                    return stext.parseStructure(text, hArgs, !!isHtml, locale);
                }
            }
        };
    })();

    var message = (function() {
        var params = {msgLang: "en", msgDir: "", phLang: "", phDir: "", phPacking: ["{","}"], phStt: {type: "none", args: {}}, guiDir: ""};
        var parametersChecked = false;

        function getDirectionOfLanguage(lang) {
            if (lang === "he" || lang === "iw" || lang === "ar") {
                return "rtl";
            }
            return "ltr";
        }

        function checkParameters(obj) {
            if (obj.msgDir.length === 0) {
                obj.msgDir = getDirectionOfLanguage(obj.msgLang);
            }
            obj.msgDir = obj.msgDir !== "ltr" && obj.msgDir !== "rtl" && obj.msgDir != "auto"? "ltr" : obj.msgDir;
            if (obj.guiDir.length === 0) {
                obj.guiDir = obj.msgDir;
            }
            obj.guiDir = obj.guiDir !== "rtl"? "ltr" : "rtl";
            if (obj.phDir.length === 0) {
                obj.phDir = obj.phLang.length === 0? obj.msgDir : getDirectionOfLanguage(obj.phLang);
            }
            obj.phDir = obj.phDir !== "ltr" && obj.phDir !== "rtl" && obj.phDir != "auto"? "ltr" : obj.phDir;
            if (typeof (obj.phPacking) === "string") {
                obj.phPacking = obj.phPacking.split("");
            }
            if (obj.phPacking.length < 2) {
                obj.phPacking = ["{","}"];
            }
        }

        return {
            setDefaults: function (args) {
                for (var prop in args) {
                    if (params.hasOwnProperty(prop)) {
                        params[prop] = args[prop];
                    }
                }
                checkParameters(params);
                parametersChecked = true;
            },

            format: function (text) {
                if (!parametersChecked) {
                    checkParameters(params);
                    parametersChecked = true;
                }
                var isHtml = false;
                var hasHtmlArg = false;
                var spLength = params.phPacking[0].length;
                var epLength = params.phPacking[1].length;
                if (arguments.length > 0) {
                    var last = arguments[arguments.length-1];
                    if (typeof (last) === "boolean") {
                        isHtml = last;
                        hasHtmlArg = true;
                    }
                }
                //Message
                var re = new RegExp(params.phPacking[0] + "\\d+" + params.phPacking[1]);
                var m;
                var tSegments = [];
                var offset = 0;
                var txt = text;
                while ((m = re.exec(txt)) != null) {
                    var lastIndex = txt.indexOf(m[0]) + m[0].length;
                    if (lastIndex > m[0].length) {
                        tSegments.push({text: txt.substring(0, lastIndex - m[0].length), ph: false});
                    }
                    tSegments.push({text: m[0], ph: true});
                    offset += lastIndex;
                    txt = txt.substring(lastIndex, txt.length);
                }
                if (offset < text.length) {
                    tSegments.push({text: text.substring(offset, text.length), ph: false});
                }
                //Parameters
                var tArgs = [];
                for (var i = 1; i < arguments.length - (hasHtmlArg? 1 : 0); i++) {
                    var arg = arguments[i];
                    var checkArr = arg;
                    var inLoop = false;
                    var indArr = 0;
                    if (Array.isArray(checkArr)) {
                        arg = checkArr[0];
                        if (typeof(arg) === "undefined") {
                            continue;
                        }
                        inLoop = true;
                    }
                    do {
                        if (typeof (arg) === "string") {
                            tArgs.push({text: arg, dir: params.phDir, stt: params.stt});
                        }
                        else if(typeof (arg) === "boolean") {
                            isHtml = arg;
                        }
                        else if(typeof (arg) === "object") {
                            tArgs.push(arg);
                            if (!arg.hasOwnProperty("text")) {
                                tArgs[tArgs.length-1].text = "{???}";
                            }
                            if (!arg.hasOwnProperty("dir") || arg.dir.length === 0) {
                                tArgs[tArgs.length-1].dir = params.phDir;
                            }
                            if (!arg.hasOwnProperty("stt") || (typeof (arg.stt) === "string" && arg.stt.length === 0) ||
                                (typeof (arg.stt) === "object" && Object.keys(arg.stt).length === 0)) {
                                tArgs[tArgs.length-1].stt = params.phStt;
                            }
                        }
                        else {
                            tArgs.push({text: "" + arg, dir: params.phDir, stt: params.phStt});
                        }
                        if (inLoop) {
                            indArr++;
                            if (indArr == checkArr.length) {
                                inLoop = false;
                            }
                            else {
                                arg = checkArr[indArr];
                            }
                        }
                    } while(inLoop);
                }
                //Indexing
                var segments = [];
                for (i = 0; i < tSegments.length; i++) {
                    var t = tSegments[i];
                    if (!t.ph) {
                        segments.push(new TextSegment({content: t.text, textDirection: params.msgDir}));
                    }
                    else {
                        var ind = parseInt(t.text.substring(spLength, t.text.length - epLength));
                        if (isNaN(ind) || ind >= tArgs.length) {
                            segments.push(new TextSegment({content: t.text, textDirection: params.msgDir}));
                            continue;
                        }
                        var sttType = "none";
                        if (!tArgs[ind].stt) {
                            tArgs[ind].stt = params.phStt;
                        }
                        if (tArgs[ind].stt) {
                            if (typeof (tArgs[ind].stt) === "string") {
                                sttType = tArgs[ind].stt;
                            }
                            else if(tArgs[ind].stt.hasOwnProperty("type")) {
                                sttType = tArgs[ind].stt.type;
                            }
                        }
                        if (sttType.toLowerCase() !== "none") {
                            var sttSegs =  getHandler(sttType).format(tArgs[ind].text, tArgs[ind].stt.args || {},
                                    params.msgDir === "rtl", false, params.msgLang, true);
                            for (var j = 0; j < sttSegs.length; j++) {
                                segments.push(sttSegs[j]);
                            }
                            segments.push(new TextSegment({isVisible: false}));
                        }
                        else {
                            segments.push(new TextSegment({content: tArgs[ind].text, textDirection: (tArgs[ind].dir? tArgs[ind].dir : params.phDir)}));
                        }
                    }
                }
                var result =  stext.displayStructure(segments, {guiDir: params.guiDir, dir: params.msgDir}, isHtml);
                return result;
            }
        };
    })();

    var event = null;

    function getHandler(type) {
        switch (type) {
        case "breadcrumb" :
            return breadcrumb;
        case "comma" :
            return comma;
        case "email" :
            return email;
        case "filepath" :
            return filepath;
        case "formula" :
            return formula;
        case "sql" :
            return sql;
        case "underscore" :
            return underscore;
        case "url" :
            return url;
        case "word" :
            return word;
        case "xpath" :
            return xpath;
        default:
            return custom;
        }
    }

    function isInputEventSupported(element) {
        var agent = window.navigator.userAgent;
        if (agent.indexOf("MSIE") >=0 || agent.indexOf("Trident") >=0 || agent.indexOf("Edge") >=0) {
            return false;
        }
        var checked = document.createElement(element.tagName);
        checked.contentEditable = true;
        var isSupported = ("oninput" in checked);
        if (!isSupported) {
          checked.setAttribute('oninput', 'return;');
          isSupported = typeof checked['oninput'] == 'function';
        }
        checked = null;
        return isSupported;
    }

    function attachElement(element, type, args, isRtl, locale) {
        //if (!element || element.nodeType != 1 || !element.isContentEditable)
        if (!element || element.nodeType != 1) {
            return false;
        }
        if (!event) {
            event = document.createEvent('Event');
            event.initEvent('TF', true, true);
        }
        element.setAttribute("data-tf-type", type);
        var sArgs = args === "undefined"? "{}" : JSON.stringify(Array.isArray(args)? args[0] : args);
        element.setAttribute("data-tf-args", sArgs);
        var dir = "ltr";
        if (isRtl === "undefined") {
            if (element.dir) {
                dir = element.dir;
            }
            else if(element.style && element.style.direction) {
                dir = element.style.direction;
            }
            isRtl = dir.toLowerCase() === "rtl";
        }
        element.setAttribute("data-tf-dir", isRtl);
        element.setAttribute("data-tf-locale", misc.getLocaleDetails(locale).lang);
        if (isInputEventSupported(element)) {
            var ehandler = element.oninput;
            element.oninput = function(event) {
                displayWithStructure(event.target);
            };
        }
        else {
            element.onkeyup = function(e) {
                displayWithStructure(e.target);
                element.dispatchEvent(event);
            };
            element.onmouseup = function(e) {
                displayWithStructure(e.target);
                element.dispatchEvent(event);
            };
        }
        displayWithStructure(element);

        return true;
    }

    function detachElement(element) {
        if (!element || element.nodeType != 1) {
            return;
        }
        element.removeAttribute("data-tf-type");
        element.removeAttribute("data-tf-args");
        element.removeAttribute("data-tf-dir");
        element.removeAttribute("data-tf-locale");
        element.innerHTML = element.textContent || "";
    }

    function displayWithStructure(element) {
        var txt = element.textContent || "";
        var selection = document.getSelection();
        if (txt.length === 0 || !selection || selection.rangeCount <= 0) {
            element.dispatchEvent(event);
            return;
        }

        var range = selection.getRangeAt(0);
        var tempRange = range.cloneRange(), startNode, startOffset;
        startNode = range.startContainer;
        startOffset = range.startOffset;
        var textOffset = 0;
        if (startNode.nodeType === 3) {
            textOffset += startOffset;
        }
        tempRange.setStart(element,0);
        tempRange.setEndBefore(startNode);
        var div = document.createElement('div');
        div.appendChild(tempRange.cloneContents());
        textOffset += div.textContent.length;

        element.innerHTML = getHandler(element.getAttribute("data-tf-type")).
            format(txt, JSON.parse(element.getAttribute("data-tf-args")), (element.getAttribute("data-tf-dir") === "true"? true : false),
            true, element.getAttribute("data-tf-locale"));
        var parent = element;
        var node = element;
        var newOffset = 0;
        var inEnd = false;
        selection.removeAllRanges();
        range.setStart(element,0);
        range.setEnd(element,0);
        while (node) {
            if (node.nodeType === 3) {
                if (newOffset + node.nodeValue.length >= textOffset) {
                    range.setStart(node, textOffset - newOffset);
                    break;
                }
                else {
                    newOffset += node.nodeValue.length;
                    node = node.nextSibling;
                }
            }
            else if(node.hasChildNodes()) {
                parent = node;
                node = parent.firstChild;
                continue;
            }
            else {
                node = node.nextSibling;
            }
            while (!node) {
                if (parent === element) {
                    inEnd = true;
                    break;
                }
                node = parent.nextSibling;
                parent = parent.parentNode;
            }
            if (inEnd) {
                break;
            }
        }

        selection.addRange(range);
        element.dispatchEvent(event);
    }

    return {
        /*!
        * Returns the HTML representation of a given structured text
        * @param text - the structured text
        * @param type - could be one of filepath, url, email
        * @param args - pass additional arguments to the handler. generally null.
        * @param isRtl - indicates if the GUI is mirrored
        * @param locale - the browser locale
        */
        getHtml: function (text, type, args, isRtl, locale) {
            return getHandler(type).format(text, args, isRtl, true, locale);
        },
        /*!
        * Handle Structured text correct display for a given HTML element.
        * @param element - the element  : should be of type div contenteditable=true
        * @param type - could be one of filepath, url, email
        * @param args - pass additional arguments to the handler. generally null.
        * @param isRtl - indicates if the GUI is mirrored
        * @param locale - the browser locale
        */
        attach: function (element, type, args, isRtl, locale) {
            return attachElement(element, type, args, isRtl, locale);
        }
    };
})();
;/**
 * 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.
 **/
RED.state = {
    DEFAULT: 0,
    MOVING: 1,
    JOINING: 2,
    MOVING_ACTIVE: 3,
    ADDING: 4,
    EDITING: 5,
    EXPORT: 6,
    IMPORT: 7,
    IMPORT_DRAGGING: 8,
    QUICK_JOINING: 9,
    PANNING: 10,
    SELECTING_NODE: 11,
    GROUP_DRAGGING: 12,
    GROUP_RESIZE: 13,
    DETACHED_DRAGGING: 14,
    SLICING: 15,
    SLICING_JUNCTION: 16
}
;RED.plugins = (function() {
    var plugins = {};
    var pluginsByType = {};
    var moduleList = {};

    function registerPlugin(id,definition) {
        plugins[id] = definition;
        if (definition.type) {
            pluginsByType[definition.type] = pluginsByType[definition.type] || [];
            pluginsByType[definition.type].push(definition);
        }
        if (RED._loadingModule) {
            definition.module = RED._loadingModule;
            definition["_"] = function() {
                var args = Array.prototype.slice.call(arguments);
                var originalKey = args[0];
                if (!/:/.test(args[0])) {
                    args[0] = definition.module+":"+args[0];
                }
                var result = RED._.apply(null,args);
                if (result === args[0]) {
                    return originalKey;
                }
                return result;
            }
        } else {
            definition["_"] = RED["_"]
        }
        if (definition.onadd && typeof definition.onadd === 'function') {
            definition.onadd();
        }
        RED.events.emit("registry:plugin-added",id);
    }

    function getPlugin(id) {
        return plugins[id]
    }

    function getPluginsByType(type) {
        return pluginsByType[type] || [];
    }

    function setPluginList(list) {
        for(let i=0;i<list.length;i++) {
            let p = list[i];
            addPlugin(p);
        }
    }

    function addPlugin(p) {

        moduleList[p.module] = moduleList[p.module] || {
            name:p.module,
            version:p.version,
            local:p.local,
            sets:{},
            plugin: true,
            id: p.id
        };
        if (p.pending_version) {
            moduleList[p.module].pending_version = p.pending_version;
        }
        moduleList[p.module].sets[p.name] = p;

        RED.events.emit("registry:plugin-module-added",p.module);
    }

    function getModule(module) {
        return moduleList[module];
    }

    return {
        registerPlugin: registerPlugin,
        getPlugin: getPlugin,
        getPluginsByType: getPluginsByType,

        setPluginList: setPluginList,
        addPlugin: addPlugin,
        getModule: getModule
    }
})();
;/**
 * 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.
 **/

/**
 * An Interface to nodes and utility functions for creating/adding/deleting nodes and links
 * @namespace RED.nodes
*/
RED.nodes = (function() {
    var PORT_TYPE_INPUT = 1;
    var PORT_TYPE_OUTPUT = 0;

    var node_defs = {};
    var linkTabMap = {};

    var configNodes = {};
    var links = [];
    var nodeLinks = {};
    var defaultWorkspace;
    var workspaces = {};
    var workspacesOrder =[];
    var subflows = {};
    var loadedFlowVersion = null;

    var groups = {};
    var groupsByZ = {};

    var junctions = {};
    var junctionsByZ = {};

    var initialLoad;

    var dirty = false;

    function setDirty(d) {
        dirty = d;
        if (!d) {
            allNodes.clearState()
        }
        RED.events.emit("workspace:dirty",{dirty:dirty});
    }

    // The registry holds information about all node types.
    var registry = (function() {
        var moduleList = {};
        var nodeList = [];
        var nodeSets = {};
        var typeToId = {};
        var nodeDefinitions = {};
        var iconSets = {};

        nodeDefinitions['tab'] = {
            defaults: {
                label: {value:""},
                disabled: {value: false},
                locked: {value: false},
                info: {value: ""},
                env: {value: []}
            }
        };

        var exports = {
            setModulePendingUpdated: function(module,version) {
                if (!!RED.plugins.getModule(module)) {
                    // The module updated is a plugin
                    RED.plugins.getModule(module).pending_version = version;
                } else {
                    moduleList[module].pending_version = version;
                }

                RED.events.emit("registry:module-updated",{module:module,version:version});
            },
            getModule: function(module) {
                return moduleList[module];
            },
            getNodeSetForType: function(nodeType) {
                return exports.getNodeSet(typeToId[nodeType]);
            },
            getModuleList: function() {
                return moduleList;
            },
            getNodeList: function() {
                return nodeList;
            },
            getNodeTypes: function() {
                return Object.keys(nodeDefinitions);
            },
            /**
             * Get an array of node definitions
             * @param {Object} options - options object
             * @param {boolean} [options.configOnly] - if true, only return config nodes
             * @param {function} [options.filter] - a filter function to apply to the list of nodes
             * @returns array of node definitions
             */
            getNodeDefinitions: function(options) {
                const result = []
                const configOnly = (options && options.configOnly)
                const filter = (options && options.filter)
                const keys = Object.keys(nodeDefinitions)
                for (const key of keys) {
                    const def = nodeDefinitions[key]
                    if(!def) { continue }
                    if (configOnly && def.category !== "config") {
                            continue
                    }
                    if (filter && !filter(nodeDefinitions[key])) {
                        continue
                    }
                    result.push(nodeDefinitions[key])
                }
                return result
            },
            setNodeList: function(list) {
                nodeList = [];
                for(var i=0;i<list.length;i++) {
                    var ns = list[i];
                    exports.addNodeSet(ns);
                }
            },
            addNodeSet: function(ns) {
                if (!ns.types) {
                    // A node has been loaded without any types. Ignore it.
                    return;
                }
                ns.added = false;
                nodeSets[ns.id] = ns;
                for (var j=0;j<ns.types.length;j++) {
                    typeToId[ns.types[j]] = ns.id;
                }
                nodeList.push(ns);

                moduleList[ns.module] = moduleList[ns.module] || {
                    name:ns.module,
                    version:ns.version,
                    local:ns.local,
                    sets:{}
                };
                if (ns.pending_version) {
                    moduleList[ns.module].pending_version = ns.pending_version;
                }
                moduleList[ns.module].sets[ns.name] = ns;
                RED.events.emit("registry:node-set-added",ns);
            },
            removeNodeSet: function(id) {
                var ns = nodeSets[id];
                if (!ns) { return {} }

                for (var j=0;j<ns.types.length;j++) {
                    delete typeToId[ns.types[j]];
                }
                delete nodeSets[id];
                for (var i=0;i<nodeList.length;i++) {
                    if (nodeList[i].id === id) {
                        nodeList.splice(i,1);
                        break;
                    }
                }
                delete moduleList[ns.module].sets[ns.name];
                if (Object.keys(moduleList[ns.module].sets).length === 0) {
                    delete moduleList[ns.module];
                }
                RED.events.emit("registry:node-set-removed",ns);
                return ns;
            },
            getNodeSet: function(id) {
                return nodeSets[id];
            },
            enableNodeSet: function(id) {
                var ns = nodeSets[id];
                ns.enabled = true;
                RED.events.emit("registry:node-set-enabled",ns);
            },
            disableNodeSet: function(id) {
                var ns = nodeSets[id];
                ns.enabled = false;
                RED.events.emit("registry:node-set-disabled",ns);
            },
            registerNodeType: function(nt,def) {
                if (nt.substring(0,8) != "subflow:") {
                    if (!nodeSets[typeToId[nt]]) {
                        var error = "";
                        var fullType = nt;
                        if (RED._loadingModule) {
                            fullType = "["+RED._loadingModule+"] "+nt;
                            if (nodeSets[RED._loadingModule]) {
                                error = nodeSets[RED._loadingModule].err || "";
                            } else {
                                error = "Unknown error";
                            }
                        }
                        RED.notify(RED._("palette.event.unknownNodeRegistered",{type:fullType, error:error}), "error");
                        return;
                    }
                    def.set = nodeSets[typeToId[nt]];
                    nodeSets[typeToId[nt]].added = true;
                    nodeSets[typeToId[nt]].enabled = true;

                    var ns;
                    if (def.set.module === "node-red") {
                        ns = "node-red";
                    } else {
                        ns = def.set.id;
                    }
                    def["_"] = function() {
                        var args = Array.prototype.slice.call(arguments, 0);
                        var original = args[0];
                        if (args[0].indexOf(":") === -1) {
                            args[0] = ns+":"+args[0];
                        }
                        var result = RED._.apply(null,args);
                        if (result === args[0]) {
                            result = original;
                        }
                        return result;
                    }
                    // TODO: too tightly coupled into palette UI
                }

                def.type = nt;
                nodeDefinitions[nt] = def;


                if (def.defaults) {
                    for (var d in def.defaults) {
                        if (def.defaults.hasOwnProperty(d)) {
                            if (def.defaults[d].type) {
                                try {
                                    def.defaults[d]._type = parseNodePropertyTypeString(def.defaults[d].type)
                                } catch(err) {
                                    console.warn(err);
                                }
                            }
                        }
                    }
                }


                RED.events.emit("registry:node-type-added",nt);
            },
            removeNodeType: function(nt) {
                if (nt.substring(0,8) != "subflow:") {
                    // NON-NLS - internal debug message
                    throw new Error("this api is subflow only. called with:",nt);
                }
                delete nodeDefinitions[nt];
                RED.events.emit("registry:node-type-removed",nt);
            },
            getNodeType: function(nt) {
                return nodeDefinitions[nt];
            },
            setIconSets: function(sets) {
                iconSets = sets;
                iconSets["font-awesome"] = RED.nodes.fontAwesome.getIconList();
            },
            getIconSets: function() {
                return iconSets;
            }
        };
        return exports;
    })();

    // allNodes holds information about the Flow nodes.
    var allNodes = (function() {
        // Map node.id -> node
        var nodes = {};
        // Map tab.id -> Array of nodes on that tab
        var tabMap = {};
        // Map tab.id -> Set of dirty object ids on that tab
        var tabDirtyMap = {};
        // Map tab.id -> Set of object ids of things deleted from the tab that weren't otherwise dirty
        var tabDeletedNodesMap = {};
        // Set of object ids of things added to a tab after initial import
        var addedDirtyObjects = new Set()

        function changeCollectionDepth(tabNodes, toMove, direction, singleStep) {
            const result = []
            const moved = new Set();
            const startIndex = direction ? tabNodes.length - 1 : 0
            const endIndex = direction ? -1 : tabNodes.length
            const step = direction ? -1 : 1
            let target = startIndex // Only used for all-the-way moves
            for (let i = startIndex; i != endIndex; i += step) {
                if (toMove.size === 0) {
                    break;
                }
                const n = tabNodes[i]
                if (toMove.has(n)) {
                    if (singleStep) {
                        if (i !== startIndex && !moved.has(tabNodes[i - step])) {
                            tabNodes.splice(i, 1)
                            tabNodes.splice(i - step, 0, n)
                            n._reordered = true
                            result.push(n)
                        }
                    } else {
                        if (i !== target) {
                            tabNodes.splice(i, 1)
                            tabNodes.splice(target, 0, n)
                            n._reordered = true
                            result.push(n)
                        }
                        target += step
                    }
                    toMove.delete(n);
                    moved.add(n);
                }
            }
            return result
        }

        var api = {
            addTab: function(id) {
                tabMap[id] = [];
                tabDirtyMap[id] = new Set();
                tabDeletedNodesMap[id] = new Set();
            },
            hasTab: function(z) {
                return tabMap.hasOwnProperty(z)
            },
            removeTab: function(id) {
                delete tabMap[id];
                delete tabDirtyMap[id];
                delete tabDeletedNodesMap[id];
            },
            addNode: function(n) {
                nodes[n.id] = n;
                if (tabMap.hasOwnProperty(n.z)) {
                    tabMap[n.z].push(n);
                    api.addObjectToWorkspace(n.z, n.id, n.changed || n.moved)
                } else {
                    console.warn("Node added to unknown tab/subflow:",n);
                    tabMap["_"] = tabMap["_"] || [];
                    tabMap["_"].push(n);
                }
            },
            removeNode: function(n) {
                delete nodes[n.id]
                if (tabMap.hasOwnProperty(n.z)) {
                    var i = tabMap[n.z].indexOf(n);
                    if (i > -1) {
                        tabMap[n.z].splice(i,1);
                    }
                    api.removeObjectFromWorkspace(n.z, n.id)
                }
            },
            /**
             * Add an object to our dirty/clean tracking state
             * @param {String} z 
             * @param {String} id 
             * @param {Boolean} isDirty 
             */
            addObjectToWorkspace: function (z, id, isDirty) {
                if (isDirty) {
                    addedDirtyObjects.add(id)
                }
                if (tabDeletedNodesMap[z].has(id)) {
                    tabDeletedNodesMap[z].delete(id)
                }
                api.markNodeDirty(z, id, isDirty)
            },
            /**
             * Remove an object from our dirty/clean tracking state
             * @param {String} z 
             * @param {String} id 
             */
            removeObjectFromWorkspace: function (z, id) {
                if (!addedDirtyObjects.has(id)) {
                    tabDeletedNodesMap[z].add(id)
                } else {
                    addedDirtyObjects.delete(id)
                }
                api.markNodeDirty(z, id, false)
            },
            hasNode: function(id) {
                return nodes.hasOwnProperty(id);
            },
            getNode: function(id) {
                return nodes[id]
            },
            moveNode: function(n, newZ) {
                api.removeNode(n);
                n.z = newZ;
                api.addNode(n)
            },
            /**
             * @param {array} nodes 
             * @param {boolean} direction true:forwards false:back
             * @param {boolean} singleStep true:single-step false:all-the-way
             */
            changeDepth: function(nodes, direction, singleStep) {
                if (!Array.isArray(nodes)) {
                    nodes = [nodes]
                }
                let result = []
                const tabNodes = tabMap[nodes[0].z];
                const toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
                if (toMove.size > 0) {
                    result = result.concat(changeCollectionDepth(tabNodes, toMove, direction, singleStep))
                    if (result.length > 0) {
                        RED.events.emit('nodes:reorder',{
                            z: nodes[0].z,
                            nodes: result
                        });
                    }
                }

                const groupNodes = groupsByZ[nodes[0].z] || []
                const groupsToMove = new Set(nodes.filter(function(n) { return n.type === 'group'}))
                if (groupsToMove.size > 0) {
                    const groupResult = changeCollectionDepth(groupNodes, groupsToMove, direction, singleStep)
                    if (groupResult.length > 0) {
                        result = result.concat(groupResult)
                        RED.events.emit('groups:reorder',{
                            z: nodes[0].z,
                            nodes: groupResult
                        });
                    }
                }
                RED.view.redraw(true)
                return result
            },
            moveNodesForwards: function(nodes) {
                return api.changeDepth(nodes, true, true)
            },
            moveNodesBackwards: function(nodes) {
                return api.changeDepth(nodes, false, true)
            },
            moveNodesToFront: function(nodes) {
                return api.changeDepth(nodes, true, false)
            },
            moveNodesToBack: function(nodes) {
                return api.changeDepth(nodes, false, false)
            },
            getNodes: function(z) {
                return tabMap[z];
            },
            clear: function() {
                nodes = {};
                tabMap = {};
                tabDirtyMap = {};
                tabDeletedNodesMap = {};
                addedDirtyObjects = new Set();
            },
            /**
             * Clear all internal state on what is dirty.
             */
            clearState: function () {
                // Called when a deploy happens, we can forget about added/remove
                // items as they have now been deployed.
                addedDirtyObjects = new Set()
                const flowsToCheck = new Set()
                for (const [z, set] of Object.entries(tabDeletedNodesMap)) {
                    if (set.size > 0) {
                        set.clear()
                        flowsToCheck.add(z)
                    }
                }
                for (const [z, set] of Object.entries(tabDirtyMap)) {
                    if (set.size > 0) {
                        set.clear()
                        flowsToCheck.add(z)
                    }
                }
                for (const z of flowsToCheck) {
                    api.checkTabState(z)
                }
            },
            eachNode: function(cb) {
                var nodeList,i,j;
                for (i in subflows) {
                    if (subflows.hasOwnProperty(i)) {
                        nodeList = tabMap[i];
                        for (j = 0; j < nodeList.length; j++) {
                            if (cb(nodeList[j]) === false) {
                                return;
                            }
                        }
                    }
                }
                for (i = 0; i < workspacesOrder.length; i++) {
                    nodeList = tabMap[workspacesOrder[i]];
                    for (j = 0; j < nodeList.length; j++) {
                        if (cb(nodeList[j]) === false) {
                            return;
                        }
                    }
                }
                // Flow nodes that do not have a valid tab/subflow
                if (tabMap["_"]) {
                    nodeList = tabMap["_"];
                    for (j = 0; j < nodeList.length; j++) {
                        if (cb(nodeList[j]) === false) {
                            return;
                        }
                    }
                }
            },
            filterNodes: function(filter) {
                var result = [];
                var searchSet = null;
                var doZFilter = false;
                if (filter.hasOwnProperty("z")) {
                    if (tabMap.hasOwnProperty(filter.z)) {
                        searchSet = tabMap[filter.z];
                    } else {
                        doZFilter = true;
                    }
                }
                var objectLookup = false;
                if (searchSet === null) {
                    searchSet = Object.keys(nodes);
                    objectLookup = true;
                }


                for (var n=0;n<searchSet.length;n++) {
                    var node = searchSet[n];
                    if (objectLookup) {
                        node = nodes[node];
                    }
                    if (filter.hasOwnProperty("type") && node.type !== filter.type) {
                        continue;
                    }
                    if (doZFilter && node.z !== filter.z) {
                        continue;
                    }
                    result.push(node);
                }
                return result;
            },
            getNodeOrder: function(z) {
                return (groupsByZ[z] || []).concat(tabMap[z]).map(n => n.id)
            },
            setNodeOrder: function(z, order) {
                var orderMap = {};
                order.forEach(function(id,i) {
                    orderMap[id] = i;
                })
                tabMap[z].sort(function(A,B) {
                    A._reordered = true;
                    B._reordered = true;
                    return orderMap[A.id] - orderMap[B.id];
                })
                if (groupsByZ[z]) {
                    groupsByZ[z].sort(function(A,B) {
                        return orderMap[A.id] - orderMap[B.id];
                    })
                }
            },
            /**
             * Update our records if an object is dirty or not
             * @param {String} z tab id
             * @param {String} id object id
             * @param {Boolean} dirty whether the object is dirty or not
             */
            markNodeDirty: function(z, id, dirty) {
                if (tabDirtyMap[z]) {
                    if (dirty) {
                        tabDirtyMap[z].add(id)
                    } else {
                        tabDirtyMap[z].delete(id)
                    }
                    api.checkTabState(z)
                }
            },
            /**
             * Check if a tab should update its contentsChange flag
             * @param {String} z tab id
             */
            checkTabState: function (z) {
                const ws = workspaces[z] || subflows[z]
                if (ws) {
                    const contentsChanged = tabDirtyMap[z].size > 0 || tabDeletedNodesMap[z].size > 0
                    if (Boolean(ws.contentsChanged) !== contentsChanged) {
                        ws.contentsChanged = contentsChanged
                        if (ws.type === 'tab') {
                            RED.events.emit("flows:change", ws);
                        } else {
                            RED.events.emit("subflows:change", ws);
                        }
                    }
                }
            }
        }
        return api;
    })()

    function getID() {
        var bytes = [];
        for (var i=0;i<8;i++) {
            bytes.push(Math.round(0xff*Math.random()).toString(16).padStart(2,'0'));
        }
        return bytes.join("");
    }

    function parseNodePropertyTypeString(typeString) {
        typeString = typeString.trim();
        var c;
        var pos = 0;
        var isArray = /\[\]$/.test(typeString);
        if (isArray) {
            typeString = typeString.substring(0,typeString.length-2);
        }

        var l = typeString.length;
        var inBrackets = false;
        var inToken = false;
        var currentToken = "";
        var types = [];
        while (pos < l) {
            c = typeString[pos];
            if (inToken) {
                if (c === "|") {
                    types.push(currentToken.trim())
                    currentToken = "";
                    inToken = false;
                } else if (c === ")") {
                    types.push(currentToken.trim())
                    currentToken = "";
                    inBrackets = false;
                    inToken = false;
                } else {
                    currentToken += c;
                }
            } else {
                if (c === "(") {
                    if (inBrackets) {
                        throw new Error("Invalid character '"+c+"' at position "+pos)
                    }
                    inBrackets = true;
                } else if (c !== " ") {
                    inToken = true;
                    currentToken = c;
                }
            }
            pos++;
        }
        currentToken = currentToken.trim();
        if (currentToken.length > 0) {
            types.push(currentToken)
        }
        return {
            types: types,
            array: isArray
        }
    }

    const nodeProxyHandler = {
        get(node, prop) {
            if (prop === '__isProxy__') {
                return true
            } else if (prop == '__node__') {
                return node
            }
            return node[prop]
        },
        set(node, prop, value) {
            if (node.z && (RED.nodes.workspace(node.z)?.locked || RED.nodes.subflow(node.z)?.locked)) {
                if (
                    node._def.defaults[prop] ||
                    prop === 'z' ||
                    prop === 'l' ||
                    prop === 'd' ||
                    (prop === 'changed' && (!!node.changed) !== (!!value)) || // jshint ignore:line
                    ((prop === 'x' || prop === 'y') && !node.resize && node.type !== 'group')
                ) {
                    throw new Error(`Cannot modified property '${prop}' of locked object '${node.type}:${node.id}'`)
                }
            }
            if (node.z && (prop === 'changed' || prop === 'moved')) {
                setTimeout(() => {
                    allNodes.markNodeDirty(node.z, node.id, node.changed || node.moved)
                }, 0)
            }
            node[prop] = value;
            return true
        }
    }

    function addNode(n) {
        let newNode
        if (!n.__isProxy__) {
            newNode = new Proxy(n, nodeProxyHandler)
        } else {
            newNode = n
        }

        if (n.type.indexOf("subflow") !== 0) {
            n["_"] = n._def._;
        } else {
            var subflowId = n.type.substring(8);
            var sf = RED.nodes.subflow(subflowId);
            if (sf) {
                sf.instances.push(newNode);
            }
            n["_"] = RED._;
        }

        // Both node and config node can use a config node
        updateConfigNodeUsers(newNode, { action: "add" });

        if (n._def.category == "config") {
            configNodes[n.id] = newNode;
        } else {
            if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; }
            n.dirty = true;
            if (n._def.category == "subflows" && typeof n.i === "undefined") {
                var nextId = 0;
                RED.nodes.eachNode(function(node) {
                    nextId = Math.max(nextId,node.i||0);
                });
                n.i = nextId+1;
            }
            allNodes.addNode(newNode);
            if (!nodeLinks[n.id]) {
                nodeLinks[n.id] = {in:[],out:[]};
            }
        }
        RED.events.emit('nodes:add',newNode);
        return newNode
    }
    function addLink(l) {
        if (nodeLinks[l.source.id]) {
            const isUnique = nodeLinks[l.source.id].out.every(function(link) {
                return link.sourcePort !== l.sourcePort || link.target.id !== l.target.id
            })
            if (!isUnique) {
                return
            }
        }
        links.push(l);
        if (l.source) {
            // Possible the node hasn't been added yet
            if (!nodeLinks[l.source.id]) {
                nodeLinks[l.source.id] = {in:[],out:[]};
            }
            nodeLinks[l.source.id].out.push(l);
        }
        if (l.target) {
            if (!nodeLinks[l.target.id]) {
                nodeLinks[l.target.id] = {in:[],out:[]};
            }
            nodeLinks[l.target.id].in.push(l);
        }
        if (l.source.z === l.target.z && linkTabMap[l.source.z]) {
            linkTabMap[l.source.z].push(l);
            allNodes.addObjectToWorkspace(l.source.z, getLinkId(l), true)
        }
        RED.events.emit("links:add",l);
    }

    function getLinkId(link) {
        return link.source.id + ':' + link.sourcePort + ':' + link.target.id
    }


    function getNode(id) {
        if (id in configNodes) {
            return configNodes[id];
        }
        return allNodes.getNode(id);
    }

    function removeNode(id) {
        var removedLinks = [];
        var removedNodes = [];
        var node;

        if (id in configNodes) {
            node = configNodes[id];
            delete configNodes[id];
            updateConfigNodeUsers(node, { action: "remove" });
            RED.events.emit('nodes:remove',node);
            RED.workspaces.refresh();
        } else if (allNodes.hasNode(id)) {
            node = allNodes.getNode(id);
            allNodes.removeNode(node);
            delete nodeLinks[id];
            removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
            removedLinks.forEach(removeLink);
            updateConfigNodeUsers(node, { action: "remove" });

            // TODO: Legacy code for exclusive config node
            var updatedConfigNode = false;
            for (var d in node._def.defaults) {
                if (node._def.defaults.hasOwnProperty(d)) {
                    var property = node._def.defaults[d];
                    if (property.type) {
                        var type = registry.getNodeType(property.type);
                        if (type && type.category == "config") {
                            var configNode = configNodes[node[d]];
                            if (configNode) {
                                updatedConfigNode = true;
                                if (configNode._def.exclusive) {
                                    removeNode(node[d]);
                                    removedNodes.push(configNode);
                                }
                            }
                        }
                    }
                }
            }

            if (node.type.indexOf("subflow:") === 0) {
                var subflowId = node.type.substring(8);
                var sf = RED.nodes.subflow(subflowId);
                if (sf) {
                    sf.instances.splice(sf.instances.indexOf(node),1);
                }
            }

            if (updatedConfigNode) {
                RED.workspaces.refresh();
            }
            try {
                if (node._def.oneditdelete) {
                    node._def.oneditdelete.call(node);
                }
            } catch(err) {
                console.log("oneditdelete",node.id,node.type,err.toString());
            }
            RED.events.emit('nodes:remove',node);
        }



        if (node && node._def.onremove) {
            // Deprecated: never documented but used by some early nodes
            console.log("Deprecated API warning: node type ",node.type," has an onremove function - should be oneditdelete - please report");
            node._def.onremove.call(node);
        }
        return {links:removedLinks,nodes:removedNodes};
    }

    function moveNodesForwards(nodes) {
        return allNodes.moveNodesForwards(nodes);
    }
    function moveNodesBackwards(nodes) {
        return allNodes.moveNodesBackwards(nodes);
    }
    function moveNodesToFront(nodes) {
        return allNodes.moveNodesToFront(nodes);
    }
    function moveNodesToBack(nodes) {
        return allNodes.moveNodesToBack(nodes);
    }

    function getNodeOrder(z) {
        return allNodes.getNodeOrder(z);
    }
    function setNodeOrder(z, order) {
        allNodes.setNodeOrder(z,order);
    }

    function moveNodeToTab(node, z) {
        if (node.type === "group") {
            moveGroupToTab(node,z);
            return;
        }
        if (node.type === "junction") {
            moveJunctionToTab(node,z);
            return;
        }
        var oldZ = node.z;
        allNodes.moveNode(node,z);
        var nl = nodeLinks[node.id];
        if (nl) {
            nl.in.forEach(function(l) {
                var idx = linkTabMap[oldZ].indexOf(l);
                if (idx != -1) {
                    linkTabMap[oldZ].splice(idx, 1);
                }
                if ((l.source.z === z) && linkTabMap[z]) {
                    linkTabMap[z].push(l);
                }
            });
            nl.out.forEach(function(l) {
                var idx = linkTabMap[oldZ].indexOf(l);
                if (idx != -1) {
                    linkTabMap[oldZ].splice(idx, 1);
                }
                if ((l.target.z === z) && linkTabMap[z]) {
                    linkTabMap[z].push(l);
                }
            });
        }
        RED.events.emit("nodes:change",node);
    }
    function moveGroupToTab(group, z) {
        var index = groupsByZ[group.z].indexOf(group);
        groupsByZ[group.z].splice(index,1);
        groupsByZ[z] = groupsByZ[z] || [];
        groupsByZ[z].push(group);
        group.z = z;
        RED.events.emit("groups:change",group);
    }

    function moveJunctionToTab(junction, z) {
        var index = junctionsByZ[junction.z].indexOf(junction);
        junctionsByZ[junction.z].splice(index,1);
        junctionsByZ[z] = junctionsByZ[z] || [];
        junctionsByZ[z].push(junction);

        var oldZ = junction.z;
        junction.z = z;

        var nl = nodeLinks[junction.id];
        if (nl) {
            nl.in.forEach(function(l) {
                var idx = linkTabMap[oldZ].indexOf(l);
                if (idx != -1) {
                    linkTabMap[oldZ].splice(idx, 1);
                }
                if ((l.source.z === z) && linkTabMap[z]) {
                    linkTabMap[z].push(l);
                }
            });
            nl.out.forEach(function(l) {
                var idx = linkTabMap[oldZ].indexOf(l);
                if (idx != -1) {
                    linkTabMap[oldZ].splice(idx, 1);
                }
                if ((l.target.z === z) && linkTabMap[z]) {
                    linkTabMap[z].push(l);
                }
            });
        }
        RED.events.emit("junctions:change",junction);
    }

    function removeLink(l) {
        var index = links.indexOf(l);
        if (index != -1) {
            links.splice(index,1);
            if (l.source && nodeLinks[l.source.id]) {
                var sIndex = nodeLinks[l.source.id].out.indexOf(l)
                if (sIndex !== -1) {
                    nodeLinks[l.source.id].out.splice(sIndex,1)
                }
            }
            if (l.target && nodeLinks[l.target.id]) {
                var tIndex = nodeLinks[l.target.id].in.indexOf(l)
                if (tIndex !== -1) {
                    nodeLinks[l.target.id].in.splice(tIndex,1)
                }
            }
            if (l.source.z === l.target.z && linkTabMap[l.source.z]) {
                index = linkTabMap[l.source.z].indexOf(l);
                if (index !== -1) {
                    linkTabMap[l.source.z].splice(index,1)
                }
                allNodes.removeObjectFromWorkspace(l.source.z, getLinkId(l))
            }
        }
        RED.events.emit("links:remove",l);
    }

    function addWorkspace(ws,targetIndex) {
        workspaces[ws.id] = ws;
        allNodes.addTab(ws.id);
        linkTabMap[ws.id] = [];

        ws._def = RED.nodes.getType('tab');
        if (targetIndex === undefined) {
            workspacesOrder.push(ws.id);
        } else {
            workspacesOrder.splice(targetIndex,0,ws.id);
        }
        RED.events.emit('flows:add',ws);
        if (targetIndex !== undefined) {
            RED.events.emit('flows:reorder',workspacesOrder)
        }
    }
    function getWorkspace(id) {
        return workspaces[id];
    }
    function removeWorkspace(id) {
        var ws = workspaces[id];
        var removedNodes = [];
        var removedLinks = [];
        var removedGroups = [];
        var removedJunctions = [];
        if (ws) {
            delete workspaces[id];
            delete linkTabMap[id];
            workspacesOrder.splice(workspacesOrder.indexOf(id),1);
            var i;
            var node;

            if (allNodes.hasTab(id)) {
                removedNodes = allNodes.getNodes(id).slice()
            }
            for (i in configNodes) {
                if (configNodes.hasOwnProperty(i)) {
                    node = configNodes[i];
                    if (node.z == id) {
                        removedNodes.push(node);
                    }
                }
            }
            removedJunctions = RED.nodes.junctions(id)

            for (i=0;i<removedNodes.length;i++) {
                var result = removeNode(removedNodes[i].id);
                removedLinks = removedLinks.concat(result.links);
            }
            for (i=0;i<removedJunctions.length;i++) {
                var result = removeJunction(removedJunctions[i])
                removedLinks = removedLinks.concat(result.links)
            }

            // Must get 'removedGroups' in the right order.
            //  - start with the top-most groups
            //  - then recurse into them
            removedGroups = (groupsByZ[id] || []).filter(function(g) { return !g.g; });
            for (i=0;i<removedGroups.length;i++) {
                removedGroups[i].nodes.forEach(function(n) {
                    if (n.type === "group") {
                        removedGroups.push(n);
                    }
                });
            }
            // Now remove them in the reverse order
            for (i=removedGroups.length-1; i>=0; i--) {
                removeGroup(removedGroups[i]);
            }
            allNodes.removeTab(id);
            RED.events.emit('flows:remove',ws);
        }
        return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions};
    }

    /**
      * Add a Subflow to the Workspace
      *
      * @param {object} sf The Subflow to add.
      * @param {boolean|undefined} createNewIds Whether to update the name.
      */
    function addSubflow(sf, createNewIds) {
        if (createNewIds) {
            // Update the Subflow name to highlight that this is a copy
            const subflowNames = Object.keys(subflows).map(function (sfid) {
                return subflows[sfid].name || "";
            })
            subflowNames.sort()

            let copyNumber = 1;
            let subflowName = sf.name;
            subflowNames.forEach(function(name) {
                if (subflowName == name) {
                    subflowName = sf.name + " (" + copyNumber + ")";
                    copyNumber++;
                }
            });

            sf.name = subflowName;
        }

        sf.instances = [];

        subflows[sf.id] = sf;
        allNodes.addTab(sf.id);
        linkTabMap[sf.id] = [];

        RED.nodes.registerType("subflow:"+sf.id, {
            defaults:{
                name:{value:""},
                env:{value:[], validate: function(value) {
                    const errors = []
                    if (value) {
                        value.forEach(env => {
                            const r = RED.utils.validateTypedProperty(env.value, env.type)
                            if (r !== true) {
                                errors.push(env.name+': '+r)
                            }
                        })
                    }
                    if (errors.length === 0) {
                        return true
                    } else {
                        return errors
                    }
                }}
            },
            icon: function() { return sf.icon||"subflow.svg" },
            category: sf.category || "subflows",
            inputs: sf.in.length,
            outputs: sf.out.length,
            color: sf.color || "#DDAA99",
            label: function() { return this.name||RED.nodes.subflow(sf.id).name },
            labelStyle: function() { return this.name?"red-ui-flow-node-label-italic":""; },
            paletteLabel: function() { return RED.nodes.subflow(sf.id).name },
            inputLabels: function(i) { return sf.inputLabels?sf.inputLabels[i]:null },
            outputLabels: function(i) { return sf.outputLabels?sf.outputLabels[i]:null },
            oneditprepare: function() {
                if (this.type !== 'subflow') {
                    // A subflow instance node
                    RED.subflow.buildEditForm("subflow",this);
                } else {
                    // A subflow template node
                    RED.subflow.buildEditForm("subflow-template", this);
                }
            },
            oneditresize: function(size) {
                if (this.type === 'subflow') {
                    $("#node-input-env-container").editableList('height',size.height - 80);
                }
            },
            set:{
                module: "node-red"
            }
        });

        sf._def = RED.nodes.getType("subflow:"+sf.id);
        RED.events.emit("subflows:add",sf);
    }
    function getSubflow(id) {
        return subflows[id];
    }
    function removeSubflow(sf) {
        if (subflows[sf.id]) {
            delete subflows[sf.id];
            allNodes.removeTab(sf.id);
            registry.removeNodeType("subflow:"+sf.id);
            RED.events.emit("subflows:remove",sf);
        }
    }

    function subflowContains(sfid,nodeid) {
        var sfNodes = allNodes.getNodes(sfid);
        for (var i = 0; i<sfNodes.length; i++) {
            var node = sfNodes[i];
            var m = /^subflow:(.+)$/.exec(node.type);
            if (m) {
                if (m[1] === nodeid) {
                    return true;
                } else {
                    var result = subflowContains(m[1],nodeid);
                    if (result) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    function getDownstreamNodes(node) {
        const downstreamLinks = nodeLinks[node.id].out
        const downstreamNodes = new Set(downstreamLinks.map(l => l.target))
        return Array.from(downstreamNodes)
    }
    function getAllDownstreamNodes(node) {
        return getAllFlowNodes(node,'down').filter(function(n) { return n !== node });
    }
    function getAllUpstreamNodes(node) {
        return getAllFlowNodes(node,'up').filter(function(n) { return n !== node });
    }
    function getAllFlowNodes(node, direction) {
        var selection = RED.view.selection();
        var visited = new Set();
        var nodes = [node];
        var initialNode = true;
        while(nodes.length > 0) {
            var n = nodes.shift();
            visited.add(n);
            var links = [];
            if (!initialNode || !direction || (initialNode && direction === 'up')) {
                links = links.concat(nodeLinks[n.id].in);
            }
            if (!initialNode || !direction || (initialNode && direction === 'down')) {
                links = links.concat(nodeLinks[n.id].out);
            }
            initialNode = false;
            links.forEach(function(l) {
                if (!visited.has(l.source)) {
                    nodes.push(l.source);
                }
                if (!visited.has(l.target)) {
                    nodes.push(l.target);
                }
            })
        }
        return Array.from(visited);
    }


    function convertWorkspace(n,opts) {
        var exportCreds = true;
        if (opts) {
            if (opts.hasOwnProperty("credentials")) {
                exportCreds = opts.credentials;
            }
        }
        var node = {};
        node.id = n.id;
        node.type = n.type;
        for (var d in n._def.defaults) {
            if (n._def.defaults.hasOwnProperty(d)) {
                if (d === 'locked' && !n.locked) {
                    continue
                }
                node[d] = n[d];
            }
        }
        if (exportCreds) {
            var credentialSet = {};
            if (n.credentials) {
                for (var tabCred in n.credentials) {
                    if (n.credentials.hasOwnProperty(tabCred)) {
                        if (!n.credentials._ ||
                            n.credentials["has_"+tabCred] != n.credentials._["has_"+tabCred] ||
                            (n.credentials["has_"+tabCred] && n.credentials[tabCred])) {
                            credentialSet[tabCred] = n.credentials[tabCred];
                        }
                    }
                }
                if (Object.keys(credentialSet).length > 0) {
                    node.credentials = credentialSet;
                }
            }
        }
        return node;
    }
    /**
     * Converts a node to an exportable JSON Object
     **/
    function convertNode(n, opts) {
        var exportCreds = true;
        var exportDimensions = false;
        if (opts === false) {
            exportCreds = false;
        } else if (typeof opts === "object") {
            if (opts.hasOwnProperty("credentials")) {
                exportCreds = opts.credentials;
            }
            if (opts.hasOwnProperty("dimensions")) {
                exportDimensions = opts.dimensions;
            }
        }

        if (n.type === 'tab') {
            return convertWorkspace(n, { credentials: exportCreds });
        }
        var node = {};
        node.id = n.id;
        node.type = n.type;
        node.z = n.z;
        if (node.z === 0 || node.z === "") {
            delete node.z;
        }
        if (n.d === true) {
            node.d = true;
        }
        if (n.g) {
            node.g = n.g;
        }
        if (node.type == "unknown") {
            for (var p in n._orig) {
                if (n._orig.hasOwnProperty(p)) {
                    node[p] = n._orig[p];
                }
            }
        } else {
            for (var d in n._def.defaults) {
                if (n._def.defaults.hasOwnProperty(d)) {
                    node[d] = n[d];
                }
            }
            if (exportCreds) {
                var credentialSet = {};
                if ((/^subflow:/.test(node.type) ||
                     (node.type === "group")) &&
                    n.credentials) {
                    // A subflow instance/group node can have arbitrary creds
                    for (var sfCred in n.credentials) {
                        if (n.credentials.hasOwnProperty(sfCred)) {
                            if (!n.credentials._ ||
                                n.credentials["has_"+sfCred] != n.credentials._["has_"+sfCred] ||
                                (n.credentials["has_"+sfCred] && n.credentials[sfCred])) {
                                credentialSet[sfCred] = n.credentials[sfCred];
                            }
                        }
                    }
                } else if (n.credentials) {
                    // All other nodes have a well-defined list of possible credentials
                    for (var cred in n._def.credentials) {
                        if (n._def.credentials.hasOwnProperty(cred)) {
                            if (n._def.credentials[cred].type == 'password') {
                                if (!n.credentials._ ||
                                    n.credentials["has_"+cred] != n.credentials._["has_"+cred] ||
                                    (n.credentials["has_"+cred] && n.credentials[cred])) {
                                    credentialSet[cred] = n.credentials[cred];
                                }
                            } else if (n.credentials[cred] != null && (!n.credentials._ || n.credentials[cred] != n.credentials._[cred])) {
                                credentialSet[cred] = n.credentials[cred];
                            }
                        }
                    }
                }
                if (Object.keys(credentialSet).length > 0) {
                    node.credentials = credentialSet;
                }
            }
        }
        if (n.type === "group") {
            node.x = n.x;
            node.y = n.y;
            node.w = n.w;
            node.h = n.h;
            // In 1.1.0, we have seen an instance of this array containing `undefined`
            // Until we know how that can happen, add a filter here to remove them
            node.nodes = node.nodes.filter(function(n) { return !!n }).map(function(n) { return n.id });
        }
        if (n.type === "tab" || n.type === "group") {
            if (node.env && node.env.length === 0) {
                delete node.env;
            }
        }
        if (n._def.category != "config" || n.type === 'junction') {
            node.x = n.x;
            node.y = n.y;
            if (exportDimensions) {
                if (!n.hasOwnProperty('w')) {
                    // This node has not yet been drawn in the view. So we need
                    // to explicitly calculate its dimensions. Store the result
                    // on the node as if it had been drawn will save us doing
                    // it again
                    var dimensions = RED.view.calculateNodeDimensions(n);
                    n.w = dimensions[0];
                    n.h = dimensions[1];
                }
                node.w = n.w;
                node.h = n.h;
            }
            node.wires = [];
            for(var i=0;i<n.outputs;i++) {
                node.wires.push([]);
            }
            var wires = links.filter(function(d){return d.source === n;});
            for (var j=0;j<wires.length;j++) {
                var w = wires[j];
                if (w.target.type != "subflow") {
                    if (w.sourcePort < node.wires.length) {
                        node.wires[w.sourcePort].push(w.target.id);
                    }
                }
            }

            if (n.inputs > 0 && n.inputLabels && !/^\s*$/.test(n.inputLabels.join("")))  {
                node.inputLabels = n.inputLabels.slice();
            }
            if (n.outputs > 0 && n.outputLabels && !/^\s*$/.test(n.outputLabels.join(""))) {
                node.outputLabels = n.outputLabels.slice();
            }
            if ((!n._def.defaults || !n._def.defaults.hasOwnProperty("icon")) && n.icon) {
                var defIcon = RED.utils.getDefaultNodeIcon(n._def, n);
                if (n.icon !== defIcon.module+"/"+defIcon.file) {
                    node.icon = n.icon;
                }
            }
            if ((!n._def.defaults || !n._def.defaults.hasOwnProperty("l")) && n.hasOwnProperty('l')) {
                var showLabel = n._def.hasOwnProperty("showLabel")?n._def.showLabel:true;
                if (showLabel != n.l) {
                    node.l = n.l;
                }
            }
        }
        if (n.info) {
            node.info = n.info;
        }
        return node;
    }

    function convertSubflow(n, opts) {
        var exportCreds = true;
        var exportDimensions = false;
        if (opts === false) {
            exportCreds = false;
        } else if (typeof opts === "object") {
            if (opts.hasOwnProperty("credentials")) {
                exportCreds = opts.credentials;
            }
            if (opts.hasOwnProperty("dimensions")) {
                exportDimensions = opts.dimensions;
            }
        }


        var node = {};
        node.id = n.id;
        node.type = n.type;
        node.name = n.name;
        node.info = n.info;
        node.category = n.category;
        node.in = [];
        node.out = [];
        node.env = n.env;
        node.meta = n.meta;

        if (exportCreds) {
            var credentialSet = {};
            // A subflow node can have arbitrary creds
            for (var sfCred in n.credentials) {
                if (n.credentials.hasOwnProperty(sfCred)) {
                    if (!n.credentials._ ||
                        n.credentials["has_"+sfCred] != n.credentials._["has_"+sfCred] ||
                        (n.credentials["has_"+sfCred] && n.credentials[sfCred])) {
                        credentialSet[sfCred] = n.credentials[sfCred];
                    }
                }
            }
            if (Object.keys(credentialSet).length > 0) {
                node.credentials = credentialSet;
            }
        }

        node.color = n.color;

        n.in.forEach(function(p) {
            var nIn = {x:p.x,y:p.y,wires:[]};
            var wires = links.filter(function(d) { return d.source === p });
            for (var i=0;i<wires.length;i++) {
                var w = wires[i];
                if (w.target.type != "subflow") {
                    nIn.wires.push({id:w.target.id})
                }
            }
            node.in.push(nIn);
        });
        n.out.forEach(function(p,c) {
            var nOut = {x:p.x,y:p.y,wires:[]};
            var wires = links.filter(function(d) { return d.target === p });
            for (i=0;i<wires.length;i++) {
                if (wires[i].source.type != "subflow") {
                    nOut.wires.push({id:wires[i].source.id,port:wires[i].sourcePort})
                } else {
                    nOut.wires.push({id:n.id,port:0})
                }
            }
            node.out.push(nOut);
        });

        if (node.in.length > 0 && n.inputLabels && !/^\s*$/.test(n.inputLabels.join("")))  {
            node.inputLabels = n.inputLabels.slice();
        }
        if (node.out.length > 0 && n.outputLabels && !/^\s*$/.test(n.outputLabels.join(""))) {
            node.outputLabels = n.outputLabels.slice();
        }
        if (n.icon) {
            if (n.icon !== "node-red/subflow.svg") {
                node.icon = n.icon;
            }
        }
        if (n.status) {
            node.status = {x: n.status.x, y: n.status.y, wires:[]};
            links.forEach(function(d) {
                if (d.target === n.status) {
                    if (d.source.type != "subflow") {
                        node.status.wires.push({id:d.source.id, port:d.sourcePort})
                    } else {
                        node.status.wires.push({id:n.id, port:0})
                    }
                }
            });
        }

        return node;
    }

    function createExportableSubflow(id) {
        var sf = getSubflow(id);
        var nodeSet;
        var sfNodes = allNodes.getNodes(sf.id);
        if (sfNodes) {
            nodeSet = sfNodes.slice();
            nodeSet.unshift(sf);
        } else {
            nodeSet = [sf];
        }
        return createExportableNodeSet(nodeSet);
    }
    /**
     * Converts the current node selection to an exportable JSON Object
     **/
    function createExportableNodeSet(set, exportedIds, exportedSubflows, exportedConfigNodes) {
        var nns = [];

        exportedIds = exportedIds || {};
        set = set.filter(function(n) {
            if (exportedIds[n.id]) {
                return false;
            }
            exportedIds[n.id] = true;
            return true;
        })

        exportedConfigNodes = exportedConfigNodes || {};
        exportedSubflows = exportedSubflows || {};
        for (var n=0;n<set.length;n++) {
            var node = set[n];
            if (node.type.substring(0,8) == "subflow:") {
                var subflowId = node.type.substring(8);
                if (!exportedSubflows[subflowId]) {
                    exportedSubflows[subflowId] = true;
                    var subflow = getSubflow(subflowId);
                    var subflowSet = allNodes.getNodes(subflowId).slice();
                    subflowSet.unshift(subflow);

                    RED.nodes.eachConfig(function(n) {
                        if (n.z == subflowId) {
                            subflowSet.push(n);
                            exportedConfigNodes[n.id] = true;
                        }
                    });

                    subflowSet = subflowSet.concat(RED.nodes.junctions(subflowId))
                    subflowSet = subflowSet.concat(RED.nodes.groups(subflowId))

                    var exportableSubflow = createExportableNodeSet(subflowSet, exportedIds, exportedSubflows, exportedConfigNodes);
                    nns = exportableSubflow.concat(nns);
                }
            }
            if (node.type !== "subflow") {
                var convertedNode = RED.nodes.convertNode(node, { credentials: false });
                for (var d in node._def.defaults) {
                    if (node._def.defaults[d].type) {
                        var nodeList = node[d];
                        if (!Array.isArray(nodeList)) {
                            nodeList = [nodeList];
                        }
                        nodeList = nodeList.filter(function(id) {
                            if (id in configNodes) {
                                var confNode = configNodes[id];
                                if (confNode._def.exportable !== false) {
                                    if (!(id in exportedConfigNodes)) {
                                        exportedConfigNodes[id] = true;
                                        set.push(confNode);
                                    }
                                    return true;
                                }
                                return false;
                            }
                            return true;
                        })
                        if (nodeList.length === 0) {
                            convertedNode[d] = Array.isArray(node[d])?[]:""
                        } else {
                            convertedNode[d] = Array.isArray(node[d])?nodeList:nodeList[0]
                        }
                    }
                }
                nns.push(convertedNode);
                if (node.type === "group") {
                    nns = nns.concat(createExportableNodeSet(node.nodes, exportedIds, exportedSubflows, exportedConfigNodes));
                }
            } else {
                var convertedSubflow = convertSubflow(node, { credentials: false });
                nns.push(convertedSubflow);
            }
        }
        return nns;
    }

    // Create the Flow JSON for the current configuration
    // opts.credentials (whether to include (known) credentials) - default: true
    // opts.dimensions (whether to include node dimensions) - default: false
    function createCompleteNodeSet(opts) {
        var nns = [];
        var i;
        for (i=0;i<workspacesOrder.length;i++) {
            if (workspaces[workspacesOrder[i]].type == "tab") {
                nns.push(convertWorkspace(workspaces[workspacesOrder[i]], opts));
            }
        }
        for (i in subflows) {
            if (subflows.hasOwnProperty(i)) {
                nns.push(convertSubflow(subflows[i], opts));
            }
        }
        for (i in groups) {
            if (groups.hasOwnProperty(i)) {
                nns.push(convertNode(groups[i], opts));
            }
        }
        for (i in junctions) {
            if (junctions.hasOwnProperty(i)) {
                nns.push(convertNode(junctions[i], opts));
            }
        }
        for (i in configNodes) {
            if (configNodes.hasOwnProperty(i)) {
                nns.push(convertNode(configNodes[i], opts));
            }
        }
        RED.nodes.eachNode(function(n) {
            nns.push(convertNode(n, opts));
        })
        return nns;
    }

    function checkForMatchingSubflow(subflow,subflowNodes) {
        subflowNodes = subflowNodes || [];
        var i;
        var match = null;
        RED.nodes.eachSubflow(function(sf) {
            if (sf.name != subflow.name ||
                sf.info != subflow.info ||
                sf.in.length != subflow.in.length ||
                sf.out.length != subflow.out.length) {
                    return;
            }
            var sfNodes = RED.nodes.filterNodes({z:sf.id});
            if (sfNodes.length != subflowNodes.length) {
                return;
            }

            var subflowNodeSet = [subflow].concat(subflowNodes);
            var sfNodeSet = [sf].concat(sfNodes);

            var exportableSubflowNodes = JSON.stringify(subflowNodeSet);
            var exportableSFNodes = JSON.stringify(createExportableNodeSet(sfNodeSet));
            var nodeMap = {};
            for (i=0;i<sfNodes.length;i++) {
                exportableSubflowNodes = exportableSubflowNodes.replace(new RegExp("\""+subflowNodes[i].id+"\"","g"),'"'+sfNodes[i].id+'"');
            }
            exportableSubflowNodes = exportableSubflowNodes.replace(new RegExp("\""+subflow.id+"\"","g"),'"'+sf.id+'"');

            if (exportableSubflowNodes !== exportableSFNodes) {
                return;
            }

            match = sf;
            return false;
        });
        return match;
    }
    function compareNodes(nodeA,nodeB,idMustMatch) {
        if (idMustMatch && nodeA.id != nodeB.id) {
            return false;
        }
        if (nodeA.type != nodeB.type) {
            return false;
        }
        var def = nodeA._def;
        for (var d in def.defaults) {
            if (def.defaults.hasOwnProperty(d)) {
                var vA = nodeA[d];
                var vB = nodeB[d];
                if (typeof vA !== typeof vB) {
                    return false;
                }
                if (vA === null || typeof vA === "string" || typeof vA === "number") {
                    if (vA !== vB) {
                        return false;
                    }
                } else {
                    if (JSON.stringify(vA) !== JSON.stringify(vB)) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    function identifyImportConflicts(importedNodes) {
        var imported = {
            tabs: {},
            subflows: {},
            groups: {},
            junctions: {},
            configs: {},
            nodes: {},
            all: [],
            conflicted: {},
            zMap: {},
        }

        importedNodes.forEach(function(n) {
            imported.all.push(n);
            if (n.type === "tab") {
                imported.tabs[n.id] = n;
            } else if (n.type === "subflow") {
                imported.subflows[n.id] = n;
            } else if (n.type === "group") {
                imported.groups[n.id] = n;
            } else if (n.type === "junction") {
                imported.junctions[n.id] = n;
            } else if (n.hasOwnProperty("x") && n.hasOwnProperty("y")) {
                imported.nodes[n.id] = n;
            } else {
                imported.configs[n.id] = n;
            }
            var nodeZ = n.z || "__global__";
            imported.zMap[nodeZ] = imported.zMap[nodeZ] || [];
            imported.zMap[nodeZ].push(n)
            if (allNodes.hasNode(n.id) || configNodes[n.id] || workspaces[n.id] || subflows[n.id] || groups[n.id] || junctions[n.id]) {
                imported.conflicted[n.id] = n;
            }
        })
        return imported;

    }

    /**
     * Replace the provided nodes.
     * This must contain complete Subflow defs or complete Flow Tabs.
     * It does not replace an individual node in the middle of a flow.
     */
    function replaceNodes(newNodes) {
        var zMap = {};
        var newSubflows = {};
        var newConfigNodes = {};
        var removedNodes = [];
        // Figure out what we're being asked to replace - subflows/configNodes
        // TODO: config nodes
        newNodes.forEach(function(n) {
            if (n.type === "subflow") {
                newSubflows[n.id] = n;
            } else if (!n.hasOwnProperty('x') && !n.hasOwnProperty('y')) {
                newConfigNodes[n.id] = n;
            }
            if (n.z) {
                zMap[n.z] = zMap[n.z] || [];
                zMap[n.z].push(n);
            }
        })

        // Filter out config nodes inside a subflow def that is being replaced
        var configNodeIds = Object.keys(newConfigNodes);
        configNodeIds.forEach(function(id) {
            var n = newConfigNodes[id];
            if (newSubflows[n.z]) {
                // This config node is in a subflow to be replaced.
                //  - remove from the list as it'll get handled with the subflow
                delete newConfigNodes[id];
            }
        });
        // Rebuild the list of ids
        configNodeIds = Object.keys(newConfigNodes);

        // ------------------------------
        // Replace subflow definitions
        //
        // For each of the subflows to be replaced:
        var newSubflowIds = Object.keys(newSubflows);
        newSubflowIds.forEach(function(id) {
            var n = newSubflows[id];
            // Get a snapshot of the existing subflow definition
            removedNodes = removedNodes.concat(createExportableSubflow(id));
            // Remove the old subflow definition - but leave the instances in place
            var removalResult = RED.subflow.removeSubflow(n.id, true);
            // Create the list of nodes for the new subflow def
            // Need to sort the list in order to remove missing nodes
            var subflowNodes = [n].concat(zMap[n.id]).filter((s) => !!s);
            // Import the new subflow - no clashes should occur as we've removed
            // the old version
            var result = importNodes(subflowNodes);
            newSubflows[id] = getSubflow(id);
        })

        // Having replaced the subflow definitions, now need to update the
        // instance nodes.
        RED.nodes.eachNode(function(n) {
            if (/^subflow:/.test(n.type)) {
                var sfId = n.type.substring(8);
                if (newSubflows[sfId]) {
                    // This is an instance of one of the replaced subflows
                    //  - update the new def's instances array to include this one
                    newSubflows[sfId].instances.push(n);
                    //  - update the instance's _def to point to the new def
                    n._def = RED.nodes.getType(n.type);
                    //  - set all the flags so the view refreshes properly
                    n.dirty = true;
                    n.changed = true;
                    n._colorChanged = true;
                }
            }
        })

        newSubflowIds.forEach(function(id) {
            var n = newSubflows[id];
            RED.events.emit("subflows:change",n);
        })
        // Just in case the imported subflow changed color.
        RED.utils.clearNodeColorCache();

        // ------------------------------
        // Replace config nodes
        //
        configNodeIds.forEach(function(id) {
            const configNode = getNode(id);
            const currentUserCount = configNode.users;

            // Add a snapshot of the Config Node
            removedNodes = removedNodes.concat(convertNode(configNode));

            // Remove the Config Node instance
            removeNode(id);

            // Import the new one
            importNodes([newConfigNodes[id]]);

            // Re-attributes the user count
            getNode(id).users = currentUserCount;
        });

        return {
            removedNodes: removedNodes
        }

    }

    /**
     * Options:
     *  - generateIds - whether to replace all node ids
     *  - addFlow - whether to import nodes to a new tab
     *  - markChanged - whether to set changed=true on all newly imported objects
     *  - reimport - if node has a .z property, dont overwrite it
     *               Only applicible when `generateIds` is false
     *  - importMap - how to resolve any conflicts.
     *       - id:import - import as-is
     *       - id:copy - import with new id
     *       - id:replace - import over the top of existing
     */
    function importNodes(newNodesObj,options) { // createNewIds,createMissingWorkspace) {
        const defOpts = { generateIds: false, addFlow: false, markChanged: false, reimport: false, importMap: {} }
        options = Object.assign({}, defOpts, options)
        options.importMap = options.importMap || {}
        const createNewIds = options.generateIds;
        const reimport = (!createNewIds && !!options.reimport)
        const createMissingWorkspace = options.addFlow;
        var i;
        var n;
        var newNodes;
        var nodeZmap = {};
        var recoveryWorkspace;
        if (typeof newNodesObj === "string") {
            if (newNodesObj === "") {
                return;
            }
            try {
                newNodes = JSON.parse(newNodesObj);
            } catch(err) {
                var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
                e.code = "NODE_RED";
                throw e;
            }
        } else {
            newNodes = newNodesObj;
        }

        if (!Array.isArray(newNodes)) {
            newNodes = [newNodes];
        }

        // Scan for any duplicate nodes and remove them. This is a temporary
        // fix to help resolve corrupted flows caused by 0.20.0 where multiple
        // copies of the flow would get loaded at the same time.
        // If the user hit deploy they would have saved those duplicates.
        var seenIds = {};
        var existingNodes = [];
        var nodesToReplace = [];

        newNodes = newNodes.filter(function(n) {
            var id = n.id;
            if (seenIds[n.id]) {
                return false;
            }
            seenIds[n.id] = true;

            if (!options.generateIds) {
                if (!options.importMap[id]) {
                    // No conflict resolution for this node
                    var existing = allNodes.getNode(id) || configNodes[id] || workspaces[id] || subflows[id] || groups[id] || junctions[id];
                    if (existing) {
                        existingNodes.push({existing:existing, imported:n});
                    }
                } else if (options.importMap[id] === "replace") {
                    nodesToReplace.push(n);
                    return false;
                }
            }

            return true;
        })

        if (existingNodes.length > 0) {
            var errorMessage = RED._("clipboard.importDuplicate",{count:existingNodes.length});
            var nodeList = $("<ul>");
            var existingNodesCount = Math.min(5,existingNodes.length);
            for (var i=0;i<existingNodesCount;i++) {
                var conflict = existingNodes[i];
                $("<li>").text(
                    conflict.existing.id+
                    " [ "+conflict.existing.type+ ((conflict.imported.type !== conflict.existing.type)?" | "+conflict.imported.type:"")+" ]").appendTo(nodeList)
            }
            if (existingNodesCount !== existingNodes.length) {
                $("<li>").text(RED._("deploy.confirm.plusNMore",{count:existingNodes.length-existingNodesCount})).appendTo(nodeList)
            }
            var wrapper = $("<p>").append(nodeList);

            var existingNodesError = new Error(errorMessage+wrapper.html());
            existingNodesError.code = "import_conflict";
            existingNodesError.importConfig = identifyImportConflicts(newNodes);
            throw existingNodesError;
        }
        var removedNodes;
        if (nodesToReplace.length > 0) {
            var replaceResult = replaceNodes(nodesToReplace);
            removedNodes = replaceResult.removedNodes;
        }


        var isInitialLoad = false;
        if (!initialLoad) {
            isInitialLoad = true;
            initialLoad = JSON.parse(JSON.stringify(newNodes));
        }
        var unknownTypes = [];
        for (i=0;i<newNodes.length;i++) {
            n = newNodes[i];
            var id = n.id;
            // TODO: remove workspace in next release+1
            if (n.type != "workspace" &&
                n.type != "tab" &&
                n.type != "subflow" &&
                n.type != "group" &&
                n.type != 'junction' &&
                !registry.getNodeType(n.type) &&
                n.type.substring(0,8) != "subflow:" &&
                unknownTypes.indexOf(n.type)==-1) {
                    unknownTypes.push(n.type);
            }
            if (n.z) {
                nodeZmap[n.z] = nodeZmap[n.z] || [];
                nodeZmap[n.z].push(n);
            } else if (isInitialLoad && n.hasOwnProperty('x') && n.hasOwnProperty('y') && !n.z) {
                // Hit the rare issue where node z values get set to 0.
                // Repair the flow - but we really need to track that down.
                if (!recoveryWorkspace) {
                    recoveryWorkspace = {
                        id: RED.nodes.id(),
                        type: "tab",
                        disabled: false,
                        label: RED._("clipboard.recoveredNodes"),
                        info: RED._("clipboard.recoveredNodesInfo"),
                        env: []
                    }
                    addWorkspace(recoveryWorkspace);
                    RED.workspaces.add(recoveryWorkspace);
                    nodeZmap[recoveryWorkspace.id] = [];
                }
                n.z = recoveryWorkspace.id;
                nodeZmap[recoveryWorkspace.id].push(n);
            }

        }
        if (!isInitialLoad && unknownTypes.length > 0) {
            var typeList = $("<ul>");
            unknownTypes.forEach(function(t) {
                $("<li>").text(t).appendTo(typeList);
            })
            typeList = typeList[0].outerHTML;
            RED.notify("<p>"+RED._("clipboard.importUnrecognised",{count:unknownTypes.length})+"</p>"+typeList,"error",false,10000);
        }

        var activeWorkspace = RED.workspaces.active();
        //TODO: check the z of the subflow instance and check _that_ if it exists
        var activeSubflow = getSubflow(activeWorkspace);
        for (i=0;i<newNodes.length;i++) {
            var m = /^subflow:(.+)$/.exec(newNodes[i].type);
            if (m) {
                var subflowId = m[1];
                var parent = getSubflow(activeWorkspace);
                if (parent) {
                    var err;
                    if (subflowId === parent.id) {
                        err = new Error(RED._("notification.errors.cannotAddSubflowToItself"));
                    }
                    if (subflowContains(subflowId,parent.id)) {
                        err = new Error(RED._("notification.errors.cannotAddCircularReference"));
                    }
                    if (err) {
                        // TODO: standardise error codes
                        err.code = "NODE_RED";
                        throw err;
                    }
                }
            }
        }

        var new_workspaces = [];
        var workspace_map = {};
        var new_subflows = [];
        var subflow_map = {};
        var subflow_denylist = {};
        var node_map = {};
        var new_nodes = [];
        var new_links = [];
        var new_groups = [];
        var new_junctions = [];
        var new_group_set = new Set();
        var nid;
        var def;
        var configNode;
        var missingWorkspace = null;
        var d;

        if (recoveryWorkspace) {
            new_workspaces.push(recoveryWorkspace);
        }

        // Find all tabs and subflow templates
        for (i=0;i<newNodes.length;i++) {
            n = newNodes[i];
            // TODO: remove workspace in next release+1
            if (n.type === "workspace" || n.type === "tab") {
                if (n.type === "workspace") {
                    n.type = "tab";
                }
                if (defaultWorkspace == null) {
                    defaultWorkspace = n;
                }
                if (activeWorkspace === 0) {
                    activeWorkspace = n.id;
                }
                if (createNewIds || options.importMap[n.id] === "copy") {
                    nid = getID();
                    workspace_map[n.id] = nid;
                    n.id = nid;
                } else {
                    workspace_map[n.id] = n.id;
                }
                addWorkspace(n);
                RED.workspaces.add(n);
                new_workspaces.push(n);
            } else if (n.type === "subflow") {
                var matchingSubflow;
                if (!options.importMap[n.id]) {
                    matchingSubflow = checkForMatchingSubflow(n,nodeZmap[n.id]);
                }
                if (matchingSubflow) {
                    subflow_denylist[n.id] = matchingSubflow;
                } else {
                    const oldId = n.id;

                    subflow_map[n.id] = n;
                    if (createNewIds || options.importMap[n.id] === "copy") {
                        nid = getID();
                        n.id = nid;
                    }
                    // TODO: handle createNewIds - map old to new subflow ids
                    n.in.forEach(function(input,i) {
                        input.type = "subflow";
                        input.direction = "in";
                        input.z = n.id;
                        input.i = i;
                        input.id = getID();
                    });
                    n.out.forEach(function(output,i) {
                        output.type = "subflow";
                        output.direction = "out";
                        output.z = n.id;
                        output.i = i;
                        output.id = getID();
                    });
                    if (n.status) {
                        n.status.type = "subflow";
                        n.status.direction = "status";
                        n.status.z = n.id;
                        n.status.id = getID();
                    }
                    new_subflows.push(n);
                    addSubflow(n,createNewIds || options.importMap[oldId] === "copy");
                }
            }
        }

        // Add a tab if there isn't one there already
        if (defaultWorkspace == null) {
            defaultWorkspace = { type:"tab", id:getID(), disabled: false, info:"",  label:RED._('workspace.defaultName',{number:1}), env:[]};
            addWorkspace(defaultWorkspace);
            RED.workspaces.add(defaultWorkspace);
            new_workspaces.push(defaultWorkspace);
            activeWorkspace = RED.workspaces.active();
        }

        const pendingConfigNodes = []
        const pendingConfigNodeIds = new Set()
        // Find all config nodes and add them
        for (i=0;i<newNodes.length;i++) {
            n = newNodes[i];
            def = registry.getNodeType(n.type);
            if (def && def.category == "config") {
                var existingConfigNode = null;
                if (createNewIds || options.importMap[n.id] === "copy") {
                    if (n.z) {
                        if (subflow_denylist[n.z]) {
                            continue;
                        } else if (subflow_map[n.z]) {
                            n.z = subflow_map[n.z].id;
                        } else {
                            n.z = workspace_map[n.z];
                            if (!workspaces[n.z]) {
                                if (createMissingWorkspace) {
                                    if (missingWorkspace === null) {
                                        missingWorkspace = RED.workspaces.add(null,true);
                                        new_workspaces.push(missingWorkspace);
                                    }
                                    n.z = missingWorkspace.id;
                                } else {
                                    n.z = activeWorkspace;
                                }
                            }
                        }
                    }
                    if (options.importMap[n.id] !== "copy") {
                        existingConfigNode = RED.nodes.node(n.id);
                        if (existingConfigNode) {
                            if (n.z && existingConfigNode.z !== n.z) {
                                existingConfigNode = null;
                                // Check the config nodes on n.z
                                for (var cn in configNodes) {
                                    if (configNodes.hasOwnProperty(cn)) {
                                        if (configNodes[cn].z === n.z && compareNodes(configNodes[cn],n,false)) {
                                            existingConfigNode = configNodes[cn];
                                            node_map[n.id] = configNodes[cn];
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    }
                } else {
                    const keepNodesCurrentZ = reimport && n.z && (RED.workspaces.contains(n.z) || RED.nodes.subflow(n.z))
                    if (!keepNodesCurrentZ && n.z && !workspace_map[n.z] && !subflow_map[n.z]) {
                        n.z = activeWorkspace;
                    }
                }

                if (!existingConfigNode || existingConfigNode._def.exclusive) { //} || !compareNodes(existingConfigNode,n,true) || existingConfigNode.z !== n.z) {
                    configNode = {
                        id:n.id,
                        z:n.z,
                        type:n.type,
                        info: n.info,
                        users:[],
                        _config:{},
                        _configNodeReferences: new Set()
                    };
                    if (!n.z) {
                        delete configNode.z;
                    }
                    if (options.markChanged) {
                        configNode.changed = true
                    }
                    if (n.hasOwnProperty('d')) {
                        configNode.d = n.d;
                    }
                    for (d in def.defaults) {
                        if (def.defaults.hasOwnProperty(d)) {
                            configNode[d] = n[d];
                            configNode._config[d] = JSON.stringify(n[d]);
                            if (def.defaults[d].type) {
                                configNode._configNodeReferences.add(n[d])
                            }
                        }
                    }
                    if (def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) {
                        configNode.credentials = {};
                        for (d in def.credentials) {
                            if (def.credentials.hasOwnProperty(d) && n.credentials.hasOwnProperty(d)) {
                                configNode.credentials[d] = n.credentials[d];
                            }
                        }
                    }
                    configNode.label = def.label;
                    configNode._def = def;
                    if (createNewIds || options.importMap[n.id] === "copy") {
                        configNode.id = getID();
                    }
                    node_map[n.id] = configNode;
                    pendingConfigNodes.push(configNode);
                    pendingConfigNodeIds.add(configNode.id)
                }
            }
        }

        // We need to sort new_nodes (which only contains config nodes at this point)
        // to ensure they get added in the right order. If NodeA depends on NodeB, then
        // NodeB must be added first.
        
        // Limit us to 5 full iterations of the list - this should be more than
        // enough to process the list as config->config node relationships are
        // not very common
        let iterationLimit = pendingConfigNodes.length * 5
        const handledConfigNodes = new Set()
        while (pendingConfigNodes.length > 0 && iterationLimit > 0) {
            const node = pendingConfigNodes.shift()
            let hasPending = false
            // Loop through the nodes referenced by this node to see if anything
            // is pending
            node._configNodeReferences.forEach(id => {
                if (pendingConfigNodeIds.has(id) && !handledConfigNodes.has(id)) {
                    // This reference is for a node we know is in this import, but
                    // it isn't added yet - flag as pending
                    hasPending = true
                }
            })
            if (!hasPending) {
                // This node has no pending config node references - safe to add
                delete node._configNodeReferences
                new_nodes.push(node)
                handledConfigNodes.add(node.id)
            } else {
                // This node has pending config node references
                // Put to the back of the queue
                pendingConfigNodes.push(node)
            }
            iterationLimit--
        }
        if (pendingConfigNodes.length > 0) {
            // We exceeded the iteration count. Could be due to reference loops
            // between the config nodes. At this point, just add the remaining
            // nodes as-is
            pendingConfigNodes.forEach(node => {
                delete node._configNodeReferences
                new_nodes.push(node)
            })
        }

        // Find regular flow nodes and subflow instances
        for (i=0;i<newNodes.length;i++) {
            n = newNodes[i];
            // TODO: remove workspace in next release+1
            if (n.type !== "workspace" && n.type !== "tab" && n.type !== "subflow") {
                def = registry.getNodeType(n.type);
                if (!def || def.category != "config") {
                    var node = {
                        x:parseFloat(n.x || 0),
                        y:parseFloat(n.y || 0),
                        z:n.z,
                        type: n.type,
                        info: n.info,
                        changed:false,
                        _config:{}
                    }
                    if (n.type !== "group" && n.type !== 'junction') {
                        node.wires = n.wires||[];
                        node.inputLabels = n.inputLabels;
                        node.outputLabels = n.outputLabels;
                        node.icon = n.icon;
                    }
                    if (n.type === 'junction') {
                        node.wires = n.wires||[];
                    }
                    if (n.hasOwnProperty('l')) {
                        node.l = n.l;
                    }
                    if (n.hasOwnProperty('d')) {
                        node.d = n.d;
                    }
                    if (n.hasOwnProperty('g')) {
                        node.g = n.g;
                    }
                    if (options.markChanged) {
                        node.changed = true
                    }
                    if (createNewIds || options.importMap[n.id] === "copy") {
                        if (subflow_denylist[n.z]) {
                            continue;
                        } else if (subflow_map[node.z]) {
                            node.z = subflow_map[node.z].id;
                        } else {
                            node.z = workspace_map[node.z];
                            if (!workspaces[node.z]) {
                                if (createMissingWorkspace) {
                                    if (missingWorkspace === null) {
                                        missingWorkspace = RED.workspaces.add(null,true);
                                        new_workspaces.push(missingWorkspace);
                                    }
                                    node.z = missingWorkspace.id;
                                } else {
                                    node.z = activeWorkspace;
                                }
                            }
                        }
                        node.id = getID();
                    } else {
                        node.id = n.id;
                        const keepNodesCurrentZ = reimport && node.z && (RED.workspaces.contains(node.z) || RED.nodes.subflow(node.z))
                        if (!keepNodesCurrentZ && (node.z == null || (!workspace_map[node.z] && !subflow_map[node.z]))) {
                            if (createMissingWorkspace) {
                                if (missingWorkspace === null) {
                                    missingWorkspace = RED.workspaces.add(null,true);
                                    new_workspaces.push(missingWorkspace);
                                }
                                node.z = missingWorkspace.id;
                            } else {
                                node.z = activeWorkspace;
                            }
                        }
                    }
                    node._def = def;
                    if (node.type === "group") {
                        node._def = RED.group.def;
                        for (d in node._def.defaults) {
                            if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
                                node[d] = n[d];
                                node._config[d] = JSON.stringify(n[d]);
                            }
                        }
                        node._config.x = node.x;
                        node._config.y = node.y;
                        if (n.hasOwnProperty('w')) {
                            node.w = n.w
                        }
                        if (n.hasOwnProperty('h')) {
                            node.h = n.h
                        }
                    } else if (n.type.substring(0,7) === "subflow") {
                        var parentId = n.type.split(":")[1];
                        var subflow = subflow_denylist[parentId]||subflow_map[parentId]||getSubflow(parentId);
                        if (!subflow){
                            node._def = {
                                color:"#fee",
                                defaults: {},
                                label: "unknown: "+n.type,
                                labelStyle: "red-ui-flow-node-label-italic",
                                outputs: n.outputs|| (n.wires && n.wires.length) || 0,
                                set: registry.getNodeSet("node-red/unknown")
                            }
                            var orig = {};
                            for (var p in n) {
                                if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") {
                                    orig[p] = n[p];
                                }
                            }
                            node._orig = orig;
                            node.name = n.type;
                            node.type = "unknown";
                        } else {
                            if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") {
                                parentId = subflow.id;
                                node.type = "subflow:"+parentId;
                                node._def = registry.getNodeType(node.type);
                                delete node.i;
                            }
                            node.name = n.name;
                            node.outputs = subflow.out.length;
                            node.inputs = subflow.in.length;
                            node.env = n.env;
                        }
                    } else if (n.type === 'junction') {
                         node._def = {defaults:{}}
                         node._config.x = node.x
                         node._config.y = node.y
                         node.inputs = 1
                         node.outputs = 1
                         node.w = 0;
                         node.h = 0;

                    } else {
                        if (!node._def) {
                            if (node.x && node.y) {
                                node._def = {
                                    color:"#fee",
                                    defaults: {},
                                    label: "unknown: "+n.type,
                                    labelStyle: "red-ui-flow-node-label-italic",
                                    outputs: n.outputs|| (n.wires && n.wires.length) || 0,
                                    set: registry.getNodeSet("node-red/unknown")
                                }
                            } else {
                                node._def = {
                                    category:"config",
                                    set: registry.getNodeSet("node-red/unknown")
                                };
                                node.users = [];
                                // This is a config node, so delete the default
                                // non-config node properties
                                delete node.x;
                                delete node.y;
                                delete node.wires;
                                delete node.inputLabels;
                                delete node.outputLabels;
                                if (!n.z) {
                                    delete node.z;
                                }
                            }
                            var orig = {};
                            for (var p in n) {
                                if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") {
                                    orig[p] = n[p];
                                }
                            }
                            node._orig = orig;
                            node.name = n.type;
                            node.type = "unknown";
                        }
                        if (node._def.category != "config") {
                            if (n.hasOwnProperty('inputs') && node._def.defaults.hasOwnProperty("inputs")) {
                                node.inputs = parseInt(n.inputs, 10);
                                node._config.inputs = JSON.stringify(n.inputs);
                            } else {
                                node.inputs = node._def.inputs;
                            }
                            if (n.hasOwnProperty('outputs') && node._def.defaults.hasOwnProperty("outputs")) {
                                node.outputs = parseInt(n.outputs, 10);
                                node._config.outputs = JSON.stringify(n.outputs);
                            } else {
                                node.outputs = node._def.outputs;
                            }

                            // The node declares outputs in its defaults, but has not got a valid value
                            // Defer to the length of the wires array
                            if (node.hasOwnProperty('wires')) {
                                if (isNaN(node.outputs)) {
                                    node.outputs = node.wires.length;
                                } else if (node.wires.length > node.outputs) {
                                    // If 'wires' is longer than outputs, clip wires
                                    console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs);
                                    node.wires = node.wires.slice(0, node.outputs);
                                }
                            }

                            for (d in node._def.defaults) {
                                if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
                                    node[d] = n[d];
                                    node._config[d] = JSON.stringify(n[d]);
                                }
                            }
                            node._config.x = node.x;
                            node._config.y = node.y;
                            if (node._def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) {
                                node.credentials = {};
                                for (d in node._def.credentials) {
                                    if (node._def.credentials.hasOwnProperty(d) && n.credentials.hasOwnProperty(d)) {
                                        node.credentials[d] = n.credentials[d];
                                    }
                                }
                            }
                        }
                    }
                    node_map[n.id] = node;
                    // If an 'unknown' config node, it will not have been caught by the
                    // proper config node handling, so needs adding to new_nodes here
                    if (node.type === 'junction') {
                        new_junctions.push(node)
                    } else if (node.type === "unknown" || node._def.category !== "config") {
                        new_nodes.push(node);
                    } else if (node.type === "group") {
                        new_groups.push(node);
                        new_group_set.add(node.id);
                    }
                }
            }
        }

        // Remap all wires and config node references
        for (i=0;i<new_nodes.length+new_junctions.length;i++) {
            if (i<new_nodes.length) {
                n = new_nodes[i];
            } else {
                n = new_junctions[i - new_nodes.length]
            }
            if (n.wires) {
                for (var w1=0;w1<n.wires.length;w1++) {
                    var wires = (Array.isArray(n.wires[w1]))?n.wires[w1]:[n.wires[w1]];
                    for (var w2=0;w2<wires.length;w2++) {
                        if (node_map.hasOwnProperty(wires[w2])) {
                            if (n.z === node_map[wires[w2]].z) {
                                var link = {source:n,sourcePort:w1,target:node_map[wires[w2]]};
                                addLink(link);
                                new_links.push(link);
                            } else {
                                console.log("Warning: dropping link that crosses tabs:",n.id,"->",node_map[wires[w2]].id);
                            }
                        }
                    }
                }
                delete n.wires;
            }
            if (n.g && node_map[n.g]) {
                n.g = node_map[n.g].id;
            } else {
                delete n.g
            }
            // If importing a link node, ensure both ends of each link are either:
            // - not in a subflow
            // - both in the same subflow (not for link call node)
            if (/^link /.test(n.type) && n.links) {
                n.links = n.links.filter(function(id) {
                    const otherNode = node_map[id] || RED.nodes.node(id);
                    if (!otherNode) {
                        // Cannot find other end - remove the link
                        return false
                    }
                    if (otherNode.z === n.z) {
                        // Both ends in the same flow/subflow
                        return true
                    } else if (n.type === "link call" && !getSubflow(otherNode.z)) {
                        // Link call node can call out of a subflow as long as otherNode is
                        // not in a subflow
                        return true
                    } else if (!!getSubflow(n.z) || !!getSubflow(otherNode.z)) {
                        // One end is in a subflow - remove the link
                        return false
                    }
                    return true
                });
            }
            for (var d3 in n._def.defaults) {
                if (n._def.defaults.hasOwnProperty(d3)) {
                    if (n._def.defaults[d3].type) {
                        var nodeList = n[d3];
                        if (!Array.isArray(nodeList)) {
                            nodeList = [nodeList];
                        }
                        nodeList = nodeList.map(function(id) {
                            var node = node_map[id];
                            if (node) {
                                return node.id;
                            }
                            return id;
                        })
                        n[d3] = Array.isArray(n[d3])?nodeList:nodeList[0];
                    }
                }
            }
        }
        for (i=0;i<new_subflows.length;i++) {
            n = new_subflows[i];
            n.in.forEach(function(input) {
                input.wires.forEach(function(wire) {
                    if (node_map.hasOwnProperty(wire.id)) {
                        var link = {source:input, sourcePort:0, target:node_map[wire.id]};
                        addLink(link);
                        new_links.push(link);
                    }
                });
                delete input.wires;
            });
            n.out.forEach(function(output) {
                output.wires.forEach(function(wire) {
                    var link;
                    if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
                        link = {source:n.in[wire.port], sourcePort:wire.port,target:output};
                    } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
                        link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output};
                    }
                    if (link) {
                        addLink(link);
                        new_links.push(link);
                    }
                });
                delete output.wires;
            });
            if (n.status) {
                n.status.wires.forEach(function(wire) {
                    var link;
                    if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
                        link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status};
                    } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
                        link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status};
                    }
                    if (link) {
                        addLink(link);
                        new_links.push(link);
                    }
                });
                delete n.status.wires;
            }
        }
        // Order the groups to ensure they are outer-most to inner-most
        var groupDepthMap = {};
        for (i=0;i<new_groups.length;i++) {
            n = new_groups[i];

            if (n.g && !new_group_set.has(n.g)) {
                delete n.g;
            }
            if (!n.g) {
                groupDepthMap[n.id] = 0;
            }
        }
        var changedDepth;
        do {
            changedDepth = false;
            for (i=0;i<new_groups.length;i++) {
                n = new_groups[i];
                if (n.g) {
                    if (groupDepthMap[n.id] !== groupDepthMap[n.g] + 1) {
                        groupDepthMap[n.id] = groupDepthMap[n.g] + 1;
                        changedDepth = true;
                    }
                }
            }
        } while(changedDepth);

        new_groups.sort(function(A,B) {
            return groupDepthMap[A.id] - groupDepthMap[B.id];
        });
        for (i=0;i<new_groups.length;i++) {
            new_groups[i] = addGroup(new_groups[i]);
            node_map[new_groups[i].id] = new_groups[i]
        }

        for (i=0;i<new_junctions.length;i++) {
            new_junctions[i] = addJunction(new_junctions[i]);
            node_map[new_junctions[i].id] = new_junctions[i]
        }


        // Now the nodes have been fully updated, add them.
        for (i=0;i<new_nodes.length;i++) {
            new_nodes[i] = addNode(new_nodes[i])
            node_map[new_nodes[i].id] = new_nodes[i]
        }

        // Finally validate them all.
        // This has to be done after everything is added so that any checks for
        // dependent config nodes will pass
        for (i=0;i<new_nodes.length;i++) {
            var node = new_nodes[i];
            RED.editor.validateNode(node);
        }
        const lookupNode = (id) => {
            const mappedNode = node_map[id]
            if (!mappedNode) {
                return null
            }
            if (mappedNode.__isProxy__) {
                return mappedNode
            } else {
                return node_map[mappedNode.id]
            }
        }
        // Update groups to reference proxy node objects
        for (i=0;i<new_groups.length;i++) {
            n = new_groups[i];
            // bypass the proxy in case the flow is locked
            n.__node__.nodes = n.nodes.map(lookupNode)
            // Just in case the group references a node that doesn't exist for some reason
            n.__node__.nodes = n.nodes.filter(function(v) {
                if (v) {
                    // Repair any nodes that have forgotten they are in this group
                    if (v.g !== n.id) {
                        v.g = n.id;
                    }
                }
                return !!v
            });
        }

        // Update links to use proxy node objects
        for (i=0;i<new_links.length;i++) {
            new_links[i].source = lookupNode(new_links[i].source.id) || new_links[i].source
            new_links[i].target = lookupNode(new_links[i].target.id) || new_links[i].target
        }

        RED.workspaces.refresh();


        if (recoveryWorkspace) {
            var notification = RED.notify(RED._("clipboard.recoveredNodesNotification",{flowName:RED._("clipboard.recoveredNodes")}),{
                type:"warning",
                fixed:true,
                buttons: [
                    {text: RED._("common.label.close"), click: function() { notification.close() }}
                ]
            });
        }

        return {
            nodes:new_nodes,
            links:new_links,
            groups:new_groups,
            junctions: new_junctions,
            workspaces:new_workspaces,
            subflows:new_subflows,
            missingWorkspace: missingWorkspace,
            removedNodes: removedNodes
        }
    }

    // TODO: supports filter.z|type
    function filterNodes(filter) {
        return allNodes.filterNodes(filter);
    }

    function filterLinks(filter) {
        var result = [];
        var candidateLinks = [];
        var hasCandidates = false;
        var filterSZ = filter.source && filter.source.z;
        var filterTZ = filter.target && filter.target.z;
        var filterZ;
        if (filterSZ || filterTZ) {
            if (filterSZ === filterTZ) {
                filterZ = filterSZ;
            } else {
                filterZ = (filterSZ === undefined)?filterTZ:filterSZ
            }
        }
        if (filterZ) {
            candidateLinks = linkTabMap[filterZ] || [];
            hasCandidates = true;
        } else if (filter.source && filter.source.hasOwnProperty("id")) {
            if (nodeLinks[filter.source.id]) {
                hasCandidates = true;
                candidateLinks = candidateLinks.concat(nodeLinks[filter.source.id].out)
            }
        } else if (filter.target && filter.target.hasOwnProperty("id")) {
            if (nodeLinks[filter.target.id]) {
                hasCandidates = true;
                candidateLinks = candidateLinks.concat(nodeLinks[filter.target.id].in)
            }
        }
        if (!hasCandidates) {
            candidateLinks = links;
        }
        for (var n=0;n<candidateLinks.length;n++) {
            var link = candidateLinks[n];
            if (filter.source) {
                if (filter.source.hasOwnProperty("id") && link.source.id !== filter.source.id) {
                    continue;
                }
                if (filter.source.hasOwnProperty("z") && link.source.z !== filter.source.z) {
                    continue;
                }
            }
            if (filter.target) {
                if (filter.target.hasOwnProperty("id") && link.target.id !== filter.target.id) {
                    continue;
                }
                if (filter.target.hasOwnProperty("z") && link.target.z !== filter.target.z) {
                    continue;
                }
            }
            if (filter.hasOwnProperty("sourcePort") && link.sourcePort !== filter.sourcePort) {
                continue;
            }
            result.push(link);
        }
        return result;
    }

    /**
     * Update any config nodes referenced by the provided node to ensure
     * their 'users' list is correct.
     *
     * @param {object} node The node in which to check if it contains references
     * @param {object} options Options to apply.
     * @param {"add" | "remove"} [options.action] Add or remove the node from
     * the Config Node users list. Default `add`.
     * @param {boolean} [options.emitEvent] Emit the `nodes:changes` event.
     * Default true.
     */
    function updateConfigNodeUsers(node, options) {
        const defaultOptions = { action: "add", emitEvent: true };
        options = Object.assign({}, defaultOptions, options);

        for (var d in node._def.defaults) {
            if (node._def.defaults.hasOwnProperty(d)) {
                var property = node._def.defaults[d];
                if (property.type) {
                    var type = registry.getNodeType(property.type);
                    // Need to ensure the type is a config node to not treat links nodes
                    if (type && type.category == "config") {
                        var configNode = configNodes[node[d]];
                        if (configNode) {
                            if (options.action === "add") {
                                if (configNode.users.indexOf(node) === -1) {
                                    configNode.users.push(node);
                                    if (options.emitEvent) {
                                        RED.events.emit('nodes:change', configNode);
                                    }
                                }
                            } else if (options.action === "remove") {
                                if (configNode.users.indexOf(node) !== -1) {
                                    const users = configNode.users;
                                    users.splice(users.indexOf(node), 1);
                                    if (options.emitEvent) {
                                        RED.events.emit('nodes:change', configNode);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        // Subflows can have config node env
        if (node.type.indexOf("subflow:") === 0) {
            node.env?.forEach((prop) => {
                if (prop.type === "conf-type" && prop.value) {
                    // Add the node to the config node users
                    const configNode = getNode(prop.value);
                    if (configNode) {
                        if (options.action === "add") {
                            if (configNode.users.indexOf(node) === -1) {
                                configNode.users.push(node);
                                if (options.emitEvent) {
                                    RED.events.emit('nodes:change', configNode);
                                }
                            }
                        } else if (options.action === "remove") {
                            if (configNode.users.indexOf(node) !== -1) {
                                const users = configNode.users;
                                users.splice(users.indexOf(node), 1);
                                if (options.emitEvent) {
                                    RED.events.emit('nodes:change', configNode);
                                }
                            }
                        }
                    }
                }
            });
        }
    }

    function flowVersion(version) {
        if (version !== undefined) {
            loadedFlowVersion = version;
        } else {
            return loadedFlowVersion;
        }
    }

    function clear() {
        links = [];
        linkTabMap = {};
        nodeLinks = {};
        configNodes = {};
        workspacesOrder = [];
        groups = {};
        groupsByZ = {};
        junctions = {};
        junctionsByZ = {};

        var workspaceIds = Object.keys(workspaces);
        // Ensure all workspaces are unlocked so we don't get any edit-protection
        // preventing removal
        workspaceIds.forEach(function(id) {
            workspaces[id].locked = false
        });

        var subflowIds = Object.keys(subflows);
        subflowIds.forEach(function(id) {
            RED.subflow.removeSubflow(id)
        });
        workspaceIds.forEach(function(id) {
            RED.workspaces.remove(workspaces[id]);
        });
        defaultWorkspace = null;
        initialLoad = null;
        workspaces = {};

        allNodes.clear();

        RED.nodes.dirty(false);
        RED.view.redraw(true, true);
        RED.palette.refresh();
        RED.workspaces.refresh();
        RED.sidebar.config.refresh();
        RED.sidebar.info.refresh();

        RED.events.emit("workspace:clear");
    }

    function addGroup(group) {
        if (!group.__isProxy__) {
            group = new Proxy(group, nodeProxyHandler)
        }
        groupsByZ[group.z] = groupsByZ[group.z] || [];
        groupsByZ[group.z].push(group);
        groups[group.id] = group;
        allNodes.addObjectToWorkspace(group.z, group.id, group.changed || group.moved)
        RED.events.emit("groups:add",group);
        return group
    }
    function removeGroup(group) {
        var i = groupsByZ[group.z].indexOf(group);
        groupsByZ[group.z].splice(i,1);
        if (groupsByZ[group.z].length === 0) {
            delete groupsByZ[group.z];
        }
        if (group.g) {
            if (groups[group.g]) {
                var index = groups[group.g].nodes.indexOf(group);
                groups[group.g].nodes.splice(index,1);
            }
        }
        RED.group.markDirty(group);
        allNodes.removeObjectFromWorkspace(group.z, group.id)
        delete groups[group.id];
        RED.events.emit("groups:remove",group);
    }
    function getGroupOrder(z) {
        const groups = groupsByZ[z]
        return groups.map(g => g.id)
    }

    function addJunction(junction) {
        if (!junction.__isProxy__) {
            junction = new Proxy(junction, nodeProxyHandler)
        }
        junctionsByZ[junction.z] = junctionsByZ[junction.z] || []
        junctionsByZ[junction.z].push(junction)
        junctions[junction.id] = junction;
        if (!nodeLinks[junction.id]) {
            nodeLinks[junction.id] = {in:[],out:[]};
        }
        allNodes.addObjectToWorkspace(junction.z, junction.id, junction.changed || junction.moved)
        RED.events.emit("junctions:add", junction)
        return junction
    }
    function removeJunction(junction) {
        var i = junctionsByZ[junction.z].indexOf(junction)
        junctionsByZ[junction.z].splice(i, 1)
        if (junctionsByZ[junction.z].length === 0) {
            delete junctionsByZ[junction.z]
        }
        delete junctions[junction.id]
        delete nodeLinks[junction.id];
        allNodes.removeObjectFromWorkspace(junction.z, junction.id)
        RED.events.emit("junctions:remove", junction)

        var removedLinks = links.filter(function(l) { return (l.source === junction) || (l.target === junction); });
        removedLinks.forEach(removeLink);
        return { links: removedLinks }
    }

    function getNodeHelp(type) {
        var helpContent = "";
        var helpElement = $("script[data-help-name='"+type+"']");
        if (helpElement) {
            helpContent = helpElement.html();
            var helpType = helpElement.attr("type");
            if (helpType === "text/markdown") {
                helpContent = RED.utils.renderMarkdown(helpContent);
            }
        }
        return helpContent;
    }

    function getNodeIslands(nodes) {
        var selectedNodes = new Set(nodes);
        // Maps node => island index
        var nodeToIslandIndex = new Map();
        // Maps island index => [nodes in island]
        var islandIndexToNodes = new Map();
        var internalLinks = new Set();
        nodes.forEach((node, index) => {
            nodeToIslandIndex.set(node,index);
            islandIndexToNodes.set(index, [node]);
            var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT);
            var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT);
            inboundLinks.forEach(l => {
                if (selectedNodes.has(l.source)) {
                    internalLinks.add(l)
                }
            })
            outboundLinks.forEach(l => {
                if (selectedNodes.has(l.target)) {
                    internalLinks.add(l)
                }
            })
        })

        internalLinks.forEach(l => {
            let source = l.source;
            let target = l.target;
            if (nodeToIslandIndex.get(source) !== nodeToIslandIndex.get(target)) {
                let sourceIsland = nodeToIslandIndex.get(source);
                let islandToMove = nodeToIslandIndex.get(target);
                let nodesToMove = islandIndexToNodes.get(islandToMove);
                nodesToMove.forEach(n => {
                    nodeToIslandIndex.set(n,sourceIsland);
                    islandIndexToNodes.get(sourceIsland).push(n);
                })
                islandIndexToNodes.delete(islandToMove);
            }
        })
        const result = [];
        islandIndexToNodes.forEach((nodes,index) => {
            result.push(nodes);
        })
        return result;
    }

    function detachNodes(nodes) {
        let allSelectedNodes = [];
        nodes.forEach(node => {
            if (node.type === 'group') {
                let groupNodes = RED.group.getNodes(node,true,true);
                allSelectedNodes = allSelectedNodes.concat(groupNodes);
            } else {
                allSelectedNodes.push(node);
            }
        })
        if (allSelectedNodes.length > 0 ) {
            const nodeIslands = RED.nodes.getNodeIslands(allSelectedNodes);
            let removedLinks = [];
            let newLinks = [];
            let createdLinkIds = new Set();

            nodeIslands.forEach(nodes => {
                let selectedNodes = new Set(nodes);
                let allInboundLinks = [];
                let allOutboundLinks = [];
                // Identify links that enter or exit this island of nodes
                nodes.forEach(node => {
                    var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT);
                    var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT);
                    inboundLinks.forEach(l => {
                        if (!selectedNodes.has(l.source)) {
                            allInboundLinks.push(l)
                        }
                    })
                    outboundLinks.forEach(l => {
                        if (!selectedNodes.has(l.target)) {
                            allOutboundLinks.push(l)
                        }
                    })
                });


                // Identify the links to restore
                allInboundLinks.forEach(inLink => {
                    // For Each inbound link,
                    //  - get source node.
                    //  - trace through to all outbound links
                    let sourceNode = inLink.source;
                    let targetNodes = new Set();
                    let visited = new Set();
                    let stack = [inLink.target];
                    while (stack.length > 0) {
                        let node = stack.pop(stack);
                        visited.add(node)
                        let links = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT);
                        links.forEach(l => {
                            if (visited.has(l.target)) {
                                return
                            }
                            visited.add(l.target);
                            if (selectedNodes.has(l.target)) {
                                // internal link
                                stack.push(l.target)
                            } else {
                                targetNodes.add(l.target)
                            }
                        })
                    }
                    targetNodes.forEach(target => {
                        let linkId = `${sourceNode.id}[${inLink.sourcePort}] -> ${target.id}`
                        if (!createdLinkIds.has(linkId)) {
                            createdLinkIds.add(linkId);
                            let link = {
                                source: sourceNode,
                                sourcePort: inLink.sourcePort,
                                target: target
                            }
                            let existingLinks = RED.nodes.filterLinks(link)
                            if (existingLinks.length === 0) {
                                newLinks.push(link);
                            }
                        }
                    })
                })

                // 2. delete all those links
                allInboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)})
                allOutboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)})
            })

            newLinks.forEach(l => RED.nodes.addLink(l));
            return {
                newLinks,
                removedLinks
            }
        }
    }

    return {
        init: function() {
            RED.events.on("registry:node-type-added",function(type) {
                var def = registry.getNodeType(type);
                var replaced = false;
                var replaceNodes = {};
                RED.nodes.eachNode(function(n) {
                    if (n.type === "unknown" && n.name === type) {
                        replaceNodes[n.id] = n;
                    }
                });
                RED.nodes.eachConfig(function(n) {
                    if (n.type === "unknown" && n.name === type) {
                        replaceNodes[n.id] = n;
                    }
                });

                const nodeGroupMap = {}
                var replaceNodeIds = Object.keys(replaceNodes);
                if (replaceNodeIds.length > 0) {
                    var reimportList = [];
                    replaceNodeIds.forEach(function(id) {
                        var n = replaceNodes[id];
                        if (configNodes.hasOwnProperty(n.id)) {
                            delete configNodes[n.id];
                        } else {
                            allNodes.removeNode(n);
                        }
                        if (n.g) {
                            // reimporting a node *without* including its group object
                            // will cause the g property to be cleared. Cache it
                            // here so we can restore it
                            nodeGroupMap[n.id] = n.g
                        }
                        reimportList.push(convertNode(n));
                        RED.events.emit('nodes:remove',n);
                    });

                    // Remove any links between nodes that are going to be reimported.
                    // This prevents a duplicate link from being added.
                    var removeLinks = [];
                    RED.nodes.eachLink(function(l) {
                        if (replaceNodes.hasOwnProperty(l.source.id) && replaceNodes.hasOwnProperty(l.target.id)) {
                            removeLinks.push(l);
                        }
                    });
                    removeLinks.forEach(removeLink);

                    // Force the redraw to be synchronous so the view updates
                    // *now* and removes the unknown node
                    RED.view.redraw(true, true);
                    var result = importNodes(reimportList,{generateIds:false, reimport: true});
                    var newNodeMap = {};
                    result.nodes.forEach(function(n) {
                        newNodeMap[n.id] = n;
                        if (nodeGroupMap[n.id]) {
                            // This node is in a group - need to substitute the
                            // node reference inside the group
                            n.g = nodeGroupMap[n.id]
                            const group = RED.nodes.group(n.g)
                            if (group) {
                                var index = group.nodes.findIndex(gn => gn.id === n.id)
                                if (index > -1) {
                                    group.nodes[index] = n
                                }
                            }
                        }
                    });
                    RED.nodes.eachLink(function(l) {
                        if (newNodeMap.hasOwnProperty(l.source.id)) {
                            l.source = newNodeMap[l.source.id];
                        }
                        if (newNodeMap.hasOwnProperty(l.target.id)) {
                            l.target = newNodeMap[l.target.id];
                        }
                    });
                    RED.view.redraw(true);
                }
            });
            RED.events.on('deploy', function () {
                allNodes.clearState()
            })
        },
        registry:registry,
        setNodeList: registry.setNodeList,

        getNodeSet: registry.getNodeSet,
        addNodeSet: registry.addNodeSet,
        removeNodeSet: registry.removeNodeSet,
        enableNodeSet: registry.enableNodeSet,
        disableNodeSet: registry.disableNodeSet,

        setIconSets: registry.setIconSets,
        getIconSets: registry.getIconSets,

        registerType: registry.registerNodeType,
        getType: registry.getNodeType,
        getNodeHelp: getNodeHelp,
        convertNode: convertNode,
        add: addNode,
        remove: removeNode,
        clear: clear,
        detachNodes: detachNodes,
        moveNodesForwards: moveNodesForwards,
        moveNodesBackwards: moveNodesBackwards,
        moveNodesToFront: moveNodesToFront,
        moveNodesToBack: moveNodesToBack,
        getNodeOrder: getNodeOrder,
        setNodeOrder: setNodeOrder,

        moveNodeToTab: moveNodeToTab,

        addLink: addLink,
        removeLink: removeLink,
        getNodeLinks: function(id, portType) {
            if (typeof id !== 'string') {
                id = id.id;
            }
            if (nodeLinks[id]) {
                if (portType === 1) {
                    // Return cloned arrays so they can be safely modified by caller
                    return [].concat(nodeLinks[id].in)
                } else {
                    return [].concat(nodeLinks[id].out)
                }
            }
            return [];
        },
        addWorkspace: addWorkspace,
        removeWorkspace: removeWorkspace,
        getWorkspaceOrder: function() { return [...workspacesOrder] },
        setWorkspaceOrder: function(order) { workspacesOrder = order; },
        workspace: getWorkspace,

        addSubflow: addSubflow,
        removeSubflow: removeSubflow,
        subflow: getSubflow,
        subflowContains: subflowContains,

        addGroup: addGroup,
        removeGroup: removeGroup,
        group: function(id) { return groups[id] },
        groups: function(z) { return groupsByZ[z]?groupsByZ[z].slice():[] },

        addJunction: addJunction,
        removeJunction: removeJunction,
        junction: function(id) { return junctions[id] },
        junctions: function(z) { return junctionsByZ[z]?junctionsByZ[z].slice():[] },

        eachNode: function(cb) {
            allNodes.eachNode(cb);
        },
        eachLink: function(cb) {
            for (var l=0;l<links.length;l++) {
                if (cb(links[l]) === false) {
                    break;
                }
            }
        },
        eachConfig: function(cb) {
            for (var id in configNodes) {
                if (configNodes.hasOwnProperty(id)) {
                    if (cb(configNodes[id]) === false) {
                        break;
                    }
                }
            }
        },
        eachSubflow: function(cb) {
            for (var id in subflows) {
                if (subflows.hasOwnProperty(id)) {
                    if (cb(subflows[id]) === false) {
                        break;
                    }
                }
            }
        },
        eachWorkspace: function(cb) {
            for (var i=0;i<workspacesOrder.length;i++) {
                if (cb(workspaces[workspacesOrder[i]]) === false) {
                    break;
                }
            }
        },
        eachGroup: function(cb) {
            for (var group of Object.values(groups)) {
                if (cb(group) === false) {
                    break
                }
            }
        },
        eachJunction: function(cb) {
            for (var junction of Object.values(junctions)) {
                if (cb(junction) === false) {
                    break
                }
            }
        },

        node: getNode,

        version: flowVersion,
        originalFlow: function(flow) {
            if (flow === undefined) {
                return initialLoad;
            } else {
                initialLoad = flow;
            }
        },

        filterNodes: filterNodes,
        filterLinks: filterLinks,

        import: importNodes,

        identifyImportConflicts: identifyImportConflicts,

        getAllFlowNodes: getAllFlowNodes,
        getAllUpstreamNodes: getAllUpstreamNodes,
        getAllDownstreamNodes: getAllDownstreamNodes,
        getDownstreamNodes: getDownstreamNodes,
        getNodeIslands: getNodeIslands,
        createExportableNodeSet: createExportableNodeSet,
        createCompleteNodeSet: createCompleteNodeSet,
        updateConfigNodeUsers: updateConfigNodeUsers,
        id: getID,
        dirty: function(d) {
            if (d == null) {
                return dirty;
            } else {
                setDirty(d);
            }
        }
    };
})();
;/**
 * 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.
 **/

RED.nodes.fontAwesome = (function() {

    var iconMap = {
        "fa-address-book-o": "\uf2ba",
        "fa-address-book": "\uf2b9",
        "fa-address-card-o": "\uf2bc",
        "fa-address-card": "\uf2bb",
        "fa-adjust": "\uf042",
        "fa-align-center": "\uf037",
        "fa-align-justify": "\uf039",
        "fa-align-left": "\uf036",
        "fa-align-right": "\uf038",
        "fa-ambulance": "\uf0f9",
        "fa-american-sign-language-interpreting": "\uf2a3",
        "fa-anchor": "\uf13d",
        "fa-angle-double-down": "\uf103",
        "fa-angle-double-left": "\uf100",
        "fa-angle-double-right": "\uf101",
        "fa-angle-double-up": "\uf102",
        "fa-angle-down": "\uf107",
        "fa-angle-left": "\uf104",
        "fa-angle-right": "\uf105",
        "fa-angle-up": "\uf106",
        "fa-archive": "\uf187",
        "fa-area-chart": "\uf1fe",
        "fa-arrow-circle-down": "\uf0ab",
        "fa-arrow-circle-left": "\uf0a8",
        "fa-arrow-circle-o-down": "\uf01a",
        "fa-arrow-circle-o-left": "\uf190",
        "fa-arrow-circle-o-right": "\uf18e",
        "fa-arrow-circle-o-up": "\uf01b",
        "fa-arrow-circle-right": "\uf0a9",
        "fa-arrow-circle-up": "\uf0aa",
        "fa-arrow-down": "\uf063",
        "fa-arrow-left": "\uf060",
        "fa-arrow-right": "\uf061",
        "fa-arrow-up": "\uf062",
        "fa-arrows-alt": "\uf0b2",
        "fa-arrows-h": "\uf07e",
        "fa-arrows-v": "\uf07d",
        "fa-arrows": "\uf047",
        "fa-asl-interpreting": "\uf2a3",
        "fa-assistive-listening-systems": "\uf2a2",
        "fa-asterisk": "\uf069",
        "fa-at": "\uf1fa",
        "fa-audio-description": "\uf29e",
        "fa-automobile": "\uf1b9",
        "fa-backward": "\uf04a",
        "fa-balance-scale": "\uf24e",
        "fa-ban": "\uf05e",
        "fa-bank": "\uf19c",
        "fa-bar-chart-o": "\uf080",
        "fa-bar-chart": "\uf080",
        "fa-barcode": "\uf02a",
        "fa-bars": "\uf0c9",
        "fa-bath": "\uf2cd",
        "fa-bathtub": "\uf2cd",
        "fa-battery-0": "\uf244",
        "fa-battery-1": "\uf243",
        "fa-battery-2": "\uf242",
        "fa-battery-3": "\uf241",
        "fa-battery-4": "\uf240",
        "fa-battery-empty": "\uf244",
        "fa-battery-full": "\uf240",
        "fa-battery-half": "\uf242",
        "fa-battery-quarter": "\uf243",
        "fa-battery-three-quarters": "\uf241",
        "fa-battery": "\uf240",
        "fa-bed": "\uf236",
        "fa-beer": "\uf0fc",
        "fa-bell-o": "\uf0a2",
        "fa-bell-slash-o": "\uf1f7",
        "fa-bell-slash": "\uf1f6",
        "fa-bell": "\uf0f3",
        "fa-bicycle": "\uf206",
        "fa-binoculars": "\uf1e5",
        "fa-birthday-cake": "\uf1fd",
        "fa-blind": "\uf29d",
        "fa-bold": "\uf032",
        "fa-bolt": "\uf0e7",
        "fa-bomb": "\uf1e2",
        "fa-book": "\uf02d",
        "fa-bookmark-o": "\uf097",
        "fa-bookmark": "\uf02e",
        "fa-braille": "\uf2a1",
        "fa-briefcase": "\uf0b1",
        "fa-bug": "\uf188",
        "fa-building-o": "\uf0f7",
        "fa-building": "\uf1ad",
        "fa-bullhorn": "\uf0a1",
        "fa-bullseye": "\uf140",
        "fa-bus": "\uf207",
        "fa-cab": "\uf1ba",
        "fa-calculator": "\uf1ec",
        "fa-calendar-check-o": "\uf274",
        "fa-calendar-minus-o": "\uf272",
        "fa-calendar-o": "\uf133",
        "fa-calendar-plus-o": "\uf271",
        "fa-calendar-times-o": "\uf273",
        "fa-calendar": "\uf073",
        "fa-camera-retro": "\uf083",
        "fa-camera": "\uf030",
        "fa-car": "\uf1b9",
        "fa-caret-down": "\uf0d7",
        "fa-caret-left": "\uf0d9",
        "fa-caret-right": "\uf0da",
        "fa-caret-square-o-down": "\uf150",
        "fa-caret-square-o-left": "\uf191",
        "fa-caret-square-o-right": "\uf152",
        "fa-caret-square-o-up": "\uf151",
        "fa-caret-up": "\uf0d8",
        "fa-cart-arrow-down": "\uf218",
        "fa-cart-plus": "\uf217",
        "fa-cc": "\uf20a",
        "fa-certificate": "\uf0a3",
        "fa-chain-broken": "\uf127",
        "fa-chain": "\uf0c1",
        "fa-check-circle-o": "\uf05d",
        "fa-check-circle": "\uf058",
        "fa-check-square-o": "\uf046",
        "fa-check-square": "\uf14a",
        "fa-check": "\uf00c",
        "fa-chevron-circle-down": "\uf13a",
        "fa-chevron-circle-left": "\uf137",
        "fa-chevron-circle-right": "\uf138",
        "fa-chevron-circle-up": "\uf139",
        "fa-chevron-down": "\uf078",
        "fa-chevron-left": "\uf053",
        "fa-chevron-right": "\uf054",
        "fa-chevron-up": "\uf077",
        "fa-child": "\uf1ae",
        "fa-circle-o-notch": "\uf1ce",
        "fa-circle-o": "\uf10c",
        "fa-circle-thin": "\uf1db",
        "fa-circle": "\uf111",
        "fa-clipboard": "\uf0ea",
        "fa-clock-o": "\uf017",
        "fa-clone": "\uf24d",
        "fa-close": "\uf00d",
        "fa-cloud-download": "\uf0ed",
        "fa-cloud-upload": "\uf0ee",
        "fa-cloud": "\uf0c2",
        "fa-cny": "\uf157",
        "fa-code-fork": "\uf126",
        "fa-code": "\uf121",
        "fa-coffee": "\uf0f4",
        "fa-cog": "\uf013",
        "fa-cogs": "\uf085",
        "fa-columns": "\uf0db",
        "fa-comment-o": "\uf0e5",
        "fa-comment": "\uf075",
        "fa-commenting-o": "\uf27b",
        "fa-commenting": "\uf27a",
        "fa-comments-o": "\uf0e6",
        "fa-comments": "\uf086",
        "fa-compass": "\uf14e",
        "fa-compress": "\uf066",
        "fa-copy": "\uf0c5",
        "fa-copyright": "\uf1f9",
        "fa-creative-commons": "\uf25e",
        "fa-credit-card-alt": "\uf283",
        "fa-credit-card": "\uf09d",
        "fa-crop": "\uf125",
        "fa-crosshairs": "\uf05b",
        "fa-cube": "\uf1b2",
        "fa-cubes": "\uf1b3",
        "fa-cut": "\uf0c4",
        "fa-cutlery": "\uf0f5",
        "fa-dashboard": "\uf0e4",
        "fa-database": "\uf1c0",
        "fa-deaf": "\uf2a4",
        "fa-deafness": "\uf2a4",
        "fa-dedent": "\uf03b",
        "fa-desktop": "\uf108",
        "fa-diamond": "\uf219",
        "fa-dollar": "\uf155",
        "fa-dot-circle-o": "\uf192",
        "fa-download": "\uf019",
        "fa-drivers-license-o": "\uf2c3",
        "fa-drivers-license": "\uf2c2",
        "fa-edit": "\uf044",
        "fa-eject": "\uf052",
        "fa-ellipsis-h": "\uf141",
        "fa-ellipsis-v": "\uf142",
        "fa-envelope-o": "\uf003",
        "fa-envelope-open-o": "\uf2b7",
        "fa-envelope-open": "\uf2b6",
        "fa-envelope-square": "\uf199",
        "fa-envelope": "\uf0e0",
        "fa-eraser": "\uf12d",
        "fa-eur": "\uf153",
        "fa-euro": "\uf153",
        "fa-exchange": "\uf0ec",
        "fa-exclamation-circle": "\uf06a",
        "fa-exclamation-triangle": "\uf071",
        "fa-exclamation": "\uf12a",
        "fa-expand": "\uf065",
        "fa-external-link-square": "\uf14c",
        "fa-external-link": "\uf08e",
        "fa-eye-slash": "\uf070",
        "fa-eye": "\uf06e",
        "fa-eyedropper": "\uf1fb",
        "fa-fast-backward": "\uf049",
        "fa-fast-forward": "\uf050",
        "fa-fax": "\uf1ac",
        "fa-feed": "\uf09e",
        "fa-female": "\uf182",
        "fa-fighter-jet": "\uf0fb",
        "fa-file-archive-o": "\uf1c6",
        "fa-file-audio-o": "\uf1c7",
        "fa-file-code-o": "\uf1c9",
        "fa-file-excel-o": "\uf1c3",
        "fa-file-image-o": "\uf1c5",
        "fa-file-movie-o": "\uf1c8",
        "fa-file-o": "\uf016",
        "fa-file-pdf-o": "\uf1c1",
        "fa-file-photo-o": "\uf1c5",
        "fa-file-picture-o": "\uf1c5",
        "fa-file-powerpoint-o": "\uf1c4",
        "fa-file-sound-o": "\uf1c7",
        "fa-file-text-o": "\uf0f6",
        "fa-file-text": "\uf15c",
        "fa-file-video-o": "\uf1c8",
        "fa-file-word-o": "\uf1c2",
        "fa-file-zip-o": "\uf1c6",
        "fa-file": "\uf15b",
        "fa-files-o": "\uf0c5",
        "fa-film": "\uf008",
        "fa-filter": "\uf0b0",
        "fa-fire-extinguisher": "\uf134",
        "fa-fire": "\uf06d",
        "fa-flag-checkered": "\uf11e",
        "fa-flag-o": "\uf11d",
        "fa-flag": "\uf024",
        "fa-flash": "\uf0e7",
        "fa-flask": "\uf0c3",
        "fa-floppy-o": "\uf0c7",
        "fa-folder-o": "\uf114",
        "fa-folder-open-o": "\uf115",
        "fa-folder-open": "\uf07c",
        "fa-folder": "\uf07b",
        "fa-font": "\uf031",
        "fa-forward": "\uf04e",
        "fa-frown-o": "\uf119",
        "fa-futbol-o": "\uf1e3",
        "fa-gamepad": "\uf11b",
        "fa-gavel": "\uf0e3",
        "fa-gbp": "\uf154",
        "fa-gear": "\uf013",
        "fa-gears": "\uf085",
        "fa-genderless": "\uf22d",
        "fa-gift": "\uf06b",
        "fa-glass": "\uf000",
        "fa-globe": "\uf0ac",
        "fa-graduation-cap": "\uf19d",
        "fa-group": "\uf0c0",
        "fa-h-square": "\uf0fd",
        "fa-hand-grab-o": "\uf255",
        "fa-hand-lizard-o": "\uf258",
        "fa-hand-o-down": "\uf0a7",
        "fa-hand-o-left": "\uf0a5",
        "fa-hand-o-right": "\uf0a4",
        "fa-hand-o-up": "\uf0a6",
        "fa-hand-paper-o": "\uf256",
        "fa-hand-peace-o": "\uf25b",
        "fa-hand-pointer-o": "\uf25a",
        "fa-hand-rock-o": "\uf255",
        "fa-hand-scissors-o": "\uf257",
        "fa-hand-spock-o": "\uf259",
        "fa-hand-stop-o": "\uf256",
        "fa-handshake-o": "\uf2b5",
        "fa-hard-of-hearing": "\uf2a4",
        "fa-hashtag": "\uf292",
        "fa-hdd-o": "\uf0a0",
        "fa-header": "\uf1dc",
        "fa-headphones": "\uf025",
        "fa-heart-o": "\uf08a",
        "fa-heart": "\uf004",
        "fa-heartbeat": "\uf21e",
        "fa-history": "\uf1da",
        "fa-home": "\uf015",
        "fa-hospital-o": "\uf0f8",
        "fa-hotel": "\uf236",
        "fa-hourglass-1": "\uf251",
        "fa-hourglass-2": "\uf252",
        "fa-hourglass-3": "\uf253",
        "fa-hourglass-end": "\uf253",
        "fa-hourglass-half": "\uf252",
        "fa-hourglass-o": "\uf250",
        "fa-hourglass-start": "\uf251",
        "fa-hourglass": "\uf254",
        "fa-i-cursor": "\uf246",
        "fa-id-badge": "\uf2c1",
        "fa-id-card-o": "\uf2c3",
        "fa-id-card": "\uf2c2",
        "fa-ils": "\uf20b",
        "fa-image": "\uf03e",
        "fa-inbox": "\uf01c",
        "fa-indent": "\uf03c",
        "fa-industry": "\uf275",
        "fa-info-circle": "\uf05a",
        "fa-info": "\uf129",
        "fa-inr": "\uf156",
        "fa-institution": "\uf19c",
        "fa-intersex": "\uf224",
        "fa-italic": "\uf033",
        "fa-jpy": "\uf157",
        "fa-key": "\uf084",
        "fa-keyboard-o": "\uf11c",
        "fa-krw": "\uf159",
        "fa-language": "\uf1ab",
        "fa-laptop": "\uf109",
        "fa-leaf": "\uf06c",
        "fa-legal": "\uf0e3",
        "fa-lemon-o": "\uf094",
        "fa-level-down": "\uf149",
        "fa-level-up": "\uf148",
        "fa-life-bouy": "\uf1cd",
        "fa-life-buoy": "\uf1cd",
        "fa-life-ring": "\uf1cd",
        "fa-life-saver": "\uf1cd",
        "fa-lightbulb-o": "\uf0eb",
        "fa-line-chart": "\uf201",
        "fa-link": "\uf0c1",
        "fa-list-alt": "\uf022",
        "fa-list-ol": "\uf0cb",
        "fa-list-ul": "\uf0ca",
        "fa-list": "\uf03a",
        "fa-location-arrow": "\uf124",
        "fa-lock": "\uf023",
        "fa-long-arrow-down": "\uf175",
        "fa-long-arrow-left": "\uf177",
        "fa-long-arrow-right": "\uf178",
        "fa-long-arrow-up": "\uf176",
        "fa-low-vision": "\uf2a8",
        "fa-magic": "\uf0d0",
        "fa-magnet": "\uf076",
        "fa-mail-forward": "\uf064",
        "fa-mail-reply-all": "\uf122",
        "fa-mail-reply": "\uf112",
        "fa-male": "\uf183",
        "fa-map-marker": "\uf041",
        "fa-map-o": "\uf278",
        "fa-map-pin": "\uf276",
        "fa-map-signs": "\uf277",
        "fa-map": "\uf279",
        "fa-mars-double": "\uf227",
        "fa-mars-stroke-h": "\uf22b",
        "fa-mars-stroke-v": "\uf22a",
        "fa-mars-stroke": "\uf229",
        "fa-mars": "\uf222",
        "fa-medkit": "\uf0fa",
        "fa-meh-o": "\uf11a",
        "fa-mercury": "\uf223",
        "fa-microchip": "\uf2db",
        "fa-microphone-slash": "\uf131",
        "fa-microphone": "\uf130",
        "fa-minus-circle": "\uf056",
        "fa-minus-square-o": "\uf147",
        "fa-minus-square": "\uf146",
        "fa-minus": "\uf068",
        "fa-mobile-phone": "\uf10b",
        "fa-mobile": "\uf10b",
        "fa-money": "\uf0d6",
        "fa-moon-o": "\uf186",
        "fa-mortar-board": "\uf19d",
        "fa-motorcycle": "\uf21c",
        "fa-mouse-pointer": "\uf245",
        "fa-music": "\uf001",
        "fa-navicon": "\uf0c9",
        "fa-neuter": "\uf22c",
        "fa-newspaper-o": "\uf1ea",
        "fa-object-group": "\uf247",
        "fa-object-ungroup": "\uf248",
        "fa-outdent": "\uf03b",
        "fa-paint-brush": "\uf1fc",
        "fa-paper-plane-o": "\uf1d9",
        "fa-paper-plane": "\uf1d8",
        "fa-paperclip": "\uf0c6",
        "fa-paragraph": "\uf1dd",
        "fa-paste": "\uf0ea",
        "fa-pause-circle-o": "\uf28c",
        "fa-pause-circle": "\uf28b",
        "fa-pause": "\uf04c",
        "fa-paw": "\uf1b0",
        "fa-pencil-square-o": "\uf044",
        "fa-pencil-square": "\uf14b",
        "fa-pencil": "\uf040",
        "fa-percent": "\uf295",
        "fa-phone-square": "\uf098",
        "fa-phone": "\uf095",
        "fa-photo": "\uf03e",
        "fa-picture-o": "\uf03e",
        "fa-pie-chart": "\uf200",
        "fa-plane": "\uf072",
        "fa-play-circle-o": "\uf01d",
        "fa-play-circle": "\uf144",
        "fa-play": "\uf04b",
        "fa-plug": "\uf1e6",
        "fa-plus-circle": "\uf055",
        "fa-plus-square-o": "\uf196",
        "fa-plus-square": "\uf0fe",
        "fa-plus": "\uf067",
        "fa-podcast": "\uf2ce",
        "fa-power-off": "\uf011",
        "fa-print": "\uf02f",
        "fa-puzzle-piece": "\uf12e",
        "fa-qrcode": "\uf029",
        "fa-question-circle-o": "\uf29c",
        "fa-question-circle": "\uf059",
        "fa-question": "\uf128",
        "fa-quote-left": "\uf10d",
        "fa-quote-right": "\uf10e",
        "fa-random": "\uf074",
        "fa-recycle": "\uf1b8",
        "fa-refresh": "\uf021",
        "fa-registered": "\uf25d",
        "fa-remove": "\uf00d",
        "fa-reorder": "\uf0c9",
        "fa-repeat": "\uf01e",
        "fa-reply-all": "\uf122",
        "fa-reply": "\uf112",
        "fa-retweet": "\uf079",
        "fa-rmb": "\uf157",
        "fa-road": "\uf018",
        "fa-rocket": "\uf135",
        "fa-rotate-left": "\uf0e2",
        "fa-rotate-right": "\uf01e",
        "fa-rouble": "\uf158",
        "fa-rss-square": "\uf143",
        "fa-rss": "\uf09e",
        "fa-rub": "\uf158",
        "fa-ruble": "\uf158",
        "fa-rupee": "\uf156",
        "fa-s15": "\uf2cd",
        "fa-save": "\uf0c7",
        "fa-scissors": "\uf0c4",
        "fa-search-minus": "\uf010",
        "fa-search-plus": "\uf00e",
        "fa-search": "\uf002",
        "fa-send-o": "\uf1d9",
        "fa-send": "\uf1d8",
        "fa-server": "\uf233",
        "fa-share-square-o": "\uf045",
        "fa-share-square": "\uf14d",
        "fa-share": "\uf064",
        "fa-shekel": "\uf20b",
        "fa-sheqel": "\uf20b",
        "fa-shield": "\uf132",
        "fa-ship": "\uf21a",
        "fa-shopping-bag": "\uf290",
        "fa-shopping-basket": "\uf291",
        "fa-shopping-cart": "\uf07a",
        "fa-shower": "\uf2cc",
        "fa-sign-in": "\uf090",
        "fa-sign-language": "\uf2a7",
        "fa-sign-out": "\uf08b",
        "fa-signal": "\uf012",
        "fa-signing": "\uf2a7",
        "fa-sitemap": "\uf0e8",
        "fa-sliders": "\uf1de",
        "fa-smile-o": "\uf118",
        "fa-snowflake-o": "\uf2dc",
        "fa-soccer-ball-o": "\uf1e3",
        "fa-sort-alpha-asc": "\uf15d",
        "fa-sort-alpha-desc": "\uf15e",
        "fa-sort-amount-asc": "\uf160",
        "fa-sort-amount-desc": "\uf161",
        "fa-sort-asc": "\uf0de",
        "fa-sort-desc": "\uf0dd",
        "fa-sort-down": "\uf0dd",
        "fa-sort-numeric-asc": "\uf162",
        "fa-sort-numeric-desc": "\uf163",
        "fa-sort-up": "\uf0de",
        "fa-sort": "\uf0dc",
        "fa-space-shuttle": "\uf197",
        "fa-spinner": "\uf110",
        "fa-spoon": "\uf1b1",
        "fa-square-o": "\uf096",
        "fa-square": "\uf0c8",
        "fa-star-half-empty": "\uf123",
        "fa-star-half-full": "\uf123",
        "fa-star-half-o": "\uf123",
        "fa-star-half": "\uf089",
        "fa-star-o": "\uf006",
        "fa-star": "\uf005",
        "fa-step-backward": "\uf048",
        "fa-step-forward": "\uf051",
        "fa-stethoscope": "\uf0f1",
        "fa-sticky-note-o": "\uf24a",
        "fa-sticky-note": "\uf249",
        "fa-stop-circle-o": "\uf28e",
        "fa-stop-circle": "\uf28d",
        "fa-stop": "\uf04d",
        "fa-street-view": "\uf21d",
        "fa-strikethrough": "\uf0cc",
        "fa-subscript": "\uf12c",
        "fa-subway": "\uf239",
        "fa-suitcase": "\uf0f2",
        "fa-sun-o": "\uf185",
        "fa-superscript": "\uf12b",
        "fa-support": "\uf1cd",
        "fa-table": "\uf0ce",
        "fa-tablet": "\uf10a",
        "fa-tachometer": "\uf0e4",
        "fa-tag": "\uf02b",
        "fa-tags": "\uf02c",
        "fa-tasks": "\uf0ae",
        "fa-taxi": "\uf1ba",
        "fa-television": "\uf26c",
        "fa-terminal": "\uf120",
        "fa-text-height": "\uf034",
        "fa-text-width": "\uf035",
        "fa-th-large": "\uf009",
        "fa-th-list": "\uf00b",
        "fa-th": "\uf00a",
        "fa-thermometer-0": "\uf2cb",
        "fa-thermometer-1": "\uf2ca",
        "fa-thermometer-2": "\uf2c9",
        "fa-thermometer-3": "\uf2c8",
        "fa-thermometer-4": "\uf2c7",
        "fa-thermometer-empty": "\uf2cb",
        "fa-thermometer-full": "\uf2c7",
        "fa-thermometer-half": "\uf2c9",
        "fa-thermometer-quarter": "\uf2ca",
        "fa-thermometer-three-quarters": "\uf2c8",
        "fa-thermometer": "\uf2c7",
        "fa-thumb-tack": "\uf08d",
        "fa-thumbs-down": "\uf165",
        "fa-thumbs-o-down": "\uf088",
        "fa-thumbs-o-up": "\uf087",
        "fa-thumbs-up": "\uf164",
        "fa-ticket": "\uf145",
        "fa-times-circle-o": "\uf05c",
        "fa-times-circle": "\uf057",
        "fa-times-rectangle-o": "\uf2d4",
        "fa-times-rectangle": "\uf2d3",
        "fa-times": "\uf00d",
        "fa-tint": "\uf043",
        "fa-toggle-down": "\uf150",
        "fa-toggle-left": "\uf191",
        "fa-toggle-off": "\uf204",
        "fa-toggle-on": "\uf205",
        "fa-toggle-right": "\uf152",
        "fa-toggle-up": "\uf151",
        "fa-trademark": "\uf25c",
        "fa-train": "\uf238",
        "fa-transgender-alt": "\uf225",
        "fa-transgender": "\uf224",
        "fa-trash-o": "\uf014",
        "fa-trash": "\uf1f8",
        "fa-tree": "\uf1bb",
        "fa-trophy": "\uf091",
        "fa-truck": "\uf0d1",
        "fa-try": "\uf195",
        "fa-tty": "\uf1e4",
        "fa-turkish-lira": "\uf195",
        "fa-tv": "\uf26c",
        "fa-umbrella": "\uf0e9",
        "fa-underline": "\uf0cd",
        "fa-undo": "\uf0e2",
        "fa-universal-access": "\uf29a",
        "fa-university": "\uf19c",
        "fa-unlink": "\uf127",
        "fa-unlock-alt": "\uf13e",
        "fa-unlock": "\uf09c",
        "fa-unsorted": "\uf0dc",
        "fa-upload": "\uf093",
        "fa-usd": "\uf155",
        "fa-user-circle-o": "\uf2be",
        "fa-user-circle": "\uf2bd",
        "fa-user-md": "\uf0f0",
        "fa-user-o": "\uf2c0",
        "fa-user-plus": "\uf234",
        "fa-user-secret": "\uf21b",
        "fa-user-times": "\uf235",
        "fa-user": "\uf007",
        "fa-users": "\uf0c0",
        "fa-vcard-o": "\uf2bc",
        "fa-vcard": "\uf2bb",
        "fa-venus-double": "\uf226",
        "fa-venus-mars": "\uf228",
        "fa-venus": "\uf221",
        "fa-video-camera": "\uf03d",
        "fa-volume-control-phone": "\uf2a0",
        "fa-volume-down": "\uf027",
        "fa-volume-off": "\uf026",
        "fa-volume-up": "\uf028",
        "fa-warning": "\uf071",
        "fa-wheelchair-alt": "\uf29b",
        "fa-wheelchair": "\uf193",
        "fa-wifi": "\uf1eb",
        "fa-window-close-o": "\uf2d4",
        "fa-window-close": "\uf2d3",
        "fa-window-maximize": "\uf2d0",
        "fa-window-minimize": "\uf2d1",
        "fa-window-restore": "\uf2d2",
        "fa-won": "\uf159",
        "fa-wrench": "\uf0ad",
        "fa-yen": "\uf157"
    };

    var brandIconMap = {
        "fa-500px": "\uf26e",
        "fa-adn": "\uf170",
        "fa-amazon": "\uf270",
        "fa-android": "\uf17b",
        "fa-angellist": "\uf209",
        "fa-apple": "\uf179",
        "fa-bandcamp": "\uf2d5",
        "fa-behance-square": "\uf1b5",
        "fa-behance": "\uf1b4",
        "fa-bitbucket-square": "\uf172",
        "fa-bitbucket": "\uf171",
        "fa-bitcoin": "\uf15a",
        "fa-black-tie": "\uf27e",
        "fa-bluetooth-b": "\uf294",
        "fa-bluetooth": "\uf293",
        "fa-btc": "\uf15a",
        "fa-buysellads": "\uf20d",
        "fa-cc-amex": "\uf1f3",
        "fa-cc-diners-club": "\uf24c",
        "fa-cc-discover": "\uf1f2",
        "fa-cc-jcb": "\uf24b",
        "fa-cc-mastercard": "\uf1f1",
        "fa-cc-paypal": "\uf1f4",
        "fa-cc-stripe": "\uf1f5",
        "fa-cc-visa": "\uf1f0",
        "fa-chrome": "\uf268",
        "fa-codepen": "\uf1cb",
        "fa-codiepie": "\uf284",
        "fa-connectdevelop": "\uf20e",
        "fa-contao": "\uf26d",
        "fa-css3": "\uf13c",
        "fa-dashcube": "\uf210",
        "fa-delicious": "\uf1a5",
        "fa-deviantart": "\uf1bd",
        "fa-digg": "\uf1a6",
        "fa-dribbble": "\uf17d",
        "fa-dropbox": "\uf16b",
        "fa-drupal": "\uf1a9",
        "fa-edge": "\uf282",
        "fa-eercast": "\uf2da",
        "fa-empire": "\uf1d1",
        "fa-envira": "\uf299",
        "fa-etsy": "\uf2d7",
        "fa-expeditedssl": "\uf23e",
        "fa-fa": "\uf2b4",
        "fa-facebook-f": "\uf09a",
        "fa-facebook-official": "\uf230",
        "fa-facebook-square": "\uf082",
        "fa-facebook": "\uf09a",
        "fa-firefox": "\uf269",
        "fa-first-order": "\uf2b0",
        "fa-flickr": "\uf16e",
        "fa-font-awesome": "\uf2b4",
        "fa-fonticons": "\uf280",
        "fa-fort-awesome": "\uf286",
        "fa-forumbee": "\uf211",
        "fa-foursquare": "\uf180",
        "fa-free-code-camp": "\uf2c5",
        "fa-ge": "\uf1d1",
        "fa-get-pocket": "\uf265",
        "fa-gg-circle": "\uf261",
        "fa-gg": "\uf260",
        "fa-git-square": "\uf1d2",
        "fa-git": "\uf1d3",
        "fa-github-alt": "\uf113",
        "fa-github-square": "\uf092",
        "fa-github": "\uf09b",
        "fa-gitlab": "\uf296",
        "fa-gittip": "\uf184",
        "fa-glide-g": "\uf2a6",
        "fa-glide": "\uf2a5",
        "fa-google-plus-circle": "\uf2b3",
        "fa-google-plus-official": "\uf2b3",
        "fa-google-plus-square": "\uf0d4",
        "fa-google-plus": "\uf0d5",
        "fa-google-wallet": "\uf1ee",
        "fa-google": "\uf1a0",
        "fa-gratipay": "\uf184",
        "fa-grav": "\uf2d6",
        "fa-hacker-news": "\uf1d4",
        "fa-houzz": "\uf27c",
        "fa-html5": "\uf13b",
        "fa-imdb": "\uf2d8",
        "fa-instagram": "\uf16d",
        "fa-internet-explorer": "\uf26b",
        "fa-ioxhost": "\uf208",
        "fa-joomla": "\uf1aa",
        "fa-jsfiddle": "\uf1cc",
        "fa-lastfm-square": "\uf203",
        "fa-lastfm": "\uf202",
        "fa-leanpub": "\uf212",
        "fa-linkedin-square": "\uf08c",
        "fa-linkedin": "\uf0e1",
        "fa-linode": "\uf2b8",
        "fa-linux": "\uf17c",
        "fa-maxcdn": "\uf136",
        "fa-meanpath": "\uf20c",
        "fa-medium": "\uf23a",
        "fa-meetup": "\uf2e0",
        "fa-mixcloud": "\uf289",
        "fa-modx": "\uf285",
        "fa-odnoklassniki-square": "\uf264",
        "fa-odnoklassniki": "\uf263",
        "fa-opencart": "\uf23d",
        "fa-openid": "\uf19b",
        "fa-opera": "\uf26a",
        "fa-optin-monster": "\uf23c",
        "fa-pagelines": "\uf18c",
        "fa-paypal": "\uf1ed",
        "fa-pied-piper-alt": "\uf1a8",
        "fa-pied-piper-pp": "\uf1a7",
        "fa-pied-piper": "\uf2ae",
        "fa-pinterest-p": "\uf231",
        "fa-pinterest-square": "\uf0d3",
        "fa-pinterest": "\uf0d2",
        "fa-product-hunt": "\uf288",
        "fa-qq": "\uf1d6",
        "fa-quora": "\uf2c4",
        "fa-ra": "\uf1d0",
        "fa-ravelry": "\uf2d9",
        "fa-rebel": "\uf1d0",
        "fa-reddit-alien": "\uf281",
        "fa-reddit-square": "\uf1a2",
        "fa-reddit": "\uf1a1",
        "fa-renren": "\uf18b",
        "fa-resistance": "\uf1d0",
        "fa-safari": "\uf267",
        "fa-scribd": "\uf28a",
        "fa-sellsy": "\uf213",
        "fa-share-alt-square": "\uf1e1",
        "fa-share-alt": "\uf1e0",
        "fa-shirtsinbulk": "\uf214",
        "fa-simplybuilt": "\uf215",
        "fa-skyatlas": "\uf216",
        "fa-skype": "\uf17e",
        "fa-slack": "\uf198",
        "fa-slideshare": "\uf1e7",
        "fa-snapchat-ghost": "\uf2ac",
        "fa-snapchat-square": "\uf2ad",
        "fa-snapchat": "\uf2ab",
        "fa-soundcloud": "\uf1be",
        "fa-spotify": "\uf1bc",
        "fa-stack-exchange": "\uf18d",
        "fa-stack-overflow": "\uf16c",
        "fa-steam-square": "\uf1b7",
        "fa-steam": "\uf1b6",
        "fa-stumbleupon-circle": "\uf1a3",
        "fa-stumbleupon": "\uf1a4",
        "fa-superpowers": "\uf2dd",
        "fa-telegram": "\uf2c6",
        "fa-tencent-weibo": "\uf1d5",
        "fa-themeisle": "\uf2b2",
        "fa-trello": "\uf181",
        "fa-tripadvisor": "\uf262",
        "fa-tumblr-square": "\uf174",
        "fa-tumblr": "\uf173",
        "fa-twitch": "\uf1e8",
        "fa-twitter-square": "\uf081",
        "fa-twitter": "\uf099",
        "fa-usb": "\uf287",
        "fa-viacoin": "\uf237",
        "fa-viadeo-square": "\uf2aa",
        "fa-viadeo": "\uf2a9",
        "fa-vimeo-square": "\uf194",
        "fa-vimeo": "\uf27d",
        "fa-vine": "\uf1ca",
        "fa-vk": "\uf189",
        "fa-wechat": "\uf1d7",
        "fa-weibo": "\uf18a",
        "fa-weixin": "\uf1d7",
        "fa-whatsapp": "\uf232",
        "fa-wikipedia-w": "\uf266",
        "fa-windows": "\uf17a",
        "fa-wordpress": "\uf19a",
        "fa-wpbeginner": "\uf297",
        "fa-wpexplorer": "\uf2de",
        "fa-wpforms": "\uf298",
        "fa-xing-square": "\uf169",
        "fa-xing": "\uf168",
        "fa-y-combinator-square": "\uf1d4",
        "fa-y-combinator": "\uf23b",
        "fa-yahoo": "\uf19e",
        "fa-yc-square": "\uf1d4",
        "fa-yc": "\uf23b",
        "fa-yelp": "\uf1e9",
        "fa-yoast": "\uf2b1",
        "fa-youtube-play": "\uf16a",
        "fa-youtube-square": "\uf166",
        "fa-youtube": "\uf167",
    };

    var iconList = Object.keys(iconMap);

    return {
        getIconUnicode: function(name) {
            return iconMap[name] || brandIconMap[name];
        },
        getIconList: function() {
            return iconList;
        },
    }
})();
;/**
 * 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.
 **/

/**
 * An API for undo / redo history buffer
 * @namespace RED.history
*/
RED.history = (function() {
    var undoHistory = [];
    var redoHistory = [];

    function nodeOrJunction(id) {
        var node = RED.nodes.node(id);
        if (node) {
            return node;
        }
        return RED.nodes.junction(id);
    }
    function ensureUnlocked(id, flowsToLock) {
        const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null);
        const isLocked = flow ? flow.locked : false;
        if (flow && isLocked) {
            flow.locked = false;
            flowsToLock.add(flow)
        }
    }
    function undoEvent(ev) {
        var i;
        var len;
        var node;
        var group;
        var subflow;
        var modifiedTabs = {};
        var inverseEv;
        if (ev) {
            if (ev.t == 'multi') {
                inverseEv = {
                    t: 'multi',
                    events: []
                };
                len = ev.events.length;
                for (i=len-1;i>=0;i--) {
                    var r = undoEvent(ev.events[i]);
                    inverseEv.events.push(r);
                }
            } else if (ev.t == 'replace') {
                if (ev.complete) {
                    // This is a replace of everything. We can short-cut
                    // the logic by clearing everyting first, then importing
                    // the ev.config.
                    // Used by RED.diff.mergeDiff
                    inverseEv = {
                        t: 'replace',
                        config: RED.nodes.createCompleteNodeSet(),
                        changed: {},
                        moved: {},
                        complete: true,
                        rev: RED.nodes.version(),
                        dirty: RED.nodes.dirty()
                    };
                    var selectedTab = RED.workspaces.active();
                    inverseEv.config.forEach(n => {
                        const node = RED.nodes.node(n.id)
                        if (node) {
                            inverseEv.changed[n.id] = node.changed
                            inverseEv.moved[n.id] = node.moved
                        }
                    })
                    RED.nodes.clear();
                    var imported = RED.nodes.import(ev.config);
                    // Clear all change flags from the import
                    RED.nodes.dirty(false);

                    const flowsToLock = new Set()
                    
                    imported.nodes.forEach(function(n) {
                        if (ev.changed[n.id]) {
                            ensureUnlocked(n.z, flowsToLock)
                            n.changed = true;
                        }
                        if (ev.moved[n.id]) {
                            ensureUnlocked(n.z, flowsToLock)
                            n.moved = true;
                        }
                    })
                    flowsToLock.forEach(flow => {
                        flow.locked = true
                    })

                    RED.nodes.version(ev.rev);
                    RED.view.redraw(true);
                    RED.palette.refresh();
                    RED.workspaces.refresh();
                    RED.workspaces.show(selectedTab, true);
                    RED.sidebar.config.refresh();
                } else {
                    var importMap = {};
                    ev.config.forEach(function(n) {
                        importMap[n.id] = "replace";
                    })
                    var importedResult = RED.nodes.import(ev.config,{importMap: importMap})
                    inverseEv = {
                        t: 'replace',
                        config: importedResult.removedNodes,
                        dirty: RED.nodes.dirty()
                    }
                }
            } else if (ev.t == 'add') {
                inverseEv = {
                    t: "delete",
                    dirty: RED.nodes.dirty()
                };
                if (ev.nodes) {
                    inverseEv.nodes = [];
                    for (i=0;i<ev.nodes.length;i++) {
                        node = RED.nodes.node(ev.nodes[i]);
                        if (node.z) {
                            modifiedTabs[node.z] = true;
                        }
                        inverseEv.nodes.push(node);
                        RED.nodes.remove(ev.nodes[i]);
                        if (node.g) {
                            var group = RED.nodes.group(node.g);
                            var index = group.nodes.indexOf(node);
                            if (index !== -1) {
                                group.nodes.splice(index,1);
                                RED.group.markDirty(group);
                            }
                        }
                    }
                }
                if (ev.links) {
                    inverseEv.links = [];
                    for (i=0;i<ev.links.length;i++) {
                        inverseEv.links.push(ev.links[i]);
                        RED.nodes.removeLink(ev.links[i]);
                    }
                }
                if (ev.junctions) {
                    inverseEv.junctions = [];
                    for (i=0;i<ev.junctions.length;i++) {
                        inverseEv.junctions.push(ev.junctions[i]);
                        RED.nodes.removeJunction(ev.junctions[i]);
                        if (ev.junctions[i].g) {
                            var group = RED.nodes.group(ev.junctions[i].g);
                            var index = group.nodes.indexOf(ev.junctions[i]);
                            if (index !== -1) {
                                group.nodes.splice(index,1);
                                RED.group.markDirty(group);
                            }
                        }


                    }
                }
                if (ev.groups) {
                    inverseEv.groups = [];
                    for (i = ev.groups.length - 1;i>=0;i--) {
                        group = ev.groups[i];
                        modifiedTabs[group.z] = true;
                        // The order of groups is important
                        //  - to invert the action, the order is reversed
                        inverseEv.groups.unshift(group);
                        RED.nodes.removeGroup(group);
                    }
                }
                if (ev.workspaces) {
                    inverseEv.workspaces = [];
                    for (i=0;i<ev.workspaces.length;i++) {
                        var workspaceOrder = RED.nodes.getWorkspaceOrder();
                        ev.workspaces[i]._index = workspaceOrder.indexOf(ev.workspaces[i].id);
                        inverseEv.workspaces.push(ev.workspaces[i]);
                        RED.nodes.removeWorkspace(ev.workspaces[i].id);
                        RED.workspaces.remove(ev.workspaces[i]);
                    }
                }
                if (ev.subflows) {
                    inverseEv.subflows = [];
                    for (i=0;i<ev.subflows.length;i++) {
                        inverseEv.subflows.push(ev.subflows[i]);
                        RED.nodes.removeSubflow(ev.subflows[i]);
                        RED.workspaces.remove(ev.subflows[i]);
                    }
                }
                if (ev.subflow) {
                    inverseEv.subflow = {};
                    if (ev.subflow.instances) {
                        inverseEv.subflow.instances = [];
                        ev.subflow.instances.forEach(function(n) {
                            inverseEv.subflow.instances.push(n);
                            var node = RED.nodes.node(n.id);
                            if (node) {
                                node.changed = n.changed;
                                node.dirty = true;
                            }
                        });
                    }
                    if (ev.subflow.hasOwnProperty('changed')) {
                        subflow = RED.nodes.subflow(ev.subflow.id);
                        if (subflow) {
                            subflow.changed = ev.subflow.changed;
                        }
                    }
                }
                if (ev.removedLinks) {
                    inverseEv.createdLinks = [];
                    for (i=0;i<ev.removedLinks.length;i++) {
                        inverseEv.createdLinks.push(ev.removedLinks[i]);
                        RED.nodes.addLink(ev.removedLinks[i]);
                    }
                }

            } else if (ev.t == "delete") {
                inverseEv = {
                    t: "add",
                    dirty: RED.nodes.dirty()
                };
                if (ev.workspaces) {
                    inverseEv.workspaces = [];
                    for (i=0;i<ev.workspaces.length;i++) {
                        inverseEv.workspaces.push(ev.workspaces[i]);
                        RED.nodes.addWorkspace(ev.workspaces[i],ev.workspaces[i]._index);
                        RED.workspaces.add(ev.workspaces[i],undefined,ev.workspaces[i]._index);
                        delete ev.workspaces[i]._index;
                    }
                }
                if (ev.subflows) {
                    inverseEv.subflows = [];
                    for (i=0;i<ev.subflows.length;i++) {
                        inverseEv.subflows.push(ev.subflows[i]);
                        RED.nodes.addSubflow(ev.subflows[i]);
                    }
                }
                if (ev.subflowInputs && ev.subflowInputs.length > 0) {
                    subflow = RED.nodes.subflow(ev.subflowInputs[0].z);
                    subflow.in.push(ev.subflowInputs[0]);
                    subflow.in[0].dirty = true;
                }
                if (ev.subflowOutputs && ev.subflowOutputs.length > 0) {
                    subflow = RED.nodes.subflow(ev.subflowOutputs[0].z);
                    ev.subflowOutputs.sort(function(a,b) { return a.i-b.i});
                    for (i=0;i<ev.subflowOutputs.length;i++) {
                        var output = ev.subflowOutputs[i];
                        subflow.out.splice(output.i,0,output);
                        for (var j=output.i+1;j<subflow.out.length;j++) {
                            subflow.out[j].i++;
                            subflow.out[j].dirty = true;
                        }
                        RED.nodes.eachLink(function(l) {
                            if (l.source.type == "subflow:"+subflow.id) {
                                if (l.sourcePort >= output.i) {
                                    l.sourcePort++;
                                }
                            }
                        });
                    }
                }
                if (ev.subflow) {
                    inverseEv.subflow = {};
                    if (ev.subflow.hasOwnProperty('instances')) {
                        inverseEv.subflow.instances = [];
                        ev.subflow.instances.forEach(function(n) {
                            inverseEv.subflow.instances.push(n);
                            var node = RED.nodes.node(n.id);
                            if (node) {
                                node.changed = n.changed;
                                node.dirty = true;
                            }
                        });
                    }
                    if (ev.subflow.hasOwnProperty('status')) {
                        subflow = RED.nodes.subflow(ev.subflow.id);
                        subflow.status = ev.subflow.status;
                    }
                }
                if (subflow) {
                    RED.nodes.filterNodes({type:"subflow:"+subflow.id}).forEach(function(n) {
                        n.inputs = subflow.in.length;
                        n.outputs = subflow.out.length;
                        n.resize = true;
                        n.dirty = true;
                    });
                }
                if (ev.groups) {
                    inverseEv.groups = [];
                    var groupsToAdd = {};
                    ev.groups.forEach(function(g) { groupsToAdd[g.id] = g; });
                    for (i = ev.groups.length - 1;i>=0;i--) {
                        RED.nodes.addGroup(ev.groups[i])
                        modifiedTabs[ev.groups[i].z] = true;
                        // The order of groups is important
                        //  - to invert the action, the order is reversed
                        inverseEv.groups.unshift(ev.groups[i]);
                        if (ev.groups[i].g) {
                            if (!groupsToAdd[ev.groups[i].g]) {
                                group = RED.nodes.group(ev.groups[i].g);
                            } else {
                                group = groupsToAdd[ev.groups[i].g];
                            }
                            if (group.nodes.indexOf(ev.groups[i]) === -1) {
                                group.nodes.push(ev.groups[i]);
                            }
                            RED.group.markDirty(ev.groups[i])
                        }
                    }
                }
                if (ev.nodes) {
                    inverseEv.nodes = [];
                    for (i=0;i<ev.nodes.length;i++) {
                        RED.nodes.add(ev.nodes[i]);
                        modifiedTabs[ev.nodes[i].z] = true;
                        inverseEv.nodes.push(ev.nodes[i].id);
                        if (ev.nodes[i].g) {
                            group = RED.nodes.group(ev.nodes[i].g);
                            if (group.nodes.indexOf(ev.nodes[i]) === -1) {
                                group.nodes.push(ev.nodes[i]);
                            }
                            RED.group.markDirty(group)
                        }
                    }
                }
                if (ev.junctions) {
                    inverseEv.junctions = [];
                    for (i=0;i<ev.junctions.length;i++) {
                        inverseEv.junctions.push(ev.junctions[i]);
                        RED.nodes.addJunction(ev.junctions[i]);
                        if (ev.junctions[i].g) {
                            group = RED.nodes.group(ev.junctions[i].g);
                            if (group.nodes.indexOf(ev.junctions[i]) === -1) {
                                group.nodes.push(ev.junctions[i]);
                            }
                            RED.group.markDirty(group)
                        }

                    }
                }
                if (ev.links) {
                    inverseEv.links = [];
                    for (i=0;i<ev.links.length;i++) {
                        RED.nodes.addLink(ev.links[i]);
                        inverseEv.links.push(ev.links[i]);
                    }
                }
                if (ev.createdLinks) {
                    inverseEv.removedLinks = [];
                    for (i=0;i<ev.createdLinks.length;i++) {
                        inverseEv.removedLinks.push(ev.createdLinks[i]);
                        RED.nodes.removeLink(ev.createdLinks[i]);
                    }
                }
                if (ev.changes) {
                    for (i in ev.changes) {
                        if (ev.changes.hasOwnProperty(i)) {
                            node = RED.nodes.node(i);
                            if (node) {
                                for (var d in ev.changes[i]) {
                                    if (ev.changes[i].hasOwnProperty(d)) {
                                        node[d] = ev.changes[i][d];
                                    }
                                }
                                node.dirty = true;
                            }
                            RED.events.emit("nodes:change",node);
                        }
                    }
                }
                if (subflow) {
                    RED.events.emit("subflows:change", subflow);
                }
            } else if (ev.t == "move") {
                inverseEv = {
                    t: 'move',
                    nodes: [],
                    dirty: RED.nodes.dirty()
                };
                for (i=0;i<ev.nodes.length;i++) {
                    var n = ev.nodes[i];
                    var rn = {n: n.n, ox: n.n.x, oy: n.n.y, dirty: true, moved: n.n.moved};
                    inverseEv.nodes.push(rn);
                    n.n.x = n.ox;
                    n.n.y = n.oy;
                    n.n.dirty = true;
                    n.n.moved = n.moved;
                }
                // A move could have caused a link splice
                if (ev.links) {
                    inverseEv.removedLinks = [];
                    for (i=0;i<ev.links.length;i++) {
                        inverseEv.removedLinks.push(ev.links[i]);
                        RED.nodes.removeLink(ev.links[i]);
                    }
                }
                if (ev.removedLinks) {
                    inverseEv.links = [];
                    for (i=0;i<ev.removedLinks.length;i++) {
                        inverseEv.links.push(ev.removedLinks[i]);
                        RED.nodes.addLink(ev.removedLinks[i]);
                    }
                }
                if (ev.addToGroup) {
                    RED.group.removeFromGroup(ev.addToGroup,ev.nodes.map(function(n) { return n.n }),false);
                    inverseEv.removeFromGroup = ev.addToGroup;
                }
                if (ev.removeFromGroup) {
                    RED.group.addToGroup(ev.removeFromGroup,ev.nodes.map(function(n) { return n.n }));
                    inverseEv.addToGroup = ev.removeFromGroup;
                }
            } else if (ev.t == "edit") {
                inverseEv = {
                    t: "edit",
                    changes: {},
                    changed: ev.node.changed,
                    dirty: RED.nodes.dirty()
                };
                inverseEv.node = ev.node;
                for (i in ev.changes) {
                    if (ev.changes.hasOwnProperty(i)) {
                        inverseEv.changes[i] = ev.node[i];
                        if (ev.node._def.defaults && ev.node._def.defaults[i] && ev.node._def.defaults[i].type) {
                            // This property is a reference to another node or nodes.
                            var nodeList = ev.node[i];
                            if (!Array.isArray(nodeList)) {
                                nodeList = [nodeList];
                            }
                            nodeList.forEach(function(id) {
                                var currentConfigNode = RED.nodes.node(id);
                                if (currentConfigNode && currentConfigNode._def.category === "config") {
                                    currentConfigNode.users.splice(currentConfigNode.users.indexOf(ev.node),1);
                                    RED.events.emit("nodes:change",currentConfigNode);
                                }
                            });
                            nodeList = ev.changes[i];
                            if (!Array.isArray(nodeList)) {
                                nodeList = [nodeList];
                            }
                            nodeList.forEach(function(id) {
                                var newConfigNode = RED.nodes.node(id);
                                if (newConfigNode && newConfigNode._def.category === "config") {
                                    newConfigNode.users.push(ev.node);
                                    RED.events.emit("nodes:change",newConfigNode);
                                }
                            });
                        } else if (i === "env" && ev.node.type.indexOf("subflow:") === 0) {
                            // Subflow can have config node in node.env
                            let nodeList = ev.node.env || [];
                            nodeList = nodeList.reduce((list, prop) => {
                                if (prop.type === "conf-type" && prop.value) {
                                    list.push(prop.value);
                                }
                                return list;
                            }, []);

                            nodeList.forEach(function(id) {
                                const configNode = RED.nodes.node(id);
                                if (configNode) {
                                    if (configNode.users.indexOf(ev.node) !== -1) {
                                        configNode.users.splice(configNode.users.indexOf(ev.node), 1);
                                        RED.events.emit("nodes:change", configNode);
                                    }
                                }
                            });

                            nodeList = ev.changes.env || [];
                            nodeList = nodeList.reduce((list, prop) => {
                                if (prop.type === "conf-type" && prop.value) {
                                    list.push(prop.value);
                                }
                                return list;
                            }, []);

                            nodeList.forEach(function(id) {
                                const configNode = RED.nodes.node(id);
                                if (configNode) {
                                    if (configNode.users.indexOf(ev.node) === -1) {
                                        configNode.users.push(ev.node);
                                        RED.events.emit("nodes:change", configNode);
                                    }
                                }
                            });
                        }
                        if (i === "credentials" && ev.changes[i]) {
                            // Reset - Only want to keep the changes
                            inverseEv.changes[i] = {};
                            for (const [key, value] of Object.entries(ev.changes[i])) {
                                // Edge case: node.credentials is cleared after a deploy, so we can't
                                // capture values for the inverse event when undoing past a deploy
                                if (ev.node.credentials) {
                                    inverseEv.changes[i][key] = ev.node.credentials[key];
                                }
                                ev.node.credentials[key] = value;
                            }
                        } else {
                            ev.node[i] = ev.changes[i];
                        }
                    }
                }

                ev.node.dirty = true;
                ev.node.changed = ev.changed;

                var eventType;
                switch(ev.node.type) {
                    case 'tab': eventType = "flows"; break;
                    case 'group': eventType = "groups"; break;
                    case 'subflow': eventType = "subflows"; break;
                    default: eventType = "nodes"; break;
                }
                eventType += ":change";
                RED.events.emit(eventType,ev.node);


                if (ev.node.type === 'tab' && ev.changes.hasOwnProperty('disabled')) {
                    $("#red-ui-tab-"+(ev.node.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!ev.node.disabled);
                }
                if (ev.node.type === 'tab' && ev.changes.hasOwnProperty('locked')) {
                    $("#red-ui-tab-"+(ev.node.id.replace(".","-"))).toggleClass('red-ui-workspace-locked',!!ev.node.locked);
                }
                if (ev.subflow) {
                    inverseEv.subflow = {};
                    if (ev.subflow.hasOwnProperty('inputCount')) {
                        inverseEv.subflow.inputCount = ev.node.in.length;
                        if (ev.node.in.length > ev.subflow.inputCount) {
                            inverseEv.subflow.inputs = ev.node.in.slice(ev.subflow.inputCount);
                            ev.node.in.splice(ev.subflow.inputCount);
                        } else if (ev.subflow.inputs.length > 0) {
                            ev.node.in = ev.node.in.concat(ev.subflow.inputs);
                        }
                    }
                    if (ev.subflow.hasOwnProperty('outputCount')) {
                        inverseEv.subflow.outputCount = ev.node.out.length;
                        if (ev.node.out.length > ev.subflow.outputCount) {
                            inverseEv.subflow.outputs = ev.node.out.slice(ev.subflow.outputCount);
                            ev.node.out.splice(ev.subflow.outputCount);
                        } else if (ev.subflow.outputs.length > 0) {
                            ev.node.out = ev.node.out.concat(ev.subflow.outputs);
                        }
                    }
                    if (ev.subflow.hasOwnProperty('instances')) {
                        inverseEv.subflow.instances = [];
                        ev.subflow.instances.forEach(function(n) {
                            inverseEv.subflow.instances.push(n);
                            var node = RED.nodes.node(n.id);
                            if (node) {
                                node.changed = n.changed;
                                node.dirty = true;
                            }
                        });
                    }
                    if (ev.subflow.hasOwnProperty('status')) {
                        if (ev.subflow.status) {
                            delete ev.node.status;
                        }
                    }
                    RED.editor.validateNode(ev.node);
                    RED.nodes.filterNodes({type:"subflow:"+ev.node.id}).forEach(function(n) {
                        n.inputs = ev.node.in.length;
                        n.outputs = ev.node.out.length;
                        RED.editor.updateNodeProperties(n);
                        RED.editor.validateNode(n);
                    });
                } else {
                    var outputMap;
                    if (ev.outputMap) {
                        outputMap = {};
                        inverseEv.outputMap = {};
                        for (var port in ev.outputMap) {
                            if (ev.outputMap.hasOwnProperty(port) && ev.outputMap[port] !== "-1") {
                                outputMap[ev.outputMap[port]] = port;
                                inverseEv.outputMap[ev.outputMap[port]] = port;
                            }
                        }
                    }
                    ev.node.__outputs = inverseEv.changes.outputs;
                    RED.editor.updateNodeProperties(ev.node,outputMap);
                    RED.editor.validateNode(ev.node);
                }
                // If it's a Config Node, validate user nodes too.
                // NOTE: The Config Node must be validated before validating users.
                if (ev.node.users) {
                    const validatedNodes = new Set();
                    const userStack = ev.node.users.slice();

                    validatedNodes.add(ev.node.id);
                    while (userStack.length) {
                        const node = userStack.pop();
                        if (!validatedNodes.has(node.id)) {
                            validatedNodes.add(node.id);
                            if (node.users) {
                                userStack.push(...node.users);
                            }
                            RED.editor.validateNode(node);
                        }
                    }
                }
                if (ev.links) {
                    inverseEv.createdLinks = [];
                    for (i=0;i<ev.links.length;i++) {
                        RED.nodes.addLink(ev.links[i]);
                        inverseEv.createdLinks.push(ev.links[i]);
                    }
                }
                if (ev.createdLinks) {
                    inverseEv.links = [];
                    for (i=0;i<ev.createdLinks.length;i++) {
                        RED.nodes.removeLink(ev.createdLinks[i]);
                        inverseEv.links.push(ev.createdLinks[i]);
                    }
                }
            } else if (ev.t == "createSubflow") {
                inverseEv = {
                    t: "deleteSubflow",
                    activeWorkspace: ev.activeWorkspace,
                    dirty: RED.nodes.dirty()
                };
                if (ev.nodes) {
                    inverseEv.movedNodes = [];
                    var z = ev.activeWorkspace;
                    var fullNodeList = RED.nodes.filterNodes({z:ev.subflow.subflow.id});
                    fullNodeList = fullNodeList.concat(RED.nodes.groups(ev.subflow.subflow.id))
                    fullNodeList = fullNodeList.concat(RED.nodes.junctions(ev.subflow.subflow.id))
                    fullNodeList.forEach(function(n) {
                        n.x += ev.subflow.offsetX;
                        n.y += ev.subflow.offsetY;
                        n.dirty = true;
                        inverseEv.movedNodes.push(n.id);
                        RED.nodes.moveNodeToTab(n, z);
                    });
                    inverseEv.subflows = [];
                    for (i=0;i<ev.nodes.length;i++) {
                        inverseEv.subflows.push(nodeOrJunction(ev.nodes[i]));
                        RED.nodes.remove(ev.nodes[i]);
                    }
                }
                if (ev.links) {
                    inverseEv.links = [];
                    for (i=0;i<ev.links.length;i++) {
                        inverseEv.links.push(ev.links[i]);
                        RED.nodes.removeLink(ev.links[i]);
                    }
                }

                inverseEv.subflow = ev.subflow;
                RED.nodes.removeSubflow(ev.subflow.subflow);
                RED.workspaces.remove(ev.subflow.subflow);

                if (ev.removedLinks) {
                    inverseEv.createdLinks = [];
                    for (i=0;i<ev.removedLinks.length;i++) {
                        inverseEv.createdLinks.push(ev.removedLinks[i]);
                        RED.nodes.addLink(ev.removedLinks[i]);
                    }
                }
            } else if (ev.t == "deleteSubflow") {
                inverseEv = {
                    t: "createSubflow",
                    activeWorkspace: ev.activeWorkspace,
                    dirty: RED.nodes.dirty(),
                };
                if (ev.subflow) {
                    RED.nodes.addSubflow(ev.subflow.subflow);
                    inverseEv.subflow = ev.subflow;
                    if (ev.subflow.subflow.g) {
                        RED.group.addToGroup(RED.nodes.group(ev.subflow.subflow.g),ev.subflow.subflow);
                    }
                }
                if (ev.subflows) {
                    inverseEv.nodes = [];
                    for (i=0;i<ev.subflows.length;i++) {
                        RED.nodes.add(ev.subflows[i]);
                        inverseEv.nodes.push(ev.subflows[i].id);
                    }
                }
                if (ev.movedNodes) {
                    ev.movedNodes.forEach(function(nid) {
                        nn = RED.nodes.node(nid);
                        if (!nn) {
                            nn = RED.nodes.group(nid);
                        }
                        nn.x -= ev.subflow.offsetX;
                        nn.y -= ev.subflow.offsetY;
                        nn.dirty = true;
                        RED.nodes.moveNodeToTab(nn, ev.subflow.subflow.id);
                    });
                }
                if (ev.links) {
                    inverseEv.links = [];
                    for (i=0;i<ev.links.length;i++) {
                        inverseEv.links.push(ev.links[i]);
                        RED.nodes.addLink(ev.links[i]);
                    }
                }
                if (ev.createdLinks) {
                    inverseEv.removedLinks = [];
                    for (i=0;i<ev.createdLinks.length;i++) {
                        inverseEv.removedLinks.push(ev.createdLinks[i]);
                        RED.nodes.removeLink(ev.createdLinks[i]);
                    }
                }
            } else if (ev.t == "reorder") {
                inverseEv = {
                    t: 'reorder',
                    dirty: RED.nodes.dirty()
                };
                if (ev.workspaces) {
                    inverseEv.workspaces = {
                        from: ev.workspaces.to,
                        to: ev.workspaces.from
                    }
                    RED.workspaces.order(ev.workspaces.from);
                }
                if (ev.nodes) {
                    inverseEv.nodes = {
                        z: ev.nodes.z,
                        from: ev.nodes.to,
                        to: ev.nodes.from
                    }
                    RED.nodes.setNodeOrder(ev.nodes.z,ev.nodes.from);
                }
            } else if (ev.t == "createGroup") {
                inverseEv = {
                    t: "ungroup",
                    dirty: RED.nodes.dirty(),
                    groups: []
                }
                if (ev.groups) {
                    for (i=0;i<ev.groups.length;i++) {
                        inverseEv.groups.push(ev.groups[i]);
                        RED.group.ungroup(ev.groups[i]);
                    }
                }
            } else if (ev.t == "ungroup") {
                inverseEv = {
                    t: "createGroup",
                    dirty: RED.nodes.dirty(),
                    groups: []
                }
                if (ev.groups) {
                    for (i=0;i<ev.groups.length;i++) {
                        inverseEv.groups.push(ev.groups[i]);
                        var nodes = ev.groups[i].nodes.slice();
                        ev.groups[i].nodes = [];
                        RED.nodes.addGroup(ev.groups[i]);
                        RED.group.addToGroup(ev.groups[i],nodes);
                        if (ev.groups[i].g) {
                            const parentGroup = RED.nodes.group(ev.groups[i].g)
                            if (parentGroup) {
                                RED.group.addToGroup(parentGroup, ev.groups[i])
                            }
                        }
                    }
                }
            } else if (ev.t == "addToGroup") {
                inverseEv = {
                    t: "removeFromGroup",
                    dirty: RED.nodes.dirty(),
                    group: ev.group,
                    nodes: ev.nodes,
                    reparent: ev.reparent
                }
                if (ev.nodes) {
                    RED.group.removeFromGroup(ev.group,ev.nodes,(ev.hasOwnProperty('reparent')&&ev.hasOwnProperty('reparent')!==undefined)?ev.reparent:true);
                }
            } else if (ev.t == "removeFromGroup") {
                inverseEv = {
                    t: "addToGroup",
                    dirty: RED.nodes.dirty(),
                    group: ev.group,
                    nodes: ev.nodes,
                    reparent: ev.reparent
                }
                if (ev.nodes) {
                    RED.group.addToGroup(ev.group,ev.nodes);
                }
            }

            if(ev.callback && typeof ev.callback === 'function') {
                inverseEv.callback = ev.callback;
                ev.callback(ev);
            }

            Object.keys(modifiedTabs).forEach(function(id) {
                var subflow = RED.nodes.subflow(id);
                if (subflow) {
                    RED.editor.validateNode(subflow);
                }
            });

            RED.nodes.dirty(ev.dirty);
            RED.view.updateActive();
            RED.view.select(null);
            RED.workspaces.refresh();
            RED.sidebar.config.refresh();
            RED.subflow.refresh();

            return inverseEv;
        }

    }

    return {
        //TODO: this function is a placeholder until there is a 'save' event that can be listened to
        markAllDirty: function() {
            for (var i=0;i<undoHistory.length;i++) {
                undoHistory[i].dirty = true;
            }
        },
        list: function() {
            return undoHistory;
        },
        listRedo: function() {
            return redoHistory;
        },
        depth: function() {
            return undoHistory.length;
        },
        push: function(ev) {
            undoHistory.push(ev);
            redoHistory = [];
            RED.menu.setDisabled("menu-item-edit-undo", false);
            RED.menu.setDisabled("menu-item-edit-redo", true);
        },
        pop: function() {
            var ev = undoHistory.pop();
            var rev = undoEvent(ev);
            if (rev) {
                redoHistory.push(rev);
            }
            RED.menu.setDisabled("menu-item-edit-undo", undoHistory.length === 0);
            RED.menu.setDisabled("menu-item-edit-redo", redoHistory.length === 0);
        },
        peek: function() {
            return undoHistory[undoHistory.length-1];
        },
        replace: function(ev) {
            if (undoHistory.length === 0) {
                RED.history.push(ev);
            } else {
                undoHistory[undoHistory.length-1] = ev;
            }
        },
        clear: function() {
            undoHistory = [];
            redoHistory = [];
            RED.menu.setDisabled("menu-item-edit-undo", true);
            RED.menu.setDisabled("menu-item-edit-redo", true);
        },
        redo: function() {
            var ev = redoHistory.pop();
            if (ev) {
                var uev = undoEvent(ev);
                if (uev) {
                    undoHistory.push(uev);
                }
            }
            RED.menu.setDisabled("menu-item-edit-undo", undoHistory.length === 0);
            RED.menu.setDisabled("menu-item-edit-redo", redoHistory.length === 0);
        }
    }

})();
;/**
 * 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.
 **/
RED.validators = {
    number: function(blankAllowed,mopt){
        return function(v, opt) {
            if (blankAllowed && (v === '' || v === undefined)) {
                return true
            }
            if (v !== '') {
                if (/^NaN$|^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$|^[+-]?(0b|0B)[01]+$|^[+-]?(0o|0O)[0-7]+$|^[+-]?(0x|0X)[0-9a-fA-F]+$/.test(v)) {
                    return true
                }
                if (/^\${[^}]+}$/.test(v)) {
                    // Allow ${ENV_VAR} value
                    return true
                }
            }
            if (!isNaN(v)) {
                return true
            }
            if (opt && opt.label) {
                return RED._("validator.errors.invalid-num-prop", {
                    prop: opt.label
                });
            }
            return opt ? RED._("validator.errors.invalid-num") : false;
        };
    },
    regex: function(re, mopt) {
        return function(v, opt) {
            if (re.test(v)) {
                return true;
            }
            if (opt && opt.label) {
                return RED._("validator.errors.invalid-regex-prop", {
                    prop: opt.label
                });
            }
            return opt ? RED._("validator.errors.invalid-regexp") : false;
        };
    },
    typedInput: function(ptypeName, isConfig, mopt) {
        let options = ptypeName
        if (typeof ptypeName === 'string' ) {
            options = {}
            options.typeField = ptypeName
            options.isConfig = isConfig
            options.allowBlank = false
        }
        return function(v, opt) {
            let ptype = options.type
            if (!ptype && options.typeField) {
                ptype = $("#node-"+(options.isConfig?"config-":"")+"input-"+options.typeField).val() || this[options.typeField];
            }
            if (options.allowBlank && v === '') {
                return true
            }
            if (options.allowUndefined && v === undefined) {
                return true
            }
            const result = RED.utils.validateTypedProperty(v, ptype, opt)
            if (result === true || opt) {
                // Valid, or opt provided - return result as-is
                return result
            }
            // No opt - need to return false for backwards compatibilty
            return false
        }
    }
};;/**
 * 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.
 **/

RED.utils = (function() {

    window._marked = window.marked;
    window.marked = function(txt) {
        console.warn("Use of 'marked()' is deprecated. Use RED.utils.renderMarkdown() instead");
        return renderMarkdown(txt);
    }

    const descriptionList = {
        name: 'descriptionList',
        level: 'block',                                     // Is this a block-level or inline-level tokenizer?
        start(src) {
            if (!src) { return null; }
            let m = src.match(/:[^:\n]/g);
            return m && m.index; // Hint to Marked.js to stop and check for a match
        },
        tokenizer(src, tokens) {
            if (!src) { return null; }
            const rule = /^(?::[^:\n]+:[^:\n]*(?:\n|$))+/;    // Regex for the complete token
            const match = rule.exec(src);
            if (match) {
                return {                                        // Token to generate
                    type: 'descriptionList',                      // Should match "name" above
                    raw: match[0],                                // Text to consume from the source
                    text: match[0].trim(),                        // Additional custom properties
                    tokens: this.lexer.inlineTokens(match[0].trim())    // inlineTokens to process **bold**, *italics*, etc.
                };
            }
        },
        renderer(token) {
            return `<dl class="message-properties">${this.parser.parseInline(token.tokens)}\n</dl>`; // parseInline to turn child tokens into HTML
        }
    };

    const description = {
        name: 'description',
        level: 'inline',           // Is this a block-level or inline-level tokenizer?
        start(src) {
            if (!src) { return null; }
            let m = src.match(/:/g);
            return m && m.index;   // Hint to Marked.js to stop and check for a match
        },
        tokenizer(src, tokens) {
            if (!src) { return null; }
            const rule = /^:([^:\n]+)\(([^:\n]+)\).*?:([^:\n]*)(?:\n|$)/;  // Regex for the complete token
            const match = rule.exec(src);
            if (match) {
                return {                                       // Token to generate
                    type: 'description',                       // Should match "name" above
                    raw: match[0],                             // Text to consume from the source
                    dt: this.lexer.inlineTokens(match[1].trim()),    // Additional custom properties
                    types: this.lexer.inlineTokens(match[2].trim()),
                    dd: this.lexer.inlineTokens(match[3].trim()),
                };
            }
        },
        renderer(token) {
            return `\n<dt>${this.parser.parseInline(token.dt)}<span class="property-type">${this.parser.parseInline(token.types)}</span></dt><dd>${this.parser.parseInline(token.dd)}</dd>`;
        },
        childTokens: ['dt', 'dd'],                 // Any child tokens to be visited by walkTokens
        walkTokens(token) {                        // Post-processing on the completed token tree
            if (token.type === 'strong') {
                token.text += ' walked';
            }
        }
    };


    const renderer = new window._marked.Renderer();

    //override list creation - add node-ports to order lists
    renderer.list = function (body, ordered, start) {
        let addClass = /dl.*?class.*?message-properties.*/.test(body);
        if (addClass && ordered) {
            return '<ol class="node-ports">' + body + '</ol>';
        } else if (ordered) {
            return '<ol>' + body + '</ol>';
        } else {
            return '<ul>' + body + '</ul>'
        }
    }

    var mermaidIsInitialized = false;
    var mermaidIsEnabled /* = undefined */;

    renderer.code = function (code, lang) {
        if(lang === "mermaid") {
            return `<pre class='mermaid'>${code}</pre>`;
        } else {
            return "<pre><code>" +code +"</code></pre>";
        }
    };

    window._marked.setOptions({
        renderer: renderer,
        gfm: true,
        tables: true,
        breaks: false,
        pedantic: false,
        smartLists: true,
        smartypants: false
    });

    window._marked.use({extensions: [descriptionList, description] } );

    function renderMarkdown(txt) {
        var rendered = _marked.parse(txt);
        var cleaned = DOMPurify.sanitize(rendered, {SAFE_FOR_JQUERY: true})
        return cleaned;
    }

    function formatString(str) {
        return str.replace(/\r?\n/g,"&crarr;").replace(/\t/g,"&rarr;");
    }
    function sanitize(m) {
        return m.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
    }

    function buildMessageSummaryValue(value) {
        var result;
        if (Array.isArray(value)) {
            result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-meta"></span>').text('array['+value.length+']');
        } else if (value === null) {
            result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-null">null</span>');
        } else if (typeof value === 'object') {
            if (value.hasOwnProperty('type') && value.type === 'undefined') {
                result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-null">undefined</span>');
            } else if (value.hasOwnProperty('type') && value.type === 'Buffer' && value.hasOwnProperty('data')) {
                result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-meta"></span>').text('buffer['+value.length+']');
            } else if (value.hasOwnProperty('type') && value.type === 'array' && value.hasOwnProperty('data')) {
                result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-meta"></span>').text('array['+value.length+']');
            } else if (value.hasOwnProperty('type') && value.type === 'set' && value.hasOwnProperty('data')) {
                result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-meta"></span>').text('set['+value.length+']');
            } else if (value.hasOwnProperty('type') && value.type === 'map' && value.hasOwnProperty('data')) {
                result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-meta"></span>').text('map');
            } else if (value.hasOwnProperty('type') && value.type === 'function') {
                result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-meta"></span>').text('function');
            } else if (value.hasOwnProperty('type') && (value.type === 'number' || value.type === 'bigint')) {
                result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-number"></span>').text(value.data);
            } else if (value.hasOwnProperty('type') && value.type === 'regexp') {
                result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-string"></span>').text(value.data);
            } else {
                result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-meta">object</span>');
            }
        } else if (typeof value === 'string') {
            var subvalue;
            if (value.length > 30) {
                subvalue = sanitize(value.substring(0,30))+"&hellip;";
            } else {
                subvalue = sanitize(value);
            }
            result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-string"></span>').html('"'+formatString(subvalue)+'"');
        } else if (typeof value === 'number') {
            result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-number"></span>').text(""+value);
        } else {
            result = $('<span class="red-ui-debug-msg-object-value red-ui-debug-msg-type-other"></span>').text(""+value);
        }
        return result;
    }
    function makeExpandable(el,onbuild,ontoggle,expand) {
        el.addClass("red-ui-debug-msg-expandable");
        el.prop('toggle',function() {
            return function(state) {
                var parent = el.parent();
                if (parent.hasClass('collapsed')) {
                    if (state) {
                        if (onbuild && !parent.hasClass('built')) {
                            onbuild();
                            parent.addClass('built');
                        }
                        parent.removeClass('collapsed');
                        return true;
                    }
                } else {
                    if (!state) {
                        parent.addClass('collapsed');
                        return true;
                    }
                }
                return false;
            }
        });
        el.on("click", function(e) {
            var parent = $(this).parent();
            var currentState = !parent.hasClass('collapsed');
            if ($(this).prop('toggle')(!currentState)) {
                if (ontoggle) {
                    ontoggle(!currentState);
                }
            }
            // if (parent.hasClass('collapsed')) {
            //     if (onbuild && !parent.hasClass('built')) {
            //         onbuild();
            //         parent.addClass('built');
            //     }
            //     if (ontoggle) {
            //         ontoggle(true);
            //     }
            //     parent.removeClass('collapsed');
            // } else {
            //     parent.addClass('collapsed');
            //     if (ontoggle) {
            //         ontoggle(false);
            //     }
            // }
            e.preventDefault();
        });
        if (expand) {
            el.trigger("click");
        }

    }

    var pinnedPaths = {};
    var formattedPaths = {};

    function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools,enablePinning) {
        if (!pinnedPaths.hasOwnProperty(sourceId)) {
            pinnedPaths[sourceId] = {}
        }
        var tools = $('<span class="red-ui-debug-msg-tools"></span>').appendTo(obj);
        var copyTools = $('<span class="red-ui-debug-msg-tools-copy button-group"></span>').appendTo(tools);
        if (!!key) {
            var copyPath = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-terminal"></i></button>').appendTo(copyTools).on("click", function(e) {
                e.preventDefault();
                e.stopPropagation();
                RED.clipboard.copyText(key,copyPath,"clipboard.copyMessagePath");
            })
            RED.popover.tooltip(copyPath,RED._("node-red:debug.sidebar.copyPath"));
        }
        var copyPayload = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-clipboard"></i></button>').appendTo(copyTools).on("click", function(e) {
            e.preventDefault();
            e.stopPropagation();
            RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue");
        })
        RED.popover.tooltip(copyPayload,RED._("node-red:debug.sidebar.copyPayload"));
        if (enablePinning && strippedKey !== undefined && strippedKey !== '') {
            var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey);

            var pinPath = $('<button class="red-ui-button red-ui-button-small red-ui-debug-msg-tools-pin"><i class="fa fa-map-pin"></i></button>').appendTo(tools).on("click", function(e) {
                e.preventDefault();
                e.stopPropagation();
                if (pinnedPaths[sourceId].hasOwnProperty(strippedKey)) {
                    delete pinnedPaths[sourceId][strippedKey];
                    $(this).removeClass("selected");
                    obj.removeClass("red-ui-debug-msg-row-pinned");
                } else {
                    var rootedPath = "$"+(strippedKey[0] === '['?"":".")+strippedKey;
                    pinnedPaths[sourceId][strippedKey] = normalisePropertyExpression(rootedPath);
                    $(this).addClass("selected");
                    obj.addClass("red-ui-debug-msg-row-pinned");
                }
            }).toggleClass("selected",isPinned);
            obj.toggleClass("red-ui-debug-msg-row-pinned",isPinned);
            RED.popover.tooltip(pinPath,RED._("node-red:debug.sidebar.pinPath"));
        }
        if (extraTools) {
            var t = extraTools;
            if (typeof t === 'function') {
                t = t(key,msg);
            }
            if (t) {
                t.addClass("red-ui-debug-msg-tools-other");
                t.appendTo(tools);
            }
        }
    }
    function checkExpanded(strippedKey, expandPaths, { minRange, maxRange, expandLeafNodes }) {
        if (expandPaths && expandPaths.length > 0) {
            if (strippedKey === '' && minRange === undefined) {
                return true;
            }
            for (var i=0;i<expandPaths.length;i++) {
                var p = expandPaths[i];
                if (expandLeafNodes && p === strippedKey) {
                    return true
                }
                if (p.indexOf(strippedKey) === 0 && (p[strippedKey.length] === "." ||  p[strippedKey.length] === "[") ) {

                    if (minRange !== undefined && p[strippedKey.length] === "[") {
                        var subkey = p.substring(strippedKey.length);
                        var m = (/\[(\d+)\]/.exec(subkey));
                        if (m) {
                            var index = parseInt(m[1]);
                            return minRange<=index && index<=maxRange;
                        }
                    } else {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    function formatNumber(element,obj,sourceId,path,cycle,initialFormat) {
        var format = (formattedPaths[sourceId] && formattedPaths[sourceId][path] && formattedPaths[sourceId][path]['number']) || initialFormat || "dec";
        if (cycle) {
            if (format === 'dec') {
                if ((obj.toString().length===13) && (obj<=2147483647000)) {
                    format = 'dateMS';
                } else if ((obj.toString().length===10) && (obj<=2147483647)) {
                    format = 'dateS';
                } else {
                    format = 'hex'
                }
            } else if (format === 'dateMS' || format == 'dateS') {
                if ((obj.toString().length===13) && (obj<=2147483647000)) {
                    format = 'dateML';
                } else if ((obj.toString().length===10) && (obj<=2147483647)) {
                    format = 'dateL';
                } else {
                    format = 'hex'
                }
            } else if (format === 'dateML' || format == 'dateL') {
                format = 'hex';
            } else {
                format = 'dec';
            }
            formattedPaths[sourceId] = formattedPaths[sourceId]||{};
            formattedPaths[sourceId][path] = formattedPaths[sourceId][path]||{};
            formattedPaths[sourceId][path]['number'] = format;
        } else if (initialFormat !== undefined){
            formattedPaths[sourceId] = formattedPaths[sourceId]||{};
            formattedPaths[sourceId][path] = formattedPaths[sourceId][path]||{};
            formattedPaths[sourceId][path]['number'] = format;
        }
        if (format === 'dec') {
            element.text(""+obj);
        } else if (format === 'dateMS') {
            element.text((new Date(obj)).toISOString());
        } else if (format === 'dateS') {
            element.text((new Date(obj*1000)).toISOString());
        } else if (format === 'dateML') {
            var dd = new Date(obj);
            element.text(dd.toLocaleString() + "  [UTC" + ( dd.getTimezoneOffset()/-60 <=0?"":"+" ) + dd.getTimezoneOffset()/-60 +"]");
        } else if (format === 'dateL') {
            var ddl = new Date(obj*1000);
            element.text(ddl.toLocaleString() + "  [UTC" + ( ddl.getTimezoneOffset()/-60 <=0?"":"+" ) + ddl.getTimezoneOffset()/-60 +"]");
        } else if (format === 'hex') {
            element.text("0x"+(obj).toString(16));
        }
    }

    function formatBuffer(element,button,sourceId,path,cycle) {
        var format = (formattedPaths[sourceId] && formattedPaths[sourceId][path] && formattedPaths[sourceId][path]['buffer']) || "raw";
        if (cycle) {
            if (format === 'raw') {
                format = 'string';
            } else {
                format = 'raw';
            }
            formattedPaths[sourceId] = formattedPaths[sourceId]||{};
            formattedPaths[sourceId][path] = formattedPaths[sourceId][path]||{};
            formattedPaths[sourceId][path]['buffer'] = format;
        }
        if (format === 'raw') {
            button.text('raw');
            element.removeClass('red-ui-debug-msg-buffer-string').addClass('red-ui-debug-msg-buffer-raw');
        } else if (format === 'string') {
            button.text('string');
            element.addClass('red-ui-debug-msg-buffer-string').removeClass('red-ui-debug-msg-buffer-raw');
        }
    }

    /**
     * Create a DOM element representation of obj - as used by Debug sidebar etc
     *
     * @params obj - the data to display
     * @params options - a bag of options
     *
     * - If you want the Copy Value button, then set `sourceId`
     * - If you want the Copy Path button, also set `path` to the value to be copied
     */
    function createObjectElement(obj,options) {
        options = options || {};
        var key = options.key;
        var typeHint = options.typeHint;
        var hideKey = options.hideKey;
        var path = options.path;
        var sourceId = options.sourceId;
        var rootPath = options.rootPath;
        var expandPaths = options.expandPaths;
        const enablePinning = options.enablePinning
        const expandLeafNodes = options.expandLeafNodes;
        var ontoggle = options.ontoggle;
        var exposeApi = options.exposeApi;
        var tools = options.tools;

        var subElements = {};
        var i;
        var e;
        var entryObj;
        var expandableHeader;
        var header;
        var headerHead;
        var value;
        var strippedKey;
        if (path !== undefined && rootPath !== undefined) {
             strippedKey = path.substring(rootPath.length+(path[rootPath.length]==="."?1:0));
        }
        var element = $('<span class="red-ui-debug-msg-element"></span>');
        element.collapse = function() {
            element.find(".red-ui-debug-msg-expandable").parent().addClass("collapsed");
        }
        header = $('<span class="red-ui-debug-msg-row"></span>').appendTo(element);
        if (sourceId) {
            addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools, enablePinning);
        }
        if (!key) {
            element.addClass("red-ui-debug-msg-top-level");
            if (sourceId && !expandPaths) {
                var pinned = pinnedPaths[sourceId];
                expandPaths = [];
                if (pinned) {
                    for (var pinnedPath in pinned) {
                        if (pinned.hasOwnProperty(pinnedPath)) {
                            try {
                                var res = getMessageProperty({$:obj},pinned[pinnedPath]);
                                if (res !== undefined) {
                                    expandPaths.push(pinnedPath);
                                }
                            } catch(err) {
                            }
                        }
                    }
                    expandPaths.sort();
                }
                element.clearPinned = function() {
                    element.find(".red-ui-debug-msg-row-pinned").removeClass("red-ui-debug-msg-row-pinned");
                    pinnedPaths[sourceId] = {};
                }
            }
        } else {
            if (!hideKey) {
                $('<span class="red-ui-debug-msg-object-key"></span>').text(key).appendTo(header);
                $('<span>: </span>').appendTo(header);
            }
        }
        entryObj = $('<span class="red-ui-debug-msg-object-value"></span>').appendTo(header);

        var isArray = Array.isArray(obj);
        var isArrayObject = false;
        if (obj && typeof obj === 'object' && obj.hasOwnProperty('type') && obj.hasOwnProperty('data') && ((obj.__enc__ && obj.type === 'set') || (obj.__enc__ && obj.type === 'array') || obj.type === 'Buffer')) {
            isArray = true;
            isArrayObject = true;
        }
        if (obj === null || obj === undefined) {
            $('<span class="red-ui-debug-msg-type-null">'+obj+'</span>').appendTo(entryObj);
        } else if (obj.__enc__ && obj.type === 'undefined') {
            $('<span class="red-ui-debug-msg-type-null">undefined</span>').appendTo(entryObj);
        } else if (obj.__enc__ && (obj.type === 'number' || obj.type === 'bigint')) {
            e = $('<span class="red-ui-debug-msg-type-number red-ui-debug-msg-object-header"></span>').text(obj.data).appendTo(entryObj);
        } else if (typeHint === "regexp" || (obj.__enc__ && obj.type === 'regexp')) {
            e = $('<span class="red-ui-debug-msg-type-string red-ui-debug-msg-object-header"></span>').text((typeof obj === "string")?obj:obj.data).appendTo(entryObj);
        } else if (typeHint === "function" || (obj.__enc__ && obj.type === 'function')) {
            e = $('<span class="red-ui-debug-msg-type-meta red-ui-debug-msg-object-header"></span>').text("function").appendTo(entryObj);
        } else if (typeHint === "internal" || (obj.__enc__ && obj.type === 'internal')) {
            e = $('<span class="red-ui-debug-msg-type-meta red-ui-debug-msg-object-header"></span>').text("[internal]").appendTo(entryObj);
        } else if (typeof obj === 'string') {
            if (/[\t\n\r]/.test(obj)) {
                element.addClass('collapsed');
                $('<i class="fa fa-caret-right red-ui-debug-msg-object-handle"></i> ').prependTo(header);
                makeExpandable(header, function() {
                    $('<span class="red-ui-debug-msg-type-meta red-ui-debug-msg-object-type-header"></span>').text(typeHint||'string').appendTo(header);
                    var row = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(element);
                    $('<pre class="red-ui-debug-msg-type-string"></pre>').text(obj).appendTo(row);
                },function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
            }
            e = $('<span class="red-ui-debug-msg-type-string red-ui-debug-msg-object-header"></span>').html('"'+formatString(sanitize(obj))+'"').appendTo(entryObj);
            if (/^#[0-9a-f]{6}$/i.test(obj)) {
                $('<span class="red-ui-debug-msg-type-string-swatch"></span>').css('backgroundColor',obj).appendTo(e);
            }

            let n = RED.nodes.node(obj) ?? RED.nodes.workspace(obj);
            if (n) {
                if (options.nodeSelector && "function" == typeof options.nodeSelector) {
                    e.css('cursor', 'pointer').on("click", function(evt) {
                        evt.preventDefault();
                        options.nodeSelector(n.id);
                    })
                }
            }

        } else if (typeof obj === 'number') {
            e = $('<span class="red-ui-debug-msg-type-number"></span>').appendTo(entryObj);

            if (Number.isInteger(obj) && (obj >= 0)) { // if it's a +ve integer
                e.addClass("red-ui-debug-msg-type-number-toggle");
                e.on("click", function(evt) {
                    evt.preventDefault();
                    formatNumber($(this), obj, sourceId, path, true);
                });
            }
            formatNumber(e,obj,sourceId,path,false,typeHint==='hex'?'hex':undefined);

        } else if (isArray) {
            element.addClass('collapsed');

            var originalLength = obj.length;
            if (typeHint) {
                var m = /\[(\d+)\]/.exec(typeHint);
                if (m) {
                    originalLength = parseInt(m[1]);
                }
            }
            var data = obj;
            var type = 'array';
            if (isArrayObject) {
                data = obj.data;
                if (originalLength === undefined) {
                    originalLength = data.length;
                }
                if (data.__enc__) {
                    data = data.data;
                }
                type = obj.type.toLowerCase();
            } else if (/buffer/.test(typeHint)) {
                type = 'buffer';
            }
            var fullLength = data.length;

            if (originalLength > 0) {
                $('<i class="fa fa-caret-right red-ui-debug-msg-object-handle"></i> ').prependTo(header);
                var arrayRows = $('<div class="red-ui-debug-msg-array-rows"></div>').appendTo(element);
                element.addClass('red-ui-debug-msg-buffer-raw');
            }
            if (key) {
                headerHead = $('<span class="red-ui-debug-msg-type-meta"></span>').text(typeHint||(type+'['+originalLength+']')).appendTo(entryObj);
            } else {
                headerHead = $('<span class="red-ui-debug-msg-object-header"></span>').appendTo(entryObj);
                $('<span>[ </span>').appendTo(headerHead);
                var arrayLength = Math.min(originalLength,10);
                for (i=0;i<arrayLength;i++) {
                    buildMessageSummaryValue(data[i]).appendTo(headerHead);
                    if (i < arrayLength-1) {
                        $('<span>, </span>').appendTo(headerHead);
                    }
                }
                if (originalLength > arrayLength) {
                    $('<span> &hellip;</span>').appendTo(headerHead);
                }
                if (arrayLength === 0) {
                    $('<span class="red-ui-debug-msg-type-meta">empty</span>').appendTo(headerHead);
                }
                $('<span> ]</span>').appendTo(headerHead);
            }
            if (originalLength > 0) {

                makeExpandable(header,function() {
                    if (!key) {
                        headerHead = $('<span class="red-ui-debug-msg-type-meta red-ui-debug-msg-object-type-header"></span>').text(typeHint||(type+'['+originalLength+']')).appendTo(header);
                    }
                    if (type === 'buffer') {
                        var stringRow = $('<div class="red-ui-debug-msg-string-rows"></div>').appendTo(element);
                        var sr = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(stringRow);
                        var stringEncoding = "";
                        try {
                            stringEncoding = String.fromCharCode.apply(null, new Uint16Array(data))
                        } catch(err) {
                            console.log(err);
                        }
                        $('<pre class="red-ui-debug-msg-type-string"></pre>').text(stringEncoding).appendTo(sr);
                        var bufferOpts = $('<span class="red-ui-debug-msg-buffer-opts"></span>').appendTo(headerHead);
                        var switchFormat = $('<a class="red-ui-button red-ui-button-small" href="#"></a>').text('raw').appendTo(bufferOpts).on("click", function(e) {
                            e.preventDefault();
                            e.stopPropagation();
                            formatBuffer(element,$(this),sourceId,path,true);
                        });
                        formatBuffer(element,switchFormat,sourceId,path,false);

                    }
                    var row;
                    if (fullLength <= 10) {
                        for (i=0;i<fullLength;i++) {
                            row = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(arrayRows);
                            subElements[path+"["+i+"]"] = createObjectElement(
                                data[i],
                                {
                                    key: ""+i,
                                    typeHint: type==='buffer'?'hex':false,
                                    hideKey: false,
                                    path: path+"["+i+"]",
                                    sourceId,
                                    rootPath,
                                    expandPaths,
                                    expandLeafNodes,
                                    ontoggle,
                                    exposeApi,
                                    // tools: tools // Do not pass tools down as we
                                                    // keep them attached to the top-level header
                                    nodeSelector: options.nodeSelector,
                                    enablePinning
                                }
                            ).appendTo(row);
                        }
                    } else {
                        for (i=0;i<fullLength;i+=10) {
                            var minRange = i;
                            row = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(arrayRows);
                            header = $('<span></span>').appendTo(row);
                            $('<i class="fa fa-caret-right red-ui-debug-msg-object-handle"></i> ').appendTo(header);
                            makeExpandable(header, (function() {
                                var min = minRange;
                                var max = Math.min(fullLength-1,(minRange+9));
                                var parent = row;
                                return function() {
                                    for (var i=min;i<=max;i++) {
                                        var row = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(parent);
                                        subElements[path+"["+i+"]"] = createObjectElement(
                                            data[i],
                                            {
                                                key: ""+i,
                                                typeHint: type==='buffer'?'hex':false,
                                                hideKey: false,
                                                path: path+"["+i+"]",
                                                sourceId,
                                                rootPath,
                                                expandPaths,
                                                expandLeafNodes,
                                                ontoggle,
                                                exposeApi,
                                                // tools: tools // Do not pass tools down as we
                                                                // keep them attached to the top-level header
                                                nodeSelector: options.nodeSelector,
                                                enablePinning
                                            }
                                        ).appendTo(row);
                                    }
                                }
                            })(),
                            (function() { var path = path+"["+i+"]"; return function(state) {if (ontoggle) { ontoggle(path,state);}}})(),
                            checkExpanded(strippedKey,expandPaths,{ minRange, maxRange: Math.min(fullLength-1,(minRange+9)), expandLeafNodes}));
                            $('<span class="red-ui-debug-msg-object-key"></span>').html("["+minRange+" &hellip; "+Math.min(fullLength-1,(minRange+9))+"]").appendTo(header);
                        }
                        if (fullLength < originalLength) {
                             $('<div class="red-ui-debug-msg-object-entry collapsed"><span class="red-ui-debug-msg-object-key">['+fullLength+' &hellip; '+originalLength+']</span></div>').appendTo(arrayRows);
                        }
                    }
                },
                function(state) {if (ontoggle) { ontoggle(path,state);}},
                checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
            }
        } else if (typeof obj === 'object') {
            element.addClass('collapsed');
            var data = obj;
            var type = "object";
            if (data.__enc__) {
                data = data.data;
                type = obj.type.toLowerCase();
            }
            var keys = Object.keys(data);
            if (key || keys.length > 0) {
                $('<i class="fa fa-caret-right red-ui-debug-msg-object-handle"></i> ').prependTo(header);
                makeExpandable(header, function() {
                    if (!key) {
                        $('<span class="red-ui-debug-msg-type-meta red-ui-debug-msg-object-type-header"></span>').text(type).appendTo(header);
                    }
                    for (i=0;i<keys.length;i++) {
                        var row = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(element);
                        var newPath = path;
                        if (newPath !== undefined) {
                            if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(keys[i])) {
                                newPath += (newPath.length > 0?".":"")+keys[i];
                            } else {
                                newPath += "[\""+keys[i].replace(/"/,"\\\"")+"\"]"
                            }
                        }
                        subElements[newPath] = createObjectElement(
                            data[keys[i]],
                            {
                                key: keys[i],
                                typeHint: false,
                                hideKey: false,
                                path: newPath,
                                sourceId,
                                rootPath,
                                expandPaths,
                                expandLeafNodes,
                                ontoggle,
                                exposeApi,
                                // tools: tools // Do not pass tools down as we
                                                // keep them attached to the top-level header
                                nodeSelector: options.nodeSelector,
                                enablePinning
                            }
                        ).appendTo(row);
                    }
                    if (keys.length === 0) {
                        $('<div class="red-ui-debug-msg-object-entry red-ui-debug-msg-type-meta collapsed"></div>').text("empty").appendTo(element);
                    }
                },
                function(state) {if (ontoggle) { ontoggle(path,state);}},
                checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
            }
            if (key) {
                $('<span class="red-ui-debug-msg-type-meta"></span>').text(type).appendTo(entryObj);
            } else {
                headerHead = $('<span class="red-ui-debug-msg-object-header"></span>').appendTo(entryObj);
                $('<span>{ </span>').appendTo(headerHead);
                var keysLength = Math.min(keys.length,5);
                for (i=0;i<keysLength;i++) {
                    $('<span class="red-ui-debug-msg-object-key"></span>').text(keys[i]).appendTo(headerHead);
                    $('<span>: </span>').appendTo(headerHead);
                    buildMessageSummaryValue(data[keys[i]]).appendTo(headerHead);
                    if (i < keysLength-1) {
                        $('<span>, </span>').appendTo(headerHead);
                    }
                }
                if (keys.length > keysLength) {
                    $('<span> &hellip;</span>').appendTo(headerHead);
                }
                if (keysLength === 0) {
                    $('<span class="red-ui-debug-msg-type-meta">empty</span>').appendTo(headerHead);
                }
                $('<span> }</span>').appendTo(headerHead);
            }
        } else {
            $('<span class="red-ui-debug-msg-type-other"></span>').text(""+obj).appendTo(entryObj);
        }
        if (exposeApi) {
            element.prop('expand', function() { return function(targetPath, state) {
                if (path === targetPath) {
                    if (header.prop('toggle')) {
                        header.prop('toggle')(state);
                    }
                } else if (subElements[targetPath] && subElements[targetPath].prop('expand') ) {
                    subElements[targetPath].prop('expand')(targetPath,state);
                } else {
                    for (var p in subElements) {
                        if (subElements.hasOwnProperty(p)) {
                            if (targetPath.indexOf(p) === 0) {
                                if (subElements[p].prop('expand') ) {
                                    subElements[p].prop('expand')(targetPath,state);
                                }
                                break;
                            }
                        }
                    }
                }
            }});
        }
        return element;
    }

    function createError(code, message) {
        var e = new Error(message);
        e.code = code;
        return e;
    }

    function normalisePropertyExpression(str,msg) {
        // This must be kept in sync with validatePropertyExpression
        // in editor/js/ui/utils.js

        var length = str.length;
        if (length === 0) {
            throw createError("INVALID_EXPR","Invalid property expression: zero-length");
        }
        var parts = [];
        var start = 0;
        var inString = false;
        var inBox = false;
        var boxExpression = false;
        var quoteChar;
        var v;
        for (var i=0;i<length;i++) {
            var c = str[i];
            if (!inString) {
                if (c === "'" || c === '"') {
                    if (i != start) {
                        throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
                    }
                    inString = true;
                    quoteChar = c;
                    start = i+1;
                } else if (c === '.') {
                    if (i===0) {
                        throw createError("INVALID_EXPR","Invalid property expression: unexpected . at position 0");
                    }
                    if (start != i) {
                        v = str.substring(start,i);
                        if (/^\d+$/.test(v)) {
                            parts.push(parseInt(v));
                        } else {
                            parts.push(v);
                        }
                    }
                    if (i===length-1) {
                        throw createError("INVALID_EXPR","Invalid property expression: unterminated expression");
                    }
                    // Next char is first char of an identifier: a-z 0-9 $ _
                    if (!/[a-z0-9\$\_]/i.test(str[i+1])) {
                        throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
                    }
                    start = i+1;
                } else if (c === '[') {
                    if (i === 0) {
                        throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
                    }
                    if (start != i) {
                        parts.push(str.substring(start,i));
                    }
                    if (i===length-1) {
                        throw createError("INVALID_EXPR","Invalid property expression: unterminated expression");
                    }
                    // Start of a new expression. If it starts with msg it is a nested expression
                    // Need to scan ahead to find the closing bracket
                    if (/^msg[.\[]/.test(str.substring(i+1))) {
                        var depth = 1;
                        var inLocalString = false;
                        var localStringQuote;
                        for (var j=i+1;j<length;j++) {
                            if (/["']/.test(str[j])) {
                                if (inLocalString) {
                                    if (str[j] === localStringQuote) {
                                        inLocalString = false
                                    }
                                } else {
                                    inLocalString = true;
                                    localStringQuote = str[j]
                                }
                            }
                            if (str[j] === '[') {
                                depth++;
                            } else if (str[j] === ']') {
                                depth--;
                            }
                            if (depth === 0) {
                                try {
                                    if (msg) {
                                        parts.push(getMessageProperty(msg, str.substring(i+1,j)))
                                    } else {
                                        parts.push(normalisePropertyExpression(str.substring(i+1,j), msg));
                                    }
                                    inBox = false;
                                    i = j;
                                    start = j+1;
                                    break;
                                } catch(err) {
                                    throw createError("INVALID_EXPR","Invalid expression started at position "+(i+1))
                                }
                            }
                        }
                        if (depth > 0) {
                            throw createError("INVALID_EXPR","Invalid property expression: unmatched '[' at position "+i);
                        }
                        continue;
                    } else if (!/["'\d]/.test(str[i+1])) {
                        // Next char is either a quote or a number
                        throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
                    }
                    start = i+1;
                    inBox = true;
                } else if (c === ']') {
                    if (!inBox) {
                        throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
                    }
                    if (start != i) {
                        v = str.substring(start,i);
                        if (/^\d+$/.test(v)) {
                            parts.push(parseInt(v));
                        } else {
                            throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start);
                        }
                    }
                    start = i+1;
                    inBox = false;
                } else if (c === ' ') {
                    throw createError("INVALID_EXPR","Invalid property expression: unexpected ' ' at position "+i);
                }
            } else {
                if (c === quoteChar) {
                    if (i-start === 0) {
                        throw createError("INVALID_EXPR","Invalid property expression: zero-length string at position "+start);
                    }
                    parts.push(str.substring(start,i));
                    // If inBox, next char must be a ]. Otherwise it may be [ or .
                    if (inBox && !/\]/.test(str[i+1])) {
                        throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start);
                    } else if (!inBox && i+1!==length && !/[\[\.]/.test(str[i+1])) {
                        throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" expression at position "+(i+1));
                    }
                    start = i+1;
                    inString = false;
                }
            }

        }
        if (inBox || inString) {
            throw new createError("INVALID_EXPR","Invalid property expression: unterminated expression");
        }
        if (start < length) {
            parts.push(str.substring(start));
        }
        return parts;
    }

    /**
     * Validate a property expression
     * @param {*} str - the property value
     * @returns {boolean|string} whether the node proprty is valid. `true`: valid `false|String`: invalid
     */
    function validatePropertyExpression(str, opt) {
        try {
            const parts = normalisePropertyExpression(str);
            return true;
        } catch(err) {
            // If the validator has opt, it is a 3.x validator that
            // can return a String to mean 'invalid' and provide a reason
            if (opt) {
                if (opt.label) {
                    return opt.label + ': ' + err.message;
                }
                return err.message;
            }
            // Otherwise, a 2.x returns a false value
            return false;
        }
    }

    /**
     * Checks a typed property is valid according to the type.
     * Returns true if valid.
     * Return String error message if invalid
     * @param {*} propertyType 
     * @param {*} propertyValue 
     * @returns true if valid, String if invalid
     */
    function validateTypedProperty(propertyValue, propertyType, opt) {
        if (propertyValue && /^\${[^}]+}$/.test(propertyValue)) {
            // Allow ${ENV_VAR} value
            return true
        }
        let error;
        if (propertyType === 'json') {
            try {
                JSON.parse(propertyValue);
            } catch(err) {
                error = RED._("validator.errors.invalid-json", {
                    error: err.message
                });
            }
        } else if (propertyType === 'msg' || propertyType === 'flow' || propertyType === 'global' ) {
            // To avoid double label
            const valid = RED.utils.validatePropertyExpression(propertyValue, opt ? {} : null);
            if (valid !== true) {
                error = opt ? valid : RED._("validator.errors.invalid-prop");
            }
        } else if (propertyType === 'num') {
            if (!/^NaN$|^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$|^[+-]?(0b|0B)[01]+$|^[+-]?(0o|0O)[0-7]+$|^[+-]?(0x|0X)[0-9a-fA-F]+$/.test(propertyValue)) {
                error = RED._("validator.errors.invalid-num");
            }
        } else if (propertyType === 'jsonata') {
            try { 
                jsonata(propertyValue)
            } catch(err) {
                error = RED._("validator.errors.invalid-expr", {
                    error: err.message
                });
            }
        }
        if (error) {
            if (opt && opt.label) {
                return opt.label + ': ' + error;
            }
            return error;
        }
        return true;
    }

    function getMessageProperty(msg,expr) {
        var result = null;
        var msgPropParts;

        if (typeof expr === 'string') {
            if (expr.indexOf('msg.')===0) {
                expr = expr.substring(4);
            }
            msgPropParts = normalisePropertyExpression(expr);
        } else {
            msgPropParts = expr;
        }
        var m;
        msgPropParts.reduce(function(obj, key) {
            result = (typeof obj[key] !== "undefined" ? obj[key] : undefined);
            if (result === undefined && obj.hasOwnProperty('type') && obj.hasOwnProperty('data')&& obj.hasOwnProperty('length')) {
                result = (typeof obj.data[key] !== "undefined" ? obj.data[key] : undefined);
            }
            return result;
        }, msg);
        return result;
    }

    function setMessageProperty(msg,prop,value,createMissing) {
        if (typeof createMissing === 'undefined') {
            createMissing = (typeof value !== 'undefined');
        }
        if (prop.indexOf('msg.')===0) {
            prop = prop.substring(4);
        }
        var msgPropParts = normalisePropertyExpression(prop);
        var depth = 0;
        var length = msgPropParts.length;
        var obj = msg;
        var key;
        for (var i=0;i<length-1;i++) {
            key = msgPropParts[i];
            if (typeof key === 'string' || (typeof key === 'number' && !Array.isArray(obj))) {
                if (obj.hasOwnProperty(key)) {
                    obj = obj[key];
                } else if (createMissing) {
                    if (typeof msgPropParts[i+1] === 'string') {
                        obj[key] = {};
                    } else {
                        obj[key] = [];
                    }
                    obj = obj[key];
                } else {
                    return null;
                }
            } else if (typeof key === 'number') {
                // obj is an array
                if (obj[key] === undefined) {
                    if (createMissing) {
                        if (typeof msgPropParts[i+1] === 'string') {
                            obj[key] = {};
                        } else {
                            obj[key] = [];
                        }
                        obj = obj[key];
                    } else {
                        return null;
                    }
                } else {
                    obj = obj[key];
                }
            }
        }
        key = msgPropParts[length-1];
        if (typeof value === "undefined") {
            if (typeof key === 'number' && Array.isArray(obj)) {
                obj.splice(key,1);
            } else {
                delete obj[key]
            }
        } else {
            obj[key] = value;
        }
    }

    function separateIconPath(icon) {
        var result = {module: "", file: ""};
        if (icon) {
            var index = icon.indexOf(RED.settings.apiRootUrl+'icons/');
            if (index === 0) {
                icon = icon.substring((RED.settings.apiRootUrl+'icons/').length);
            }
            var match = /^((?:@[^/]+\/)?[^/]+)\/(.*)$/.exec(icon);
            if (match) {
                result.module = match[1];
                result.file = match[2];
            } else {
                result.file = icon;
            }
        }
        return result;
    }

    function getDefaultNodeIcon(def,node) {
        def = def || {};
        var icon_url;
        if (node && node.type === "subflow") {
            icon_url = "node-red/subflow.svg";
        } else if (typeof def.icon === "function") {
            try {
                icon_url = def.icon.call(node);
            } catch(err) {
                console.log("Definition error: "+def.type+".icon",err);
                icon_url = "arrow-in.svg";
            }
        } else {
            icon_url = def.icon;
        }

        var iconPath = separateIconPath(icon_url);
        if (!iconPath.module) {
            if (def.set) {
                iconPath.module = def.set.module;
            } else {
                // Handle subflow instance nodes that don't have def.set
                iconPath.module = "node-red";
            }
        }
        return iconPath;
    }

    function isIconExists(iconPath) {
        var iconSets = RED.nodes.getIconSets();
        var iconFileList = iconSets[iconPath.module];
        if (iconFileList && iconFileList.indexOf(iconPath.file) !== -1) {
            return true;
        } else {
            return false;
        }
    }

    function getNodeIcon(def,node) {
        def = def || {};
        if (node && node.type === '_selection_') {
            return "font-awesome/fa-object-ungroup";
        } else if (node && node.type === 'group') {
            return "font-awesome/fa-object-group"
        } else if ((node && node.type === 'junction') || (def.type === "junction") ) {
            return "font-awesome/fa-circle-o"
        } else if (def.category === 'config') {
            return RED.settings.apiRootUrl+"icons/node-red/cog.svg"
        } else if ((node && /^_action_:/.test(node.type)) || /^_action_:/.test(def.type)) {
            return "font-awesome/fa-cogs"
        } else if (node && node.type === 'tab') {
            return "red-ui-icons/red-ui-icons-flow"
            // return RED.settings.apiRootUrl+"images/subflow_tab.svg"
        } else if (node && node.type === 'unknown') {
            return RED.settings.apiRootUrl+"icons/node-red/alert.svg"
        } else if (node && node.icon) {
            var iconPath = separateIconPath(node.icon);
            if (isIconExists(iconPath)) {
                if (iconPath.module === "font-awesome") {
                    return node.icon;
                } else {
                    return RED.settings.apiRootUrl+"icons/" + node.icon;
                }
            } else if (iconPath.module !== "font-awesome" && /.png$/i.test(iconPath.file)) {
                iconPath.file = iconPath.file.replace(/.png$/,".svg");
                if (isIconExists(iconPath)) {
                    return RED.settings.apiRootUrl+"icons/" + node.icon.replace(/.png$/,".svg");
                }
            }
        }

        var iconPath = getDefaultNodeIcon(def, node);
        if (isIconExists(iconPath)) {
            if (iconPath.module === "font-awesome") {
                return iconPath.module+"/"+iconPath.file;
            } else {
                return RED.settings.apiRootUrl+"icons/"+iconPath.module+"/"+iconPath.file;
            }
        }

        if (/.png$/i.test(iconPath.file)) {
            var originalFile = iconPath.file;
            iconPath.file = iconPath.file.replace(/.png$/,".svg");
            if (isIconExists(iconPath)) {
                return RED.settings.apiRootUrl+"icons/"+iconPath.module+"/"+iconPath.file;
            }
            iconPath.file = originalFile;
        }

        // This could be a non-core node trying to use a core icon.
        iconPath.module = 'node-red';
        if (isIconExists(iconPath)) {
            return RED.settings.apiRootUrl+"icons/"+iconPath.module+"/"+iconPath.file;
        }
        if (/.png$/i.test(iconPath.file)) {
            iconPath.file = iconPath.file.replace(/.png$/,".svg");
            if (isIconExists(iconPath)) {
                return RED.settings.apiRootUrl+"icons/"+iconPath.module+"/"+iconPath.file;
            }
        }
        if (def.category === 'subflows') {
            return RED.settings.apiRootUrl+"icons/node-red/subflow.svg";
        }
        return RED.settings.apiRootUrl+"icons/node-red/arrow-in.svg";
    }

    function getNodeLabel(node,defaultLabel) {
        defaultLabel = defaultLabel||"";
        var l;
        if (node.type === 'tab') {
            l = node.label || defaultLabel
        } else if (node.type === 'group') {
            l = node.name || defaultLabel
        } else if (node.type === 'junction') {
            l = 'junction'
        } else {
            l = node._def.label;
            try {
                l = (typeof l === "function" ? l.call(node) : l)||defaultLabel;
            } catch(err) {
                console.log("Definition error: "+node.type+".label",err);
                l = defaultLabel;
            }
        }
        return RED.text.bidi.enforceTextDirectionWithUCC(l);
    }

    function getPaletteLabel(nodeType, def) {
        var label = nodeType;
        if (typeof def.paletteLabel !== "undefined") {
            try {
                label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
            } catch(err) {
                console.log("Definition error: "+nodeType+".paletteLabel",err);
            }
        }
        return label
    }

    var nodeColorCache = {};
    function clearNodeColorCache() {
        nodeColorCache = {};
    }

    function getNodeColor(type, def) {
        def = def || {};
        var result = def.color;
        var paletteTheme = RED.settings.theme('palette.theme') || [];
        if (paletteTheme.length > 0) {
            if (!nodeColorCache.hasOwnProperty(type)) {
                nodeColorCache[type] = def.color;
                var l = paletteTheme.length;
                for (var i = 0; i < l; i++ ){
                    var themeRule = paletteTheme[i];
                    if (themeRule.hasOwnProperty('category')) {
                        if (!themeRule.hasOwnProperty('_category')) {
                            themeRule._category = new RegExp(themeRule.category);
                        }
                        if (!themeRule._category.test(def.category)) {
                            continue;
                        }
                    }
                    if (themeRule.hasOwnProperty('type')) {
                        if (!themeRule.hasOwnProperty('_type')) {
                            themeRule._type = new RegExp(themeRule.type);
                        }
                        if (!themeRule._type.test(type)) {
                            continue;
                        }
                    }
                    nodeColorCache[type] = themeRule.color || def.color;
                    break;
                }
            }
            result = nodeColorCache[type];
        }
        if (result) {
            return result;
        } else {
            return "#ddd";
        }
    }

    function addSpinnerOverlay(container,contain) {
        var spinner = $('<div class="red-ui-component-spinner "><img src="red/images/spin.svg"/></div>').appendTo(container);
        if (contain) {
            spinner.addClass('red-ui-component-spinner-contain');
        }
        return spinner;
    }

    function decodeObject(payload,format) {
        if ((format === 'number') && (payload === "NaN")) {
            payload = Number.NaN;
        } else if ((format === 'number') && (payload === "Infinity")) {
            payload = Infinity;
        } else if ((format === 'number') && (payload === "-Infinity")) {
            payload = -Infinity;
        } else if (format === 'Object' || /^(array|set|map)/.test(format) || format === 'boolean' || format === 'number' ) {
            payload = JSON.parse(payload);
        } else if (/error/i.test(format)) {
            payload = JSON.parse(payload);
            payload = (payload.name?payload.name+": ":"")+payload.message;
        } else if (format === 'null') {
            payload = null;
        } else if (format === 'undefined') {
            payload = undefined;
        } else if (/^buffer/.test(format)) {
            var buffer = payload;
            payload = [];
            for (var c = 0; c < buffer.length; c += 2) {
                payload.push(parseInt(buffer.substr(c, 2), 16));
            }
        }
        return payload;
    }

    function parseContextKey(key, defaultStore) {
        var parts = {};
        var m = /^#:\((\S+?)\)::(.*)$/.exec(key);
        if (m) {
            parts.store = m[1];
            parts.key = m[2];
        } else {
            parts.key = key;
            if (defaultStore) {
                parts.store = defaultStore;
            } else if (RED.settings.context) {
                parts.store = RED.settings.context.default;
            }
        }
        return parts;
    }

     /**
      * Create or update an icon element and append it to iconContainer.
      * @param iconUrl - Url of icon.
      * @param iconContainer - Icon container element with red-ui-palette-icon-container class.
      * @param isLarge - Whether the icon size is large.
      */
    function createIconElement(iconUrl, iconContainer, isLarge) {
        // Removes the previous icon when icon was changed.
        var iconElement = iconContainer.find(".red-ui-palette-icon");
        if (iconElement.length !== 0) {
            iconElement.remove();
        }
        var faIconElement = iconContainer.find("i");
        if (faIconElement.length !== 0) {
            faIconElement.remove();
        }

        // Show either icon image or font-awesome icon
        var iconPath = separateIconPath(iconUrl);
        if (iconPath.module === "font-awesome") {
            var fontAwesomeUnicode = RED.nodes.fontAwesome.getIconUnicode(iconPath.file);
            if (fontAwesomeUnicode) {
                var faIconElement = $('<i/>').appendTo(iconContainer);
                var faLarge = isLarge ? "fa-lg " : "";
                faIconElement.addClass("red-ui-palette-icon-fa fa fa-fw " + faLarge + iconPath.file);
                return;
            }
            // If the specified name is not defined in font-awesome, show arrow-in icon.
            iconUrl = RED.settings.apiRootUrl+"icons/node-red/arrow-in.svg"
        } else if (iconPath.module === "red-ui-icons") {
            var redIconElement = $('<i/>').appendTo(iconContainer);
            redIconElement.addClass("red-ui-palette-icon red-ui-icons " + iconPath.file);
            return;
        }
        var imageIconElement = $('<div/>',{class:"red-ui-palette-icon"}).appendTo(iconContainer);
        imageIconElement.css("backgroundImage", "url("+iconUrl+")");
    }

    function createNodeIcon(node, includeLabel) {
        var container = $('<span class="red-ui-node-icon-container">');

        var def = node._def;
        var nodeDiv = $('<div>',{class:"red-ui-node-icon"})
        if (node.type === "_selection_") {
            nodeDiv.addClass("red-ui-palette-icon-selection");
        } else if (node.type === "group") {
            nodeDiv.addClass("red-ui-palette-icon-group");
        } else if (node.type === "junction") {
            nodeDiv.addClass("red-ui-palette-icon-junction");
        } else if (node.type === 'tab') {
            nodeDiv.addClass("red-ui-palette-icon-flow");
        } else {
            var colour = RED.utils.getNodeColor(node.type,def);
            // if (node.type === 'tab') {
            //     colour = "#C0DEED";
            // }
            nodeDiv.css('backgroundColor',colour);
            var borderColor = getDarkerColor(colour);
            if (borderColor !== colour) {
                nodeDiv.css('border-color',borderColor)
            }
        }

        var icon_url = RED.utils.getNodeIcon(def,node);
        RED.utils.createIconElement(icon_url, nodeDiv, true);

        nodeDiv.appendTo(container);

        if (includeLabel) {
            var labelText = RED.utils.getNodeLabel(node,node.name || (node.type+": "+node.id));
            var label = $('<div>',{class:"red-ui-node-label"}).appendTo(container);
            if (labelText) {
                label.text(labelText)
            } else {
                label.html("&nbsp;")
            }
        }
        return container;
    }

    function getDarkerColor(c) {
        var r,g,b;
        if (/^#[a-f0-9]{6}$/i.test(c)) {
            r = parseInt(c.substring(1, 3), 16);
            g = parseInt(c.substring(3, 5), 16);
            b = parseInt(c.substring(5, 7), 16);
        } else if (/^#[a-f0-9]{3}$/i.test(c)) {
            r = parseInt(c.substring(1, 2)+c.substring(1, 2), 16);
            g = parseInt(c.substring(2, 3)+c.substring(2, 3), 16);
            b = parseInt(c.substring(3, 4)+c.substring(3, 4), 16);
        } else {
            return c;
        }
        var l = 0.3 * r/255 + 0.59 * g/255 + 0.11 * b/255 ;
        r = Math.max(0,r-50);
        g = Math.max(0,g-50);
        b = Math.max(0,b-50);
        var s = ((r<<16) + (g<<8) + b).toString(16);
        return '#'+'000000'.slice(0, 6-s.length)+s;
    }

    function parseModuleList(list) {
        list = list || ["*"];
        return list.map(function(rule) {
            var m = /^(.+?)(?:@(.*))?$/.exec(rule);
            var wildcardPos = m[1].indexOf("*");
            wildcardPos = wildcardPos===-1?Infinity:wildcardPos;

            return {
                module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"),
                version: m[2],
                wildcardPos: wildcardPos
            }
        })
    }

    function checkAgainstList(module,version,list) {
        for (var i=0;i<list.length;i++) {
            var rule = list[i];
            if (rule.module.test(module)) {
                // Without a full semver library in the editor,
                // we skip the version check.
                // Not ideal - but will get caught in the runtime
                // if the user tries to install.
                return rule;
            }
        }
    }

    function checkModuleAllowed(module,version,allowList,denyList) {
        if (!allowList && !denyList) {
            // Default to allow
            return true;
        }
        if (allowList.length === 0 && denyList.length === 0) {
            return true;
        }

        var allowedRule = checkAgainstList(module,version,allowList);
        var deniedRule = checkAgainstList(module,version,denyList);
        // console.log("A",allowedRule)
        // console.log("D",deniedRule)

        if (allowedRule && !deniedRule) {
            return true;
        }
        if (!allowedRule && deniedRule) {
            return false;
        }
        if (!allowedRule && !deniedRule) {
            return true;
        }
        if (allowedRule.wildcardPos !== deniedRule.wildcardPos) {
            return allowedRule.wildcardPos > deniedRule.wildcardPos
        } else {
            // First wildcard in same position.
            // Go with the longer matching rule. This isn't going to be 100%
            // right, but we are deep into edge cases at this point.
            return allowedRule.module.toString().length > deniedRule.module.toString().length
        }
        return false;
    }

    function getBrowserInfo() {
        var r = {}
        try {
            var ua = navigator.userAgent;
            r.ua = ua;
            r.browser = /Edge\/\d+/.test(ua) ? 'ed' : /MSIE 9/.test(ua) ? 'ie9' : /MSIE 10/.test(ua) ? 'ie10' : /MSIE 11/.test(ua) ? 'ie11' : /MSIE\s\d/.test(ua) ? 'ie?' : /rv\:11/.test(ua) ? 'ie11' : /Firefox\W\d/.test(ua) ? 'ff' : /Chrom(e|ium)\W\d|CriOS\W\d/.test(ua) ? 'gc' : /\bSafari\W\d/.test(ua) ? 'sa' : /\bOpera\W\d/.test(ua) ? 'op' : /\bOPR\W\d/i.test(ua) ? 'op' : typeof MSPointerEvent !== 'undefined' ? 'ie?' : '';
            r.os = /Windows NT 10/.test(ua) ? "win10" : /Windows NT 6\.0/.test(ua) ? "winvista" : /Windows NT 6\.1/.test(ua) ? "win7" : /Windows NT 6\.\d/.test(ua) ? "win8" : /Windows NT 5\.1/.test(ua) ? "winxp" : /Windows NT [1-5]\./.test(ua) ? "winnt" : /Mac/.test(ua) ? "mac" : /Linux/.test(ua) ? "linux" : /X11/.test(ua) ? "nix" : "";
            r.touch = 'ontouchstart' in document.documentElement;
            r.mobile = /IEMobile|Windows Phone|Lumia/i.test(ua) ? 'w' : /iPhone|iP[oa]d/.test(ua) ? 'i' : /Android/.test(ua) ? 'a' : /BlackBerry|PlayBook|BB10/.test(ua) ? 'b' : /Mobile Safari/.test(ua) ? 's' : /webOS|Mobile|Tablet|Opera Mini|\bCrMo\/|Opera Mobi/i.test(ua) ? 1 : 0;
            r.tablet = /Tablet|iPad/i.test(ua);
            r.ie = /MSIE \d|Trident.*rv:/.test(navigator.userAgent);
            r.android = /android/i.test(navigator.userAgent);
        } catch (error) { }
        return r;
    }

    return {
        createObjectElement: createObjectElement,
        getMessageProperty: getMessageProperty,
        setMessageProperty: setMessageProperty,
        normalisePropertyExpression: normalisePropertyExpression,
        validatePropertyExpression: validatePropertyExpression,
        separateIconPath: separateIconPath,
        getDefaultNodeIcon: getDefaultNodeIcon,
        getNodeIcon: getNodeIcon,
        getNodeLabel: getNodeLabel,
        getNodeColor: getNodeColor,
        getPaletteLabel: getPaletteLabel,
        clearNodeColorCache: clearNodeColorCache,
        addSpinnerOverlay: addSpinnerOverlay,
        decodeObject: decodeObject,
        parseContextKey: parseContextKey,
        createIconElement: createIconElement,
        sanitize: sanitize,
        renderMarkdown: renderMarkdown,
        createNodeIcon: createNodeIcon,
        getDarkerColor: getDarkerColor,
        parseModuleList: parseModuleList,
        checkModuleAllowed: checkModuleAllowed,
        getBrowserInfo: getBrowserInfo,
        validateTypedProperty: validateTypedProperty
    }
})();
;/**
 * 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.
 **/
(function($) {

/**
 * options:
 *   - addButton : boolean|string - text for add label, default 'add'
 *   - buttons : array - list of custom buttons (objects with fields 'id', 'label', 'icon', 'title', 'click')
 *   - height : number|'auto'
 *   - resize : function - called when list as a whole is resized
 *   - resizeItem : function(item) - called to resize individual item
 *   - sortable : boolean|string - string is the css selector for handle
 *   - sortItems : function(items) - when order of items changes
 *   - connectWith : css selector of other sortables
 *   - removable : boolean - whether to display delete button on items
 *   - addItem : function(row,index,itemData) - when an item is added
 *   - removeItem : function(itemData) - called when an item is removed
 *   - filter : function(itemData) - called for each item to determine if it should be shown
 *   - sort : function(itemDataA,itemDataB) - called to sort items
 *   - scrollOnAdd : boolean - whether to scroll to newly added items
 * methods:
 *   - addItem(itemData)
 *   - insertItemAt : function(data,index) - add an item at the specified index
 *   - removeItem(itemData, detach) - remove the item. Optionally detach to preserve any event handlers on the item's label
 *   - getItemAt(index)
 *   - indexOf(itemData)
 *   - width(width)
 *   - height(height)
 *   - items()
 *   - empty()
 *   - filter(filter)
 *   - sort(sort)
 *   - length()
 */
    $.widget( "nodered.editableList", {
        _create: function() {
            var that = this;

            this.element.addClass('red-ui-editableList-list');
            this.uiWidth = this.element.width();
            this.uiContainer = this.element
                .wrap( "<div>" )
                .parent();

            if (this.options.header) {
                this.options.header.addClass("red-ui-editableList-header");
                this.borderContainer = this.uiContainer.wrap("<div>").parent();
                this.borderContainer.prepend(this.options.header);
                this.topContainer = this.borderContainer.wrap("<div>").parent();
            } else {
                this.topContainer = this.uiContainer.wrap("<div>").parent();
            }
            this.topContainer.addClass('red-ui-editableList');
            if (this.options.class) {
                this.topContainer.addClass(this.options.class);
            }

            var buttons = this.options.buttons || [];

            if (this.options.addButton !== false) {
                var addLabel, addTitle;
                if (typeof this.options.addButton === 'string') {
                    addLabel = this.options.addButton
                } else {
                    if (RED && RED._) {
                        addLabel = RED._("editableList.add");
                        addTitle = RED._("editableList.addTitle");
                    } else {
                        addLabel = 'add';
                        addTitle = 'add new item';
                    }
                }
                buttons.unshift({
                    label: addLabel,
                    icon: "fa fa-plus",
                    click: function(evt) {
                        that.addItem({});
                    },
                    title: addTitle
                });
            }

            buttons.forEach(function(button) {
                var element = $('<button type="button" class="red-ui-button red-ui-button-small red-ui-editableList-addButton" style="margin-top: 4px; margin-right: 5px;"></button>')
                    .appendTo(that.topContainer)
                    .on("click", function(evt) {
                        evt.preventDefault();
                        if (button.click !== undefined) {
                            button.click(evt);
                        }
                    });

                if (button.id) {
                    element.attr("id", button.id);
                }
                if (button.title) {
                    element.attr("title", button.title);
                }
                if (button.icon) {
                    element.append($("<i></i>").attr("class", button.icon));
                }
                if (button.label) {
                    element.append($("<span></span>").text(" " + button.label));
                }
            });

            if (this.element.css("position") === "absolute") {
                ["top","left","bottom","right"].forEach(function(s) {
                    var v = that.element.css(s);
                    if (v!=="auto" && v!=="") {
                        that.topContainer.css(s,v);
                        that.uiContainer.css(s,"0");
                        if (s === "top" && that.options.header) {
                            that.uiContainer.css(s,"20px")
                        }
                        that.element.css(s,'auto');
                    }
                })
                this.element.css("position","static");
                this.topContainer.css("position","absolute");
                this.uiContainer.css("position","absolute");

            }
            if (this.options.header) {
                this.borderContainer.addClass("red-ui-editableList-border");
            } else {
                this.uiContainer.addClass("red-ui-editableList-border");
            }
            this.uiContainer.addClass("red-ui-editableList-container");

            this.uiHeight = this.element.height();

            this.activeFilter = this.options.filter||null;
            this.activeSort = this.options.sort||null;
            this.scrollOnAdd = this.options.scrollOnAdd;
            if (this.scrollOnAdd === undefined) {
                this.scrollOnAdd = true;
            }
            var minHeight = this.element.css("minHeight");
            if (minHeight !== '0px') {
                this.uiContainer.css("minHeight",minHeight);
                this.element.css("minHeight",0);
            }
            var maxHeight = this.element.css("maxHeight");
            if (maxHeight !== '0px') {
                this.uiContainer.css("maxHeight",maxHeight);
                this.element.css("maxHeight",null);
            }
            if (this.options.height !== 'auto') {
                this.uiContainer.css("overflow-y","auto");
                if (!isNaN(this.options.height)) {
                    this.uiHeight = this.options.height;
                }
            }
            this.element.height('auto');

            var attrStyle = this.element.attr('style');
            var m;
            if ((m = /width\s*:\s*(\d+%)/i.exec(attrStyle)) !== null) {
                this.element.width('100%');
                this.uiContainer.width(m[1]);
            }
            if (this.options.sortable) {
                var isCanceled = false; // Flag to track if an item has been canceled from being dropped into a different list
                var noDrop = false; // Flag to track if an item is being dragged into a different list
                var handle = (typeof this.options.sortable === 'string')?
                                this.options.sortable :
                                ".red-ui-editableList-item-handle";
                var sortOptions = {
                    axis: "y",
                    update: function( event, ui ) {
                        // dont trigger update if the item is being canceled
                        const targetList = $(event.target);
                        const draggedItem = ui.item;
                        const draggedItemParent = draggedItem.parent();
                        if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) {
                            noDrop = true;
                        }
                        if (isCanceled || noDrop) {
                            return;
                        }
                        if (that.options.sortItems) {
                            that.options.sortItems(that.items());
                        }
                    },
                    handle:handle,
                    cursor: "move",
                    tolerance: "pointer",
                    forcePlaceholderSize:true,
                    placeholder: "red-ui-editabelList-item-placeholder",
                    start: function (event, ui) {
                        isCanceled = false;
                        ui.placeholder.height(ui.item.height() - 4);
                        ui.item.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead?
                    },
                    stop: function (event, ui) {
                        ui.item.css('cursor', 'auto');
                    },
                    receive: function (event, ui) {
                        if (ui.item.hasClass("red-ui-editableList-item-constrained")) {
                            isCanceled = true;
                            $(ui.sender).sortable('cancel');
                        }
                    },
                    over: function (event, ui) {
                        // if the dragged item is constrained, prevent it from being dropped into a different list
                        const targetList = $(event.target);
                        const draggedItem = ui.item;
                        const draggedItemParent = draggedItem.parent();
                        if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) {
                            noDrop = true;
                            draggedItem.css('cursor', 'no-drop'); // TODO: this doesn't seem to work, use a class instead?
                        } else {
                            noDrop = false;
                            draggedItem.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead?
                        }
                    }
                };
                if (this.options.connectWith) {
                    sortOptions.connectWith = this.options.connectWith;
                }

                this.element.sortable(sortOptions);
            }

            this._resize();

            // this.menu = this._createMenu(this.types, function(v) { that.type(v) });
            // this.type(this.options.default||this.types[0].value);
        },
        _resize: function() {
            var currentFullHeight = this.topContainer.height();
            var innerHeight = this.uiContainer.height();
            var delta = currentFullHeight - innerHeight;
            if (this.uiHeight !== 0) {
                this.uiContainer.height(this.uiHeight-delta);
            }
            if (this.options.resize) {
                this.options.resize();
            }
            if (this.options.resizeItem) {
                var that = this;
                this.element.children().each(function(i) {
                    that.options.resizeItem($(this).children(".red-ui-editableList-item-content"),i);
                });
            }
        },
        _destroy: function() {
            if (this.topContainer) {
                var tc = this.topContainer;
                delete this.topContainer;
                tc.remove();
            }
        },
        _refreshFilter: function() {
            var that = this;
            var count = 0;
            if (!this.activeFilter) {
                return this.element.children().show();
            }
            var items = this.items();
            items.each(function (i,el) {
                var data = el.data('data');
                try {
                    if (that.activeFilter(data)) {
                        el.parent().show();
                        count++;
                    } else {
                        el.parent().hide();
                    }
                } catch(err) {
                    console.log(err);
                    el.parent().show();
                    count++;
                }
            });
            return count;
        },
        _refreshSort: function() {
            if (this.activeSort) {
                var items = this.element.children();
                var that = this;
                items.sort(function(A,B) {
                    return that.activeSort($(A).children(".red-ui-editableList-item-content").data('data'),$(B).children(".red-ui-editableList-item-content").data('data'));
                });
                $.each(items,function(idx,li) {
                    that.element.append(li);
                })
            }
        },
        width: function(desiredWidth) {
            this.uiWidth = desiredWidth;
            this._resize();
        },
        height: function(desiredHeight) {
            this.uiHeight = desiredHeight;
            this._resize();
        },
        getItemAt: function(index) {
            var items = this.items();
            if (index >= 0 && index < items.length) {
                return $(items[index]).data('data');
            } else {
                return;
            }
        },
        indexOf: function(data) {
            var items = this.items();
            for (var i=0;i<items.length;i++) {
                if ($(items[i]).data('data') === data) {
                    return i
                }
            }
            return -1
        },
        insertItemAt: function(data,index) {
            var that = this;
            data = data || {};
            var li = $('<li>');
            var row = $('<div/>').addClass("red-ui-editableList-item-content").appendTo(li);
            row.data('data',data);
            if (this.options.sortable === true) {
                $('<i class="red-ui-editableList-item-handle fa fa-bars"></i>').appendTo(li);
                li.addClass("red-ui-editableList-item-sortable");
            }
            if (this.options.removable) {
                var deleteButton = $('<a/>',{href:"#",class:"red-ui-editableList-item-remove red-ui-button red-ui-button-small"}).appendTo(li);
                $('<i/>',{class:"fa fa-remove"}).appendTo(deleteButton);
                li.addClass("red-ui-editableList-item-removable");
                deleteButton.on("click", function(evt) {
                    evt.preventDefault();
                    var data = row.data('data');
                    li.addClass("red-ui-editableList-item-deleting")
                    li.fadeOut(300, function() {
                        $(this).remove();
                        if (that.options.removeItem) {
                            that.options.removeItem(data);
                        }
                    });
                });
            }
            var added = false;
            if (this.activeSort) {
                var items = this.items();
                var skip = false;
                items.each(function(i,el) {
                    if (added) { return }
                    var itemData = el.data('data');
                    if (that.activeSort(data,itemData) < 0) {
                         li.insertBefore(el.closest("li"));
                         added = true;
                    }
                });
            }
            if (!added) {
                if (index <= 0) {
                    li.prependTo(this.element);
                } else if (index > that.element.children().length-1) {
                    li.appendTo(this.element);
                } else {
                    li.insertBefore(this.element.children().eq(index));
                }
            }
            if (this.options.addItem) {
                var index = that.element.children().length-1;
                // setTimeout(function() {
                    that.options.addItem(row,index,data);
                    if (that.activeFilter) {
                        try {
                            if (!that.activeFilter(data)) {
                                li.hide();
                            }
                        } catch(err) {
                        }
                    }

                    if (!that.activeSort && that.scrollOnAdd) {
                        setTimeout(function() {
                            that.uiContainer.scrollTop(that.element.height());
                        },0);
                    }
                // },0);
            }
        },
        addItem: function(data) {
            this.insertItemAt(data,this.element.children().length)
        },
        addItems: function(items) {
            for (var i=0; i<items.length;i++) {
                this.addItem(items[i]);
            }
        },
        removeItem: function(data,detach) {
            var items = this.element.children().filter(function(f) {
                return data === $(this).children(".red-ui-editableList-item-content").data('data');
            });
            if (detach) {
                items.detach();
            } else {
                items.remove();
            }
            if (this.options.removeItem) {
                this.options.removeItem(data);
            }
        },
        items: function() {
            return this.element.children().map(function(i) { return $(this).children(".red-ui-editableList-item-content"); });
        },
        empty: function() {
            this.element.empty();
            this.uiContainer.scrollTop(0);
        },
        filter: function(filter) {
            if (filter !== undefined) {
                this.activeFilter = filter;
            }
            return this._refreshFilter();
        },
        sort: function(sort) {
            if (sort !== undefined) {
                this.activeSort = sort;
            }
            return this._refreshSort();
        },
        length: function() {
            return this.element.children().length;
        },
        show: function(item) {
            var items = this.element.children().filter(function(f) {
                return item === $(this).children(".red-ui-editableList-item-content").data('data');
            });
            if (items.length > 0) {
                this.uiContainer.scrollTop(this.uiContainer.scrollTop()+items.position().top)
            }
        },
        getItem: function(li) {
            var el = li.children(".red-ui-editableList-item-content");
            if (el.length) {
                return el.data('data');
            } else {
                return null;
            }
        },
        cancel: function() {
            this.element.sortable("cancel");
        }
    });
})(jQuery);
;/**
 * 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.
 **/
(function($) {

/**
 * options:
 *   - data : array - initial items to display in tree
 *   - multi : boolean - if true, .selected will return an array of results
 *                       otherwise, returns the first selected item
 *   - sortable: boolean/string - TODO: see editableList
 *   - selectable: boolean - default true - whether individual items can be selected
 *   - rootSortable: boolean - if 'sortable' is set, then setting this to
 *                             false, prevents items being sorted to the
 *                             top level of the tree
 *   - autoSelect: boolean - default true - triggers item selection when navigating
 *                           list by keyboard. If the list has checkboxed items
 *                           you probably want to set this to false
 *
 * methods:
 *   - data(items) - clears existing items and replaces with new data
 *   - clearSelection - clears the selected items
 *   - filter(filterFunc) - filters the tree using the provided function
 * events:
 *   - treelistselect : function(event, item) {}
 *   - treelistconfirm : function(event,item) {}
 *   - treelistchangeparent: function(event,item, oldParent, newParent) {}
 *
 * data:
 * [
 *     {
 *         label: 'Local', // label for the item
 *         sublabel: 'Local', // a sub-label for the item
 *         icon: 'fa fa-rocket', // (optional) icon for the item
 *         checkbox: true/false, // (optional) if present, display checkbox accordingly
 *         radio: 'group-name',  // (optional) if present, display radio box - using group-name to set radio group
 *         selected: true/false, // (optional) whether the item is selected or not
 *         children: [] | function(done,item) // (optional) an array of child items, or a function
 *                                       // that will call the `done` callback with an array
 *                                       // of child items
 *         expanded: true/false, // show the child items by default
 *         deferBuild: true/false, // don't build any ui elements for the item's children
 *                                    until it is expanded by the user.
 *         element: // custom dom element to use for the item - ignored if `label` is set
 *         collapsible: true/false, // prevent a parent item from being collapsed. default true.
 *     }
 * ]
 *
 * var treeList = $("<div>").css({width: "100%", height: "100%"}).treeList({data:[...]})
 * treeList.on('treelistselect', function(e,item) { console.log(item)})
 * treeList.treeList('data',[ ... ] )
 *
 *
 * After `data` has been added to the tree, each item is augmented the following
 * properties and functions:
 *
 *   item.parent - set to the parent item
 *   item.depth - the depth in the tree (0 == root)
 *   item.treeList.container
 *   item.treeList.label - the label element for the item
 *   item.treeList.parentList  - the editableList instance this item is in
 *   item.treeList.remove(detach) - removes the item from the tree. Optionally detach to preserve any event handlers on the item's label
 *   item.treeList.makeLeaf(detachChildElements) - turns an element with children into a leaf node,
 *                                                 removing the UI decoration etc.
 *                                                 detachChildElements - any children with custom
 *                                                 elements will be detached rather than removed
 *                                                 so jQuery event handlers are preserved in case
 *                                                 the child elements need to be reattached later
 *   item.treeList.makeParent(children) - turns an element into a parent node, adding the necessary
 *                                        UI decoration.
 *   item.treeList.insertChildAt(newItem,position,select) - adds a child item an the specified position.
 *                                                          Optionally selects the item after adding.
 *   item.treeList.addChild(newItem,select) - appends a child item.
 *                                            Optionally selects the item after adding.
 *   item.treeList.expand(done) - expands the parent item to show children. Optional 'done' callback.
 *   item.treeList.collapse() - collapse the parent item to hide children.
 *   item.treeList.sortChildren(sortFunction) - does a one-time sort of the children using sortFunction
 *   item.treeList.replaceElement(element) - replace the custom element for the item
 *
 *
 */

    $.widget( "nodered.treeList", {
        _create: function() {
            var that = this;
            var autoSelect = true;
            if (that.options.autoSelect === false) {
                autoSelect = false;
            }
            this.element.addClass('red-ui-treeList');
            this.element.attr("tabIndex",0);
            var wrapper = $('<div>',{class:'red-ui-treeList-container'}).appendTo(this.element);
            this.element.on('keydown', function(evt) {
                var focussed = that._topList.find(".focus").parent().data('data');
                if (!focussed && (evt.keyCode === 40 || evt.keyCode === 38)) {
                    if (that._data[0]) {
                        if (autoSelect) {
                            that.select(that._data[0]);
                        } else {
                            that._topList.find(".focus").removeClass("focus")
                        }
                        that._data[0].treeList.label.addClass('focus')
                    }
                    return;
                }
                var target;
                switch(evt.keyCode) {
                    case 32: // SPACE
                    case 13: // ENTER
                        if (!that.options.selectable) { return }
                        if (evt.altKey || evt.ctrlKey || evt.metaKey || evt.shiftKey) {
                            return
                        }
                        evt.preventDefault();
                        evt.stopPropagation();
                        if (focussed.checkbox) {
                            focussed.treeList.checkbox.trigger("click");
                        } else if (focussed.radio) {
                            focussed.treeList.radio.trigger("click");
                        } else if (focussed.children) {
                            if (focussed.treeList.container.hasClass("expanded")) {
                                focussed.treeList.collapse()
                            } else {
                                focussed.treeList.expand()
                            }
                        } else {
                            that._trigger("confirm",null,focussed)
                        }
                    break;
                    case 37: // LEFT
                        evt.preventDefault();
                        evt.stopPropagation();
                        if (focussed.children&& focussed.treeList.container.hasClass("expanded")) {
                            focussed.treeList.collapse()
                        } else if (focussed.parent) {
                            target = focussed.parent;
                        }
                    break;
                    case 38: // UP
                        evt.preventDefault();
                        evt.stopPropagation();
                        target = that._getPreviousSibling(focussed);
                        if (target) {
                            target = that._getLastDescendant(target);
                        }
                        if (!target && focussed.parent) {
                            target = focussed.parent;
                        }
                    break;
                    case 39: // RIGHT
                        evt.preventDefault();
                        evt.stopPropagation();
                        if (focussed.children) {
                            if (!focussed.treeList.container.hasClass("expanded")) {
                                focussed.treeList.expand()
                            }
                        }
                    break
                    case 40: //DOWN
                        evt.preventDefault();
                        evt.stopPropagation();
                        if (focussed.children && Array.isArray(focussed.children) && focussed.children.length > 0 && focussed.treeList.container.hasClass("expanded")) {
                            target = focussed.children[0];
                        } else {
                            target = that._getNextSibling(focussed);
                            while (!target && focussed.parent) {
                                focussed = focussed.parent;
                                target = that._getNextSibling(focussed);
                            }
                        }
                    break
                }
                if (target) {
                    if (autoSelect) {
                        that.select(target);
                    } else {
                        that._topList.find(".focus").removeClass("focus")
                    }
                    target.treeList.label.addClass('focus')
                }
            });
            this._data = [];
            this._items = {};
            this._selected = new Set();
            this._topList = $('<ol class="red-ui-treeList-list">').css({
                position:'absolute',
                top:0,
                left:0,
                right:0,
                bottom:0
            }).appendTo(wrapper);

            var topListOptions = {
                addButton: false,
                scrollOnAdd: false,
                height: '100%',
                addItem: function(container,i,item) {
                    that._addSubtree(that._topList,container,item,0);
                }
            };
            if (this.options.header) {
                topListOptions.header = this.options.header;
            }
            if (this.options.rootSortable !== false && !!this.options.sortable) {
                topListOptions.sortable = this.options.sortable;
                topListOptions.connectWith = '.red-ui-treeList-sortable';
                this._topList.addClass('red-ui-treeList-sortable');
            }
            this._topList.editableList(topListOptions)


            if (this.options.data) {
                this.data(this.options.data);
            }
        },
        _getLastDescendant: function(item) {
            // Gets the last visible descendant of the item
            if (!item.children || !item.treeList.container.hasClass("expanded") || item.children.length === 0) {
                return item;
            }
            return this._getLastDescendant(item.children[item.children.length-1]);
        },
        _getPreviousSibling: function(item) {
            var candidates;
            if (!item.parent) {
                candidates = this._data;
            } else {
                candidates = item.parent.children;
            }
            var index = candidates.indexOf(item);
            if (index === 0) {
                return null;
            } else {
                return candidates[index-1];
            }
        },
        _getNextSibling: function(item) {
            var candidates;
            if (!item.parent) {
                candidates = this._data;
            } else {
                candidates = item.parent.children;
            }
            var index = candidates.indexOf(item);
            if (index === candidates.length - 1) {
                return null;
            } else {
                return candidates[index+1];
            }
        },
        _addChildren: function(container,parent,children,depth,onCompleteChildren) {
            var that = this;
            var subtree = $('<ol class="red-ui-treeList-list">').appendTo(container).editableList({
                connectWith: ".red-ui-treeList-sortable",
                sortable: that.options.sortable,
                addButton: false,
                scrollOnAdd: false,
                height: 'auto',
                addItem: function(container,i,item) {
                    that._addSubtree(subtree,container,item,depth+1);
                },
                sortItems: function(data) {
                    var children = [];
                    var reparented = [];
                    data.each(function() {
                        var child = $(this).data('data');
                        children.push(child);
                        var evt = that._fixDepths(parent,child);
                        if (evt) {
                            reparented.push(evt);
                        }
                    })
                    if (Array.isArray(parent.children)) {
                        parent.children = children;
                    }
                    reparented.forEach(function(evt) {
                        that._trigger("changeparent",null,evt);
                    });
                    that._trigger("sort",null,parent);
                },
                filter: parent.treeList.childFilter
            });
            if (!!that.options.sortable) {
                subtree.addClass('red-ui-treeList-sortable');
            }
            var sliceSize = 30;
            var index = 0;
            var addSlice = function() {
                var start = index;
                for (var i=0;i<sliceSize;i++) {
                    index = start+i;
                    if (index === children.length) {
                        setTimeout(function() {
                            if (onCompleteChildren) {
                                onCompleteChildren();
                            }
                        },10);
                        return;
                    }
                    children[index].parent = parent;
                    subtree.editableList('addItem',children[index])
                }
                index++;
                if (index < children.length) {
                    setTimeout(function() {
                        addSlice();
                    },10);
                }
            }
            addSlice();
            subtree.hide()
            return subtree;
        },
        _fixDepths: function(parent,child) {
            // If child has just been moved into parent in the UI
            // this will fix up the internal data structures to match.
            // The calling function must take care of getting child
            // into the parent.children array. The rest is up to us.
            var that = this;
            var reparentedEvent = null;
            if (child.parent !== parent) {
                reparented = true;
                var oldParent = child.parent;
                child.parent = parent;
                reparentedEvent = {
                    item: child,
                    old: oldParent,
                }
            }
            if (child.depth !== parent.depth+1) {
                child.depth = parent.depth+1;
                // var labelPaddingWidth = ((child.gutter ? child.gutter[0].offsetWidth + 2 : 0) + (child.depth * 20));
                var labelPaddingWidth = (((child.gutter&&!child.gutter.hasClass("red-ui-treeList-gutter-float"))?child.gutter.width()+2:0)+(child.depth*20));
                child.treeList.labelPadding.width(labelPaddingWidth+'px');
                if (child.element) {
                    $(child.element).css({
                        width: "calc(100% - "+(labelPaddingWidth+20+(child.icon?20:0))+"px)"
                    })
                }
                // This corrects all child item depths
                if (child.children && Array.isArray(child.children)) {
                    child.children.forEach(function(item) {
                        that._fixDepths(child,item);
                    })
                }
            }
            return reparentedEvent;
        },
        _initItem: function(item,depth) {
            if (item.treeList) {
                return;
            }
            var that = this;
            this._items[item.id] = item;
            item.treeList = {};
            item.depth = depth;
            item.treeList.remove = function(detach) {
                if (item.treeList.parentList) {
                    item.treeList.parentList.editableList('removeItem',item,detach);
                }
                if (item.parent) {
                    var index = item.parent.children.indexOf(item);
                    item.parent.children.splice(index,1)
                    that._trigger("sort",null,item.parent);
                }
                that._selected.delete(item);
                delete item.treeList;
                delete that._items[item.id];
                if(item.depth === 0) {
                    for(var key in that._items) {
                        if (that._items.hasOwnProperty(key)) {
                            var child = that._items[key];
                            if(child.parent && child.parent.id === item.id) {
                                delete that._items[key].treeList;
                                delete that._items[key];
                            }
                        }
                    }
                    that._data = that._data.filter(function(data) { return data.id !== item.id})
                }
            }
            item.treeList.insertChildAt = function(newItem,position,select) {
                newItem.parent = item;
                item.children.splice(position,0,newItem);
                var processChildren = function(parent,i) {
                    that._initItem(i,parent.depth+1)
                    i.parent = parent;
                    if (i.children && typeof i.children !== 'function') {
                        i.children.forEach(function(item) {
                            processChildren(i, item, parent.depth+2)
                        });
                    }
                }
                processChildren(item,newItem);

                if (!item.deferBuild && item.treeList.childList) {
                    item.treeList.childList.editableList('insertItemAt',newItem,position)
                    if (select) {
                        setTimeout(function() {
                            that.select(newItem)
                        },100);
                    }
                    that._trigger("sort",null,item);

                    if (that.activeFilter) {
                        that.filter(that.activeFilter);
                    }
                }
            }
            item.treeList.addChild = function(newItem,select) {
                item.treeList.insertChildAt(newItem,item.children.length,select);
            }
            item.treeList.expand = function(done) {
                if (!item.children) {
                    if (done) { done(false) }
                    return;
                }
                if (!item.treeList.container) {
                    item.expanded = true;
                    if (done) { done(false) }
                    return;
                }
                var container = item.treeList.container;
                if (container.hasClass("expanded")) {
                    if (done) { done(false) }
                    return;
                }

                if (!container.hasClass("built") && (item.deferBuild || typeof item.children === 'function')) {
                    container.addClass('built');
                    var childrenAdded = false;
                    var spinner;
                    var startTime = 0;
                    var started = Date.now();
                    var completeBuild = function(children) {
                        childrenAdded = true;
                        item.treeList.childList = that._addChildren(container,item,children,depth, function() {
                            if (done) { done(true) }
                            that._trigger("childrenloaded",null,item)
                        });
                        var delta = Date.now() - startTime;
                        if (delta < 400) {
                            setTimeout(function() {
                                item.treeList.childList.slideDown('fast');
                                if (spinner) {
                                    spinner.remove();
                                }
                            },400-delta);
                        } else {
                            item.treeList.childList.slideDown('fast');
                            if (spinner) {
                                spinner.remove();
                            }
                        }
                        item.expanded = true;
                    }
                    if (typeof item.children === 'function') {
                        item.children(completeBuild,item);
                    } else {
                        delete item.deferBuild;
                        completeBuild(item.children);
                    }
                    if (!childrenAdded) {
                        startTime = Date.now();
                        spinner = $('<div class="red-ui-treeList-spinner">').css({
                            "background-position": (35+depth*20)+'px 50%'
                        }).appendTo(container);
                    }

                } else {
                    if (that._loadingData || item.children.length > 20) {
                        item.treeList.childList.show();
                    } else {
                        item.treeList.childList.slideDown('fast');
                    }
                    item.expanded = true;
                    if (done) { done(!that._loadingData) }
                }
                container.addClass("expanded");
            }
            item.treeList.collapse = function() {
                if (item.collapsible === false) {
                    return
                }
                if (!item.children) {
                    return;
                }
                item.expanded = false;
                if (item.treeList.container) {
                    if (item.children.length < 20) {
                        item.treeList.childList.slideUp('fast');
                    } else {
                        item.treeList.childList.hide();
                    }
                    item.treeList.container.removeClass("expanded");
                }
            }
            item.treeList.sortChildren = function(sortFunc) {
                if (!item.children) {
                    return;
                }
                item.children.sort(sortFunc);
                if (item.treeList.childList) {
                    // Do a one-off sort of the list, which means calling sort twice:
                    // 1. first with the desired sort function
                    item.treeList.childList.editableList('sort',sortFunc);
                    // 2. and then with null to remove it
                    item.treeList.childList.editableList('sort',null);
                }
            }
            item.treeList.replaceElement = function (element) {
                if (item.element) {
                    if (item.treeList.container) {
                        $(item.element).remove();
                        $(element).appendTo(item.treeList.label);
                        // using the JQuery Object, the gutter width will
                        // be wrong when the element is reattached the second time
                        var labelPaddingWidth = (item.gutter ? item.gutter[0].offsetWidth + 2 : 0) + (item.depth * 20);

                        $(element).css({
                            width: "calc(100% - "+(labelPaddingWidth+20+(item.icon?20:0))+"px)"
                        })
                    }
                    item.element = element;
                }
            }

            if (item.children && typeof item.children !== "function") {
                item.children.forEach(function(i) {
                    that._initItem(i,depth+1);
                })
            }
        },
        _addSubtree: function(parentList, container, item, depth) {
            var that = this;
            this._initItem(item,depth);
            // item.treeList = {};
            // item.treeList.depth = depth;
            item.treeList.container = container;

            item.treeList.parentList = parentList;

            var label = $("<div>",{class:"red-ui-treeList-label"});
            label.appendTo(container);
            item.treeList.label = label;
            if (item.class) {
                label.addClass(item.class);
            }
            if (item.gutter) {
                item.gutter.css({
                    position: 'absolute'
                }).appendTo(label)

            }

            var labelPaddingWidth = ((item.gutter&&!item.gutter.hasClass("red-ui-treeList-gutter-float"))?item.gutter.width()+2:0)+(depth*20);

            item.treeList.labelPadding = $('<span>').css({
                display: "inline-block",
                "flex-shrink": 0,
                width:  labelPaddingWidth+'px'
            }).appendTo(label);

            label.on('mouseover',function(e) { that._trigger('itemmouseover',e,item); })
            label.on('mouseout',function(e) { that._trigger('itemmouseout',e,item); })
            label.on('mouseenter',function(e) { that._trigger('itemmouseenter',e,item); })
            label.on('mouseleave',function(e) { that._trigger('itemmouseleave',e,item); })

            item.treeList.makeLeaf = function(detachChildElements) {
                if (!treeListIcon.children().length) {
                    // Already a leaf
                    return
                }
                if (detachChildElements && item.children) {
                    var detachChildren = function(item) {
                        if (item.children) {
                            item.children.forEach(function(child) {
                                if (child.element) {
                                    child.element.detach();
                                }
                                if (child.gutter) {
                                    child.gutter.detach();
                                }
                                detachChildren(child);
                            });
                        }
                    }
                    detachChildren(item);
                }
                treeListIcon.empty();
                if (!item.deferBuild) {
                    item.treeList.childList.remove();
                    delete item.treeList.childList;
                }
                label.off("click.red-ui-treeList-expand");
                treeListIcon.off("click.red-ui-treeList-expand");
                delete item.children;
                container.removeClass("expanded");
                delete item.expanded;
            }
            item.treeList.makeParent = function(children) {
                if (treeListIcon.children().length) {
                    // Already a parent because we've got the angle-right icon
                    return;
                }
                $('<i class="fa fa-angle-right" />').toggleClass("hide",item.collapsible === false).appendTo(treeListIcon);
                treeListIcon.on("click.red-ui-treeList-expand", function(e) {
                        e.stopPropagation();
                        e.preventDefault();
                        if (container.hasClass("expanded")) {
                            item.treeList.collapse();
                        } else {
                            item.treeList.expand();
                        }
                    });
                // $('<span class="red-ui-treeList-icon"><i class="fa fa-folder-o" /></span>').appendTo(label);
                label.on("click.red-ui-treeList-expand", function(e) {
                    if (container.hasClass("expanded")) {
                        if (item.hasOwnProperty('selected') || label.hasClass("selected")) {
                            item.treeList.collapse();
                        }
                    } else {
                        item.treeList.expand();
                    }
                })
                if (!item.children) {
                    item.children = children||[];
                    item.treeList.childList = that._addChildren(container,item,item.children,depth);
                }
            }

            var treeListIcon = $('<span class="red-ui-treeList-icon"></span>').appendTo(label);
            if (item.children) {
                item.treeList.makeParent();
            }

            if (item.checkbox) {
                var selectWrapper = $('<span class="red-ui-treeList-icon"></span>');
                var cb = $('<input class="red-ui-treeList-checkbox" type="checkbox">').prop('checked',item.selected).appendTo(selectWrapper);
                cb.on('click', function(e) {
                    e.stopPropagation();
                });
                cb.on('change', function(e) {
                    item.selected = this.checked;
                    if (item.selected) {
                        that._selected.add(item);
                    } else {
                        that._selected.delete(item);
                    }
                    label.toggleClass("selected",this.checked);
                    that._trigger("select",e,item);
                })
                if (!item.children) {
                    label.on("click", function(e) {
                        e.stopPropagation();
                        cb.trigger("click");
                        that._topList.find(".focus").removeClass("focus")
                        label.addClass('focus')
                    })
                }
                item.treeList.select = function(v) {
                    if (v !== item.selected) {
                        cb.trigger("click");
                    }
                }
                item.treeList.checkbox = cb;
                selectWrapper.appendTo(label)
            } else if (item.radio) {
                var selectWrapper = $('<span class="red-ui-treeList-icon"></span>');
                var cb = $('<input class="red-ui-treeList-radio" type="radio">').prop('name', item.radio).prop('checked',item.selected).appendTo(selectWrapper);
                cb.on('click', function(e) {
                    e.stopPropagation();
                });
                cb.on('change', function(e) {
                    item.selected = this.checked;
                    that._selected.forEach(function(selectedItem) {
                        if (selectedItem.radio === item.radio) {
                            selectedItem.treeList.label.removeClass("selected");
                            selectedItem.selected = false;
                            that._selected.delete(selectedItem);
                        }
                    })
                    if (item.selected) {
                        that._selected.add(item);
                    } else {
                        that._selected.delete(item);
                    }
                    label.toggleClass("selected",this.checked);
                    that._trigger("select",e,item);
                })
                if (!item.children) {
                    label.on("click", function(e) {
                        e.stopPropagation();
                        cb.trigger("click");
                        that._topList.find(".focus").removeClass("focus")
                        label.addClass('focus')
                    })
                }
                item.treeList.select = function(v) {
                    if (v !== item.selected) {
                        cb.trigger("click");
                    }
                }
                selectWrapper.appendTo(label)
                item.treeList.radio = cb;
            } else {
                label.on("click", function(e) {
                    if (!that.options.multi) {
                        that.clearSelection();
                    }
                    label.addClass("selected");
                    that._selected.add(item);
                    that._topList.find(".focus").removeClass("focus")
                    label.addClass('focus')

                    that._trigger("select",e,item)
                })
                label.on("dblclick", function(e) {
                    that._topList.find(".focus").removeClass("focus")
                    label.addClass('focus')
                    if (!item.children) {
                        that._trigger("confirm",e,item);
                    }
                })
                item.treeList.select = function(v) {
                    if (!that.options.multi) {
                        that.clearSelection();
                    }
                    label.toggleClass("selected",v);
                    if (v) {
                        that._selected.add(item);
                        that._trigger("select",null,item)
                    } else {
                        that._selected.delete(item);
                    }
                    that.reveal(item);
                }
            }
            label.toggleClass("selected",!!item.selected);
            if (item.selected) {
                that._selected.add(item);
            }
            if (item.icon) {
                if (typeof item.icon === "string") {
                    $('<span class="red-ui-treeList-icon"><i class="'+item.icon+'" /></span>').appendTo(label);
                } else {
                    $('<span class="red-ui-treeList-icon">').appendTo(label).append(item.icon);
                }
            }
            if (item.hasOwnProperty('label') || item.hasOwnProperty('sublabel')) {
                if (item.hasOwnProperty('label')) {
                    $('<span class="red-ui-treeList-label-text"></span>').text(item.label).appendTo(label);
                }
                if (item.hasOwnProperty('sublabel')) {
                    $('<span class="red-ui-treeList-sublabel-text"></span>').text(item.sublabel).appendTo(label);
                }

            } else if (item.element) {
                $(item.element).appendTo(label);
                $(item.element).css({
                    width: "calc(100% - "+(labelPaddingWidth+20+(item.icon?20:0))+"px)"
                })
            }
            if (item.children) {
                if (Array.isArray(item.children) && !item.deferBuild) {
                    item.treeList.childList = that._addChildren(container,item,item.children,depth);
                }
                if (item.expanded) {
                    item.treeList.expand();
                }
            }
            // label.appendTo(container);
        },
        empty: function() {
            this._topList.editableList('empty');
        },
        data: function(items) {
            var that = this;
            if (items !== undefined) {
                this._data = items;
                this._items = {};
                this._topList.editableList('empty');
                this._loadingData = true;
                for (var i=0; i<items.length;i++) {
                    this._topList.editableList('addItem',items[i]);
                }
                setTimeout(function() {
                    delete that._loadingData;
                },200);
                this._trigger("select")

            } else {
                return this._data;
            }
        },
        show: function(item, done) {
            if (typeof item === "string") {
                item = this._items[item]
            }
            if (!item) {
                return;
            }
            var that = this;
            var stack = [];
            var i = item;
            while(i) {
                stack.unshift(i);
                i = i.parent;
            }
            var isOpening = false;
            var handleStack = function(opening) {
                isOpening = isOpening ||opening
                var item = stack.shift();
                if (stack.length === 0) {
                    setTimeout(function() {
                        that.reveal(item);
                        if (done) { done(); }
                    },isOpening?200:0);
                } else {
                    item.treeList.expand(handleStack)
                }
            }
            handleStack();
        },
        reveal: function(item) {
            if (typeof item === "string") {
                item = this._items[item]
            }
            if (!item) {
                return;
            }
            var listOffset = this._topList.offset().top;
            var itemOffset = item.treeList.label.offset().top;
            var scrollTop = this._topList.parent().scrollTop();
            itemOffset -= listOffset+scrollTop;
            var treeHeight = this._topList.parent().height();
            var itemHeight = item.treeList.label.outerHeight();
            if (itemOffset < itemHeight/2) {
                this._topList.parent().scrollTop(scrollTop+itemOffset-itemHeight/2-itemHeight)
            } else if (itemOffset+itemHeight > treeHeight) {
                this._topList.parent().scrollTop(scrollTop+((itemOffset+2.5*itemHeight)-treeHeight));
            }
        },
        select: function(item, triggerEvent, deselectExisting) {
            var that = this;
            if (!this.options.multi && deselectExisting !== false) {
                this.clearSelection();
            }
            if (Array.isArray(item)) {
                item.forEach(function(i) {
                    that.select(i,triggerEvent,false);
                })
                return;
            }
            if (typeof item === "string") {
                item = this._items[item]
            }
            if (!item) {
                return;
            }
            // this.show(item.id);
            item.selected = true;
            this._selected.add(item);

            if (item.treeList.label) {
                item.treeList.label.addClass("selected");
            }

            that._topList.find(".focus").removeClass("focus");

            if (triggerEvent !== false) {
                this._trigger("select",null,item)
            }
        },
        clearSelection: function() {
            this._selected.forEach(function(item) {
                item.selected = false;
                if (item.treeList.checkbox) {
                    item.treeList.checkbox.prop('checked',false)
                }
                if (item.treeList.label) {
                    item.treeList.label.removeClass("selected")
                }
            });
            this._selected.clear();
        },
        selected: function() {
            var selected = [];
            this._selected.forEach(function(item) {
                selected.push(item);
            })
            if (this.options.multi) {
                return selected;
            }
            if (selected.length) {
                return selected[0]
            } else {
                // TODO: This may be a bug.. it causes the call to return itself
                // not undefined.
                return undefined;
            }
        },
        filter: function(filterFunc) {
            this.activeFilter = filterFunc;
            var totalCount = 0;
            var filter = function(item) {
                var matchCount = 0;
                if (filterFunc && filterFunc(item)) {
                    matchCount++;
                    totalCount++;
                }
                var childCount = 0;
                if (item.children && typeof item.children !== "function") {
                    if (item.treeList.childList) {
                        childCount = item.treeList.childList.editableList('filter', filter);
                    } else {
                        item.treeList.childFilter = filter;
                        if (filterFunc) {
                            item.children.forEach(function(i) {
                                if (filter(i)) {
                                    childCount++;
                                }
                            })

                        }
                    }
                    matchCount += childCount;
                    if (filterFunc && childCount > 0) {
                        setTimeout(function() {
                            item.treeList.expand();
                        },10);
                    }
                }
                if (!filterFunc) {
                    totalCount++;
                    return true
                }
                return matchCount > 0
            }
            this._topList.editableList('filter', filter);
            return totalCount;
        },
        get: function(id) {
            return this._items[id] || null;
        }
    });

})(jQuery);
;/**
 * 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.
 **/
(function($) {
    $.widget( "nodered.checkboxSet", {
        _create: function() {
            var that = this;
            this.uiElement = this.element.wrap( "<span>" ).parent();
            this.uiElement.addClass("red-ui-checkboxSet");
            if (this.options.parent) {
                this.parent = this.options.parent;
                this.parent.checkboxSet('addChild',this.element);
            }
            this.children = [];
            this.partialFlag = false;
            this.stateValue = 0;
            var initialState = this.element.prop('checked');
            this.states = [
                $('<span class="red-ui-checkboxSet-option hide"><i class="fa fa-square-o"></i></span>').appendTo(this.uiElement),
                $('<span class="red-ui-checkboxSet-option hide"><i class="fa fa-check-square-o"></i></span>').appendTo(this.uiElement),
                $('<span class="red-ui-checkboxSet-option hide"><i class="fa fa-minus-square-o"></i></span>').appendTo(this.uiElement)
            ];
            if (initialState) {
                this.states[1].show();
            } else {
                this.states[0].show();
            }

            this.element.on("change", function() {
                if (this.checked) {
                    that.states[0].hide();
                    that.states[1].show();
                    that.states[2].hide();
                } else {
                    that.states[1].hide();
                    that.states[0].show();
                    that.states[2].hide();
                }
                var isChecked = this.checked;
                that.children.forEach(function(child) {
                    child.checkboxSet('state',isChecked,false,true);
                })
            })
            this.uiElement.on("click", function(e) {
                e.stopPropagation();
                // state returns null for a partial state. Clicking on that should
                // result in false.
                that.state((that.state()===false)?true:false);
            })
            if (this.parent) {
                this.parent.checkboxSet('updateChild',this);
            }
        },
        _destroy: function() {
            if (this.parent) {
                this.parent.checkboxSet('removeChild',this.element);
            }
        },
        addChild: function(child) {
            var that = this;
            this.children.push(child);
        },
        removeChild: function(child) {
            var index = this.children.indexOf(child);
            if (index > -1) {
                this.children.splice(index,1);
            }
        },
        updateChild: function(child) {
            var checkedCount = 0;
            this.children.forEach(function(c,i) {
                if (c.checkboxSet('state') === true) {
                    checkedCount++;
                }
            });
            if (checkedCount === 0) {

                this.state(false,true);
            } else if (checkedCount === this.children.length) {
                this.state(true,true);
            } else {
                this.state(null,true);
            }
        },
        disable: function() {
            this.uiElement.addClass('disabled');
        },
        state: function(state,suppressEvent,suppressParentUpdate) {

            if (arguments.length === 0) {
                return this.partialFlag?null:this.element.is(":checked");
            } else {
                this.partialFlag = (state === null);
                var trueState = this.partialFlag||state;
                this.element.prop('checked',trueState);
                if (state === true) {
                    this.states[0].hide();
                    this.states[1].show();
                    this.states[2].hide();
                } else if (state === false) {
                    this.states[2].hide();
                    this.states[1].hide();
                    this.states[0].show();
                } else if (state === null) {
                    this.states[0].hide();
                    this.states[1].hide();
                    this.states[2].show();
                }
                if (!suppressEvent) {
                    this.element.trigger('change',null);
                }
                if (!suppressParentUpdate && this.parent) {
                    this.parent.checkboxSet('updateChild',this);
                }
            }
        }
    })

})(jQuery);
;/**
 * 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.
 **/
RED.menu = (function() {

    var menuItems = {};
    let menuItemCount = 0

    function createMenuItem(opt) {
        var item;

        if (opt !== null && opt.id) {
            var themeSetting = RED.settings.theme("menu."+opt.id);
            if (themeSetting === false) {
                return null;
            }
        }

        function setInitialState() {
            var savedStateActive = RED.settings.get("menu-" + opt.id);
            if (opt.setting) {
                // May need to migrate pre-0.17 setting

                if (savedStateActive !== null) {
                    RED.settings.set(opt.setting,savedStateActive);
                    RED.settings.remove("menu-" + opt.id);
                } else {
                    savedStateActive = RED.settings.get(opt.setting);
                }
            }
            if (savedStateActive) {
                link.addClass("active");
                triggerAction(opt.id,true);
            } else if (savedStateActive === false) {
                link.removeClass("active");
                triggerAction(opt.id,false);
            } else if (opt.hasOwnProperty("selected")) {
                if (opt.selected) {
                    link.addClass("active");
                } else {
                    link.removeClass("active");
                }
                triggerAction(opt.id,opt.selected);
            }
        }

        if (opt === null) {
            item = $('<li class="red-ui-menu-divider"></li>');
        } else {
            item = $('<li></li>');
            if (!opt.id) {
                opt.id = 'red-ui-menu-item-'+(menuItemCount++)
            }
            if (opt.group) {
                item.addClass("red-ui-menu-group-"+opt.group);
            }
            var linkContent = '<a '+(opt.id?'id="'+opt.id+'" ':'')+'tabindex="-1" href="#">';
            if (opt.toggle) {
                linkContent += '<i class="fa fa-square'+(opt.direction!=='right'?" pull-left":"")+'"></i>';
                linkContent += '<i class="fa fa-check-square'+(opt.direction!=='right'?" pull-left":"")+'"></i>';

            }
            if (opt.icon !== undefined) {
                if (/\.(png|svg)/.test(opt.icon)) {
                    linkContent += '<img src="'+opt.icon+'"/> ';
                } else {
                    linkContent += '<i class="'+(opt.icon?opt.icon:'" style="display: inline-block;"')+'"></i> ';
                }
            }
            let label = opt.label
            if (!opt.label && typeof opt.onselect === 'string') {
                label = RED.actions.getLabel(opt.onselect)
            }
            if (opt.sublabel) {
                linkContent += '<span class="red-ui-menu-label-container"><span class="red-ui-menu-label">'+label+'</span>'+
                               '<span class="red-ui-menu-sublabel">'+opt.sublabel+'</span></span>'
            } else {
                linkContent += '<span class="red-ui-menu-label"><span>'+label+'</span></span>'
            }

            linkContent += '</a>';

            var link = $(linkContent).appendTo(item);
            opt.link = link;
            if (typeof opt.onselect === 'string' || opt.shortcut) {
                var shortcut = opt.shortcut || RED.keyboard.getShortcut(opt.onselect);
                if (shortcut && shortcut.key) {
                    opt.shortcutSpan = $('<span class="red-ui-popover-key">'+RED.keyboard.formatKey(shortcut.key, true)+'</span>').appendTo(link.find(".red-ui-menu-label"));
                }
            }

            menuItems[opt.id] = opt;

            if (opt.onselect) {
                link.on("click", function(e) {
                    e.preventDefault();
                    if ($(this).parent().hasClass("disabled")) {
                        return;
                    }
                    if (opt.toggle) {
                        if (opt.toggle === true) {
                            setSelected(opt.id, !isSelected(opt.id));
                        } else {
                            setSelected(opt.id, true);
                        }
                    } else {
                        triggerAction(opt.id);
                    }
                });
                if (opt.toggle) {
                    setInitialState();
                }
            } else if (opt.href) {
                link.attr("target","_blank").attr("href",opt.href);
            } else if (!opt.options) {
                item.addClass("disabled");
                link.on("click", function(event) {
                    event.preventDefault();
                });
            }
            if (opt.options) {
                item.addClass("red-ui-menu-dropdown-submenu"+(opt.direction!=='right'?" pull-left":""));
                var submenu = $('<ul id="'+opt.id+'-submenu" class="red-ui-menu-dropdown"></ul>').appendTo(item);
                var hasIcons = false
                var hasSubmenus = false

                for (var i=0;i<opt.options.length;i++) {

                    if (opt.options[i]) {
                        if (opt.onpreselect && opt.options[i].onpreselect === undefined) {
                            opt.options[i].onpreselect = opt.onpreselect
                        }
                        if (opt.onpostselect && opt.options[i].onpostselect === undefined) {
                            opt.options[i].onpostselect = opt.onpostselect
                        }
                        opt.options[i].direction = opt.direction
                        hasIcons = hasIcons || (opt.options[i].icon);
                        hasSubmenus = hasSubmenus || (opt.options[i].options);
                    }

                    var li = createMenuItem(opt.options[i]);
                    if (li) {
                        li.appendTo(submenu);
                    }
                }
                if (!hasIcons) {
                    submenu.addClass("red-ui-menu-dropdown-noicons")
                }
                if (hasSubmenus) {
                    submenu.addClass("red-ui-menu-dropdown-submenus")
                }


            }
            if (opt.disabled) {
                item.addClass("disabled");
            }
            if (opt.visible === false) {
                item.addClass("hide");
            }
        }


        return item;

    }
    function createMenu(options) {
        var topMenu = $("<ul/>",{class:"red-ui-menu red-ui-menu-dropdown pull-right"});
        if (options.direction) {
            topMenu.addClass("red-ui-menu-dropdown-direction-"+options.direction)
        }
        if (options.id) {
            topMenu.attr({id:options.id+"-submenu"});
            var menuParent = $("#"+options.id);
            if (menuParent.length === 1) {
                topMenu.insertAfter(menuParent);
                menuParent.on("click", function(evt) {
                    evt.stopPropagation();
                    evt.preventDefault();
                    if (topMenu.is(":visible")) {
                        $(document).off("click.red-ui-menu");
                        topMenu.hide();
                    } else {
                        $(document).on("click.red-ui-menu", function(evt) {
                            $(document).off("click.red-ui-menu");
                            activeMenu = null;
                            topMenu.hide();
                        });
                        $(".red-ui-menu.red-ui-menu-dropdown").hide();
                        topMenu.show();
                    }
                })
            }
        }

        var lastAddedSeparator = false;
        var hasSubmenus = false;
        var hasIcons = false;
        for (var i=0;i<options.options.length;i++) {
            var opt = options.options[i];
            if (opt) {
                if (options.onpreselect && opt.onpreselect === undefined) {
                    opt.onpreselect = options.onpreselect
                }
                if (options.onpostselect && opt.onpostselect === undefined) {
                    opt.onpostselect = options.onpostselect
                }
                opt.direction = options.direction || 'left'
            }
            if (opt !== null || !lastAddedSeparator) {
                hasIcons = hasIcons || (opt && opt.icon);
                hasSubmenus = hasSubmenus || (opt && opt.options);
                var li = createMenuItem(opt);
                if (li) {
                    li.appendTo(topMenu);
                    lastAddedSeparator = (opt === null);
                }
            }
        }
        if (!hasIcons) {
            topMenu.addClass("red-ui-menu-dropdown-noicons")
        }
        if (hasSubmenus) {
            topMenu.addClass("red-ui-menu-dropdown-submenus")
        }
        return topMenu;
    }

    function triggerAction(id, args) {
        var opt = menuItems[id];
        var callback = opt.onselect;
        if (opt.onpreselect) {
            opt.onpreselect.call(opt,args)
        }
        if (typeof opt.onselect === 'string') {
            callback = RED.actions.get(opt.onselect);
        }
        if (callback) {
            callback.call(opt,args);
        } else {
            console.log("No callback for",id,opt.onselect);
        }
        if (opt.onpostselect) {
            opt.onpostselect.call(opt,args)
        }
    }

    function isSelected(id) {
        return $("#" + id).hasClass("active");
    }

    function setSelected(id,state) {
        var alreadySet = false;
        if (isSelected(id) == state) {
            alreadySet = true;
        }
        var opt = menuItems[id];
        if (state) {
            $("#"+id).addClass("active");
        } else {
            $("#"+id).removeClass("active");
        }
        if (opt) {
            if (opt.toggle && typeof opt.toggle === "string") {
                if (state) {
                    for (var m in menuItems) {
                        if (menuItems.hasOwnProperty(m)) {
                            var mi = menuItems[m];
                            if (mi.id != opt.id && opt.toggle == mi.toggle) {
                                setSelected(mi.id,false);
                            }
                        }
                    }
                }
            }
            if (!alreadySet && opt.onselect) {
                triggerAction(opt.id,state);
            }
            if (!opt.local && !alreadySet) {
                RED.settings.set(opt.setting||("menu-"+opt.id), state);
            }
        }
    }

    function toggleSelected(id) {
        setSelected(id,!isSelected(id));
    }

    function setDisabled(id,state) {
        if (state) {
            $("#"+id).parent().addClass("disabled");
        } else {
            $("#"+id).parent().removeClass("disabled");
        }
    }

    function setVisible(id,state) {
        if (!state) {
            $("#"+id).parent().addClass("hide");
        } else {
            $("#"+id).parent().removeClass("hide");
        }
    }

    function addItem(id,opt) {
        var item = createMenuItem(opt);
        if (opt !== null && opt.group) {
            var groupItems = $("#"+id+"-submenu").children(".red-ui-menu-group-"+opt.group);
            if (groupItems.length === 0) {
                item.appendTo("#"+id+"-submenu");
            } else {
                for (var i=0;i<groupItems.length;i++) {
                    var groupItem = groupItems[i];
                    var label = $(groupItem).find(".red-ui-menu-label").html();
                    if (opt.label < label) {
                        $(groupItem).before(item);
                        break;
                    }
                }
                if (i === groupItems.length) {
                    item.appendTo("#"+id+"-submenu");
                }
            }
        } else {
            item.appendTo("#"+id+"-submenu");
        }
    }
    function removeItem(id) {
        $("#"+id).parent().remove();
    }

    function setAction(id,action) {
        var opt = menuItems[id];
        if (opt) {
            opt.onselect = action;
        }
    }

    function refreshShortcuts() {
        for (var id in menuItems) {
            if (menuItems.hasOwnProperty(id)) {
                var opt = menuItems[id];
                if (typeof opt.onselect === "string" && opt.shortcutSpan) {
                    opt.shortcutSpan.remove();
                    delete opt.shortcutSpan;
                    var shortcut = RED.keyboard.getShortcut(opt.onselect);
                    if (shortcut && shortcut.key) {
                        opt.shortcutSpan = $('<span class="red-ui-popover-key">'+RED.keyboard.formatKey(shortcut.key, true)+'</span>').appendTo(opt.link.find(".red-ui-menu-label"));
                    }
                }
            }
        }
    }

    return {
        init: createMenu,
        setSelected: setSelected,
        isSelected: isSelected,
        toggleSelected: toggleSelected,
        setDisabled: setDisabled,
        setVisible: setVisible,
        addItem: addItem,
        removeItem: removeItem,
        setAction: setAction,
        refreshShortcuts: refreshShortcuts
    }
})();
;/**
 * 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.
 **/


RED.panels = (function() {

    function createPanel(options) {
        var container = options.container || $("#"+options.id);
        var children = container.children();
        if (children.length !== 2) {
            console.log(options.id);
            throw new Error("Container must have exactly two children");
        }
        var vertical = (!options.dir || options.dir === "vertical");
        container.addClass("red-ui-panels");
        if (!vertical) {
            container.addClass("red-ui-panels-horizontal");
        }

        $(children[0]).addClass("red-ui-panel");
        $(children[1]).addClass("red-ui-panel");

        var separator = $('<div class="red-ui-panels-separator"></div>').insertAfter(children[0]);
        var startPosition;
        var panelSizes = [];
        var modifiedSizes = false;
        var panelRatio = 0.5;
        separator.draggable({
                axis: vertical?"y":"x",
                containment: container,
                scroll: false,
                start:function(event,ui) {
                    startPosition = vertical?ui.position.top:ui.position.left;

                    panelSizes = [
                        vertical?$(children[0]).height():$(children[0]).width(),
                        vertical?$(children[1]).height():$(children[1]).width()
                    ];
                },
                drag: function(event,ui) {
                    var size = vertical?container.height():container.width();
                    var delta = (vertical?ui.position.top:ui.position.left)-startPosition;
                    var newSizes = [panelSizes[0]+delta,panelSizes[1]-delta];
                    if (vertical) {
                        $(children[0]).height(newSizes[0]);
                        // $(children[1]).height(newSizes[1]);
                        ui.position.top -= delta;
                    } else {
                        $(children[0]).width(newSizes[0]);
                        // $(children[1]).width(newSizes[1]);
                        ui.position.left -= delta;
                    }
                    if (options.resize) {
                        options.resize(newSizes[0],newSizes[1]);
                    }
                    panelRatio = newSizes[0]/(size-8);
                },
                stop:function(event,ui) {
                    modifiedSizes = true;
                }
        });

        var panel = {
            ratio: function(ratio) {
                if (ratio === undefined) {
                    return panelRatio;
                }
                panelRatio = ratio;
                modifiedSizes = true;
                if (ratio === 0 || ratio === 1) {
                    separator.hide();
                } else {
                    separator.show();
                }
                if (vertical) {
                    panel.resize(container.height());
                } else {
                    panel.resize(container.width());
                }
            },
            resize: function(size) {
                var panelSizes;
                if (vertical) {
                    panelSizes = [$(children[0]).outerHeight(),$(children[1]).outerHeight()];
                    container.height(size);
                } else {
                    panelSizes = [$(children[0]).outerWidth(),$(children[1]).outerWidth()];
                    container.width(size);
                }
                if (modifiedSizes) {
                    var topPanelSize = panelRatio*(size-8);
                    var bottomPanelSize = size - topPanelSize - 8;
                    panelSizes = [topPanelSize,bottomPanelSize];
                    if (vertical) {
                        $(children[0]).outerHeight(panelSizes[0]);
                        // $(children[1]).outerHeight(panelSizes[1]);
                    } else {
                        $(children[0]).outerWidth(panelSizes[0]);
                        // $(children[1]).outerWidth(panelSizes[1]);
                    }
                }
                if (options.resize) {
                    if (vertical) {
                        panelSizes = [$(children[0]).height(),$(children[1]).height()];
                    } else {
                        panelSizes = [$(children[0]).width(),$(children[1]).width()];
                    }
                    options.resize(panelSizes[0],panelSizes[1]);
                }
            }
        }
        return panel;
    }

    return {
        create: createPanel
    }
})();
;/**
 * 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.
 **/
/*
 * RED.popover.create(options) - create a popover callout box
 * RED.popover.tooltip(target,content, action) - add a tooltip to an element
 * RED.popover.menu(options) - create a dropdown menu
 * RED.popover.panel(content) - create a dropdown container element
 */


/*
 * RED.popover.create(options)
 *
 *  options
 *    - target : DOM element - the element to target with the popover
 *    - direction : string - position of the popover relative to target
 *                  'top', 'right'(default), 'bottom', 'left', 'inset-[top,right,bottom,left]'
 *    - trigger : string - what triggers the popover to be displayed
 *                  'hover' - display when hovering the target
 *                  'click' - display when target is clicked
 *                  'modal' - hmm not sure, need to find where we use that mode
 *    - content : string|function - contents of the popover. If a string, handled
 *                                  as raw HTML, so take care.
 *                                  If a function, can return a String to be added
 *                                  as text (not HTML), or a DOM element to append
 *    - delay : object - sets show/hide delays after mouseover/out events
 *                  { show: 750, hide: 50 }
 *    - autoClose : number - delay before closing the popover in some cases
 *                     if trigger is click - delay after mouseout
 *                     else if trigger not hover/modal - delay after showing
 *    - width : number - width of popover, default 'auto'
 *    - maxWidth : number - max width of popover, default 'auto'
 *    - size : string - scale of popover. 'default', 'small'
 *    - offset : number - px offset from target
 *    - tooltip : boolean - if true, clicking on popover closes it
 *    - class : string - optional css class to apply to popover
 *    - interactive : if trigger is 'hover' and this is set to true, allow the mouse
 *                    to move over the popover without hiding it.
 *
 * Returns the popover object with the following properties/functions:
 *   properties:
 *    - element : DOM element - the popover dom element
 *   functions:
 *    - setContent(content) - change the popover content. This only works if the
 *                            popover is not currently displayed. It does not
 *                            change the content of a visible popover.
 *    - open(instant) - show the popover. If 'instant' is true, don't fade in
 *    - close(instant) - hide the popover. If 'instant' is true, don't fade out
 *    - move(options) - move the popover. The options parameter can take many
 *                      of the options detailed above including:
 *                       target,direction,content,width,offset
 *                      Other settings probably won't work because we haven't needed to change them
 */

/*
 * RED.popover.tooltip(target,content, action)
 *
 *  - target : DOM element - the element to apply the tooltip to
 *  - content : string - the text of the tooltip
 *  - action : string - *optional* the name of an Action this tooltip is tied to
 *                      For example, it 'target' is a button that triggers a particular action.
 *                      The tooltip will include the keyboard shortcut for the action
 *                      if one is defined
 *
 */

/*
 * RED.popover.menu(options)
 *
 *  options
 *    - options : array - list of menu options - see below for format
 *    - width : number - width of the menu. Default: 'auto'
 *    - class : string - class to apply to the menu container
 *    - maxHeight : number - maximum height of menu before scrolling items. Default: none
 *    - onselect : function(item) - called when a menu item is selected, if that item doesn't
 *                                  have its own onselect function
 *    - onclose : function(cancelled) - called when the menu is closed
 *    - disposeOnClose : boolean - by default, the menu is discarded when it closes
 *                                 and mustbe rebuilt to redisplay. Setting this to 'false'
 *                                 keeps the menu on the DOM so it can be shown again.
 *
 *  Menu Options array:
 *  [
 *      label : string|DOM element - the label of the item. Can be custom DOM element
 *      onselect : function - called when the item is selected
 *  ]
 *
 * Returns the menu object with the following functions:
 *
 *  - options([menuItems]) - if menuItems is undefined, returns the current items.
 *                           otherwise, sets the current menu items
 *  - show(opts) - shows the menu. `opts` is an object of options. See  RED.popover.panel.show(opts)
 *                 for the full list of options. In most scenarios, this just needs:
 *                  - target : DOM element - the element to display the menu below
 *  - hide(cancelled) - hide the menu
 */

/*
 * RED.popover.panel(content)
 *  Create a UI panel that can be displayed relative to any target element.
 *  Handles auto-closing when mouse clicks outside the panel
 *
 *  - 'content' - DOM element to display in the panel
 *
 * Returns the panel object with the following functions:
 *
 *  properties:
 *    - container : DOM element - the panel element
 *
 *  functions:
 *    - show(opts) - show the panel.
 *       opts:
 *          - onclose : function - called when the panel closes
 *          - closeButton : DOM element - if the panel is closeable by a click of a button,
 *                                        by providing a reference to it here, we can
 *                                        handle the events properly to hide the panel
 *          - target : DOM element - the element to display the panel relative to
 *          - align : string - should the panel align to the left or right edge of target
 *                             default: 'right'
 *          - offset : Array - px offset to apply from the target. [width, height]
 *          - dispose : boolean - whether the panel should be removed from DOM when hidden
 *                                default: true
 *    - hide(dispose) - hide the panel.
 */

RED.popover = (function() {
    var deltaSizes = {
        "default": {
            x: 12,
            y: 12
        },
        "small": {
            x:8,
            y:8
        }
    }
    function createPopover(options) {
        var target = options.target;
        var direction = options.direction || "right";
        var trigger = options.trigger;
        var content = options.content;
        var delay = options.delay ||  { show: 750, hide: 50 };
        var autoClose = options.autoClose;
        var width = options.width||"auto";
        var maxWidth = options.maxWidth;
        var size = options.size||"default";
        var popupOffset = options.offset || 0;
        if (!deltaSizes[size]) {
            throw new Error("Invalid RED.popover size value:",size);
        }

        var timer = null;
        var active;
        var div;
        var contentDiv;
        var currentStyle;

        var openPopup = function(instant) {
            if (active) {
                var existingPopover = target.data("red-ui-popover");
                if (options.tooltip && existingPopover) {
                    active = false;
                    return;
                }
                div = $('<div class="red-ui-popover"></div>');
                if (options.class) {
                    div.addClass(options.class);
                }
                contentDiv = $('<div class="red-ui-popover-content">').appendTo(div);
                if (size !== "default") {
                    div.addClass("red-ui-popover-size-"+size);
                }
                if (typeof content === 'function') {
                    var result = content.call(res);
                    if (result === null) {
                        return;
                    }
                    if (typeof result === 'string') {
                        contentDiv.text(result);
                    } else {
                        contentDiv.append(result);
                    }
                } else {
                    contentDiv.html(content);
                }
                div.appendTo("body");

                movePopup({target,direction,width,maxWidth});

                if (existingPopover) {
                    existingPopover.close(true);
                }
                if (options.trigger !== 'manual') {
                    target.data("red-ui-popover",res)
                }
                if (options.tooltip) {
                    div.on("mousedown", function(evt) {
                        closePopup(true);
                    });
                }
                if (/*trigger === 'hover' && */options.interactive) {
                    div.on('mouseenter', function(e) {
                        clearTimeout(timer);
                        active = true;
                    })
                    div.on('mouseleave', function(e) {
                        if (timer) {
                            clearTimeout(timer);
                        }
                        if (active) {
                            timer = setTimeout(function() {
                                active = false;
                                closePopup();
                            },delay.hide);
                        }
                    })
                }
                if (instant) {
                    div.show();
                } else {
                    div.fadeIn("fast");
                }
            }
        }
        var movePopup = function(options) {
            target = options.target || target;
            direction = options.direction || direction || "right";
            popupOffset = options.offset || popupOffset;
            var transition = options.transition;

            var width = options.width||"auto";
            div.width(width);
            if (options.maxWidth) {
                div.css("max-width",options.maxWidth)
            } else {
                div.css("max-width", 'auto');
            }

            var targetPos = target[0].getBoundingClientRect();
            var targetHeight = targetPos.height;
            var targetWidth = targetPos.width;

            var divHeight = div.outerHeight();
            var divWidth = div.outerWidth();
            var paddingRight = 10;

            var viewportTop = $(window).scrollTop();
            var viewportLeft = $(window).scrollLeft();
            var viewportBottom = viewportTop + $(window).height();
            var viewportRight = viewportLeft + $(window).width();
            var top = 0;
            var left = 0;
            if (direction === 'right') {
                top = targetPos.top+targetHeight/2-divHeight/2;
                left = targetPos.left+targetWidth+deltaSizes[size].x+popupOffset;
            } else if (direction === 'left') {
                top = targetPos.top+targetHeight/2-divHeight/2;
                left = targetPos.left-deltaSizes[size].x-divWidth-popupOffset;
            } else if (direction === 'bottom') {
                top = targetPos.top+targetHeight+deltaSizes[size].y+popupOffset;
                left = targetPos.left+targetWidth/2-divWidth/2;
                if (left < 0) {
                    direction = "right";
                    top = targetPos.top+targetHeight/2-divHeight/2;
                    left = targetPos.left+targetWidth+deltaSizes[size].x+popupOffset;
                } else if (left+divWidth+paddingRight > viewportRight) {
                    direction = "left";
                    top = targetPos.top+targetHeight/2-divHeight/2;
                    left = targetPos.left-deltaSizes[size].x-divWidth-popupOffset;
                    if (top+divHeight+targetHeight/2 + 5 > viewportBottom) {
                        top -= (top+divHeight+targetHeight/2 - viewportBottom + 5)
                    }
                } else if (top+divHeight > viewportBottom) {
                    direction = 'top';
                    top = targetPos.top-deltaSizes[size].y-divHeight-popupOffset;
                    left = targetPos.left+targetWidth/2-divWidth/2;
                }
            } else if (direction === 'top') {
                top = targetPos.top-deltaSizes[size].y-divHeight-popupOffset;
                left = targetPos.left+targetWidth/2-divWidth/2;
                if (top < 0) {
                    direction = 'bottom';
                    top = targetPos.top+targetHeight+deltaSizes[size].y+popupOffset;
                    left = targetPos.left+targetWidth/2-divWidth/2;
                }
            } else if (/inset/.test(direction)) {
                top = targetPos.top + targetHeight/2 - divHeight/2;
                left = targetPos.left + targetWidth/2 - divWidth/2;

                if (/bottom/.test(direction)) {
                    top = targetPos.top + targetHeight - divHeight-popupOffset;
                }
                if (/top/.test(direction)) {
                    top = targetPos.top+popupOffset;
                }
                if (/left/.test(direction)) {
                    left = targetPos.left+popupOffset;
                }
                if (/right/.test(direction)) {
                    left = targetPos.left + targetWidth - divWidth-popupOffset;
                }
            }
            if (currentStyle) {
                div.removeClass(currentStyle);
            }
            if (transition) {
                div.css({
                    "transition": "0.6s ease",
                    "transition-property": "top,left,right,bottom"
                })
            }
            currentStyle = 'red-ui-popover-'+direction;
            div.addClass(currentStyle).css({top: top, left: left});
            if (transition) {
                setTimeout(function() {
                    div.css({
                        "transition": "none"
                    });
                },600);
            }

        }
        var closePopup = function(instant) {
            $(document).off('mousedown.red-ui-popover');
            if (!active) {
                if (div) {
                    if (instant) {
                        div.remove();
                    } else {
                        div.fadeOut("fast",function() {
                            $(this).remove();
                        });
                    }
                    div = null;
                    target.removeData("red-ui-popover",res)
                }
            }
        }

        target.on("remove", function (ev) {
            if (timer) {
                clearTimeout(timer);
            }
            if (active) {
                active = false;
                setTimeout(closePopup,delay.hide);
            }
        });

        if (trigger === 'hover') {
            target.on('mouseenter',function(e) {
                clearTimeout(timer);
                if (!active) {
                    active = true;
                    timer = setTimeout(openPopup,delay.show);
                }
            });
            target.on('mouseleave disabled', function(e) {
                if (timer) {
                    clearTimeout(timer);
                }
                if (active) {
                    active = false;
                    setTimeout(closePopup,delay.hide);
                }
            });
        } else if (trigger === 'click') {
            target.on("click", function(e) {
                e.preventDefault();
                e.stopPropagation();
                active = !active;
                if (!active) {
                    closePopup();
                } else {
                    openPopup();
                }
            });
            if (autoClose) {
                target.on('mouseleave disabled', function(e) {
                    if (timer) {
                        clearTimeout(timer);
                    }
                    if (active) {
                        active = false;
                        setTimeout(closePopup,autoClose);
                    }
                });
            }

        } else if (trigger === 'modal') {
            $(document).on('mousedown.red-ui-popover', function (event) {
                var target = event.target;
                while (target.nodeName !== 'BODY' && target !== div[0]) {
                    target = target.parentElement;
                }
                if (target.nodeName === 'BODY') {
                    active = false;
                    closePopup();
                }
            });
        } else if (autoClose) {
            setTimeout(function() {
                active = false;
                closePopup();
            },autoClose);
        }
        var res = {
            get element() { return div },
            setContent: function(_content) {
                content = _content;

                return res;
            },
            open: function (instant) {
                active = true;
                openPopup(instant);
                return res;
            },
            close: function (instant) {
                active = false;
                closePopup(instant);
                return res;
            },
            move: function(options) {
                movePopup(options);
                return
            }
        }
        return res;

    }

    return {
        create: createPopover,
        tooltip: function(target,content, action, interactive) {
            var label = function() {
                var label = content;
                if (typeof content === 'function') {
                    label = content()
                }
                if (action) {
                    var shortcut = RED.keyboard.getShortcut(action);
                    if (shortcut && shortcut.key) {
                        label = $('<span>'+content+' <span class="red-ui-popover-key">'+RED.keyboard.formatKey(shortcut.key, true)+'</span></span>');
                    }
                }
                return label;
            }
            var popover = RED.popover.create({
                tooltip: true,
                target:target,
                trigger: "hover",
                size: "small",
                direction: "bottom",
                content: label,
                interactive,
                delay: { show: 750, hide: 50 }
            });
            popover.setContent = function(newContent) {
                content = newContent;
            }
            popover.setAction = function(newAction) {
                action = newAction;
            }
            popover.delete = function() {
                popover.close(true)
                target.off("mouseenter");
                target.off("mouseleave");
            };
            return popover;

        },
        menu: function(options) {
            var list = $('<ul class="red-ui-menu"></ul>');
            if (options.style === 'compact') {
                list.addClass("red-ui-menu-compact");
            }
            var menuOptions = options.options || [];
            var first;

            var container = RED.popover.panel(list);
            if (options.width) {
                container.container.width(options.width);
            }
            if (options.class) {
                container.container.addClass(options.class);
            }
            if (options.maxHeight) {
                container.container.css({
                    "max-height": options.maxHeight,
                    "overflow-y": 'auto'
                })
            }
            var menu = {
                options: function(opts) {
                    if (opts === undefined) {
                        return menuOptions
                    }
                    menuOptions = opts || [];
                    list.empty();
                    menuOptions.forEach(function(opt) {
                        var item = $('<li>').appendTo(list);
                        var link = $('<a href="#"></a>').appendTo(item);
                        if (typeof opt.label == "string") {
                            link.text(opt.label)
                        } else if (opt.label){
                            opt.label.appendTo(link);
                        }
                        link.on("click", function(evt) {
                            evt.preventDefault();
                            if (opt.onselect) {
                                opt.onselect();
                            } else if (options.onselect) {
                                options.onselect(opt);
                            }
                            menu.hide();
                        })
                        if (!first) { first = link}
                    })
                },
                show: function(opts) {
                    $(document).on("keydown.red-ui-menu", function(evt) {
                        var currentItem = list.find(":focus").parent();
                        if (evt.keyCode === 40) {
                            evt.preventDefault();
                            // DOWN
                            if (currentItem.length > 0) {
                                if (currentItem.index() === menuOptions.length-1) {
                                    // Wrap to top of list
                                    list.children().first().children().first().focus();
                                } else {
                                    currentItem.next().children().first().focus();
                                }
                            } else {
                                list.children().first().children().first().focus();
                            }
                        } else if (evt.keyCode === 38) {
                            evt.preventDefault();
                            // UP
                            if (currentItem.length > 0) {
                                if (currentItem.index() === 0) {
                                    // Wrap to bottom of list
                                    list.children().last().children().first().focus();
                                } else {
                                    currentItem.prev().children().first().focus();
                                }
                            } else {
                                list.children().last().children().first().focus();
                            }
                        } else if (evt.keyCode === 27) {
                            // ESCAPE
                            evt.preventDefault();
                            menu.hide(true);
                        } else if (evt.keyCode === 9 && options.tabSelect) {
                            // TAB - with tabSelect enabled
                            evt.preventDefault();
                            currentItem.find("a").trigger("click");

                        }
                        evt.stopPropagation();
                    })
                    opts.onclose = function() {
                        $(document).off("keydown.red-ui-menu");
                        if (options.onclose) {
                            options.onclose(true);
                        }
                    }
                    container.show(opts);
                },
                hide: function(cancelled) {
                    $(document).off("keydown.red-ui-menu");
                    container.hide(options.disposeOnClose);
                    if (options.onclose) {
                        options.onclose(cancelled);
                    }
                }
            }
            menu.options(menuOptions);
            return menu;
        },
        panel: function(content) {
            var panel = $('<div class="red-ui-editor-dialog red-ui-popover-panel"></div>');
            panel.css({ display: "none" });
            panel.appendTo(document.body);
            content.appendTo(panel);

            function hide(dispose) {
                $(document).off("mousedown.red-ui-popover-panel-close");
                $(document).off("keydown.red-ui-popover-panel-close");
                panel.hide();
                panel.css({
                    height: "auto"
                });
                if (dispose !== false) {
                    panel.remove();
                }
            }
            function show(options) {
                var closeCallback = options.onclose;
                var closeButton = options.closeButton;
                var target = options.target;
                var align = options.align || "right";
                var offset = options.offset || [0,0];
                var xPos = options.x;
                var yPos = options.y;
                var isAbsolutePosition = (xPos !== undefined && yPos !== undefined)

                var pos = isAbsolutePosition?{left:xPos, top: yPos}:target.offset();
                var targetWidth = isAbsolutePosition?0:target.width();
                var targetHeight = isAbsolutePosition?0:target.outerHeight();
                var panelHeight = panel.height();
                var panelWidth = panel.width();

                var top = (targetHeight+pos.top) + offset[1];
                if (top+panelHeight-$(document).scrollTop() > $(window).height()) {
                    top -= (top+panelHeight)-$(window).height() + 5;
                }
                if (top < 0) {
                    panel.height(panelHeight+top)
                    top = 0;
                }
                if (align === "right") {
                    panel.css({
                        top: top+"px",
                        left: (pos.left+offset[0])+"px",
                    });
                } else if (align === "left") {
                    panel.css({
                        top: top+"px",
                        left: (pos.left-panelWidth+offset[0])+"px",
                    });
                }
                panel.slideDown(100);

                $(document).on("keydown.red-ui-popover-panel-close", function(event) {
                    if (event.keyCode === 27) {
                        // ESCAPE
                        if (closeCallback) {
                            closeCallback();
                        }
                        hide(options.dispose);
                    }
                });

                $(document).on("mousedown.red-ui-popover-panel-close", function(event) {
                    var hitCloseButton = closeButton && $(event.target).closest(closeButton).length;
                    if(!hitCloseButton && !$(event.target).closest(panel).length && !$(event.target).closest(".red-ui-editor-dialog").length) {
                        if (closeCallback) {
                            closeCallback();
                        }
                        hide(options.dispose);
                    }
                    // if ($(event.target).closest(target).length) {
                    //     event.preventDefault();
                    // }
                })
            }
            return  {
                container: panel,
                show:show,
                hide:hide
            }
        }
    }

})();
;/**
 * 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.
 **/
(function($) {

/**
 * options:
 *   - minimumLength : the minimum length of text before firing a change event
 *   - delay : delay, in ms, after a keystroke before firing change event
 *
 * methods:
 *   - value([val]) - gets the current value, or, if `val` is provided, sets the value
 *   - count - sets or clears a sub-label on the input. This can be used to provide
 *             a feedback on the number of matches, or number of available entries to search
 *   - change - trigger a change event
 *
 */

    $.widget( "nodered.searchBox", {
        _create: function() {
            var that = this;

            this.currentTimeout = null;
            this.lastSent = "";
            this.element.val("");
            this.element.addClass("red-ui-searchBox-input");
            this.uiContainer = this.element.wrap("<div>").parent();
            this.uiContainer.addClass("red-ui-searchBox-container");

            if (this.options.style === "compact") {
                this.uiContainer.addClass("red-ui-searchBox-compact");
            }

            if (this.element.parents("form").length === 0) {
                var form = this.element.wrap("<form>").parent();
                form.addClass("red-ui-searchBox-form");
            }
            $('<i class="fa fa-search"></i>').prependTo(this.uiContainer);
            this.clearButton = $('<a class="red-ui-searchBox-clear" href="#"><i class="fa fa-times"></i></a>').appendTo(this.uiContainer);
            this.clearButton.on("click",function(e) {
                e.preventDefault();
                that.element.val("");
                that._change("",true);
                that.element.trigger("focus");
            });

            if (this.options.options) {
                this.uiContainer.addClass("red-ui-searchBox-has-options");
                this.optsButton = $('<a class="red-ui-searchBox-opts" href="#"><i class="fa fa-caret-down"></i></a>').appendTo(this.uiContainer);
                var menuShown = false;
                this.optsMenu = RED.popover.menu({
                    style: this.options.style,
                    options: this.options.options.map(function(opt) {
                        return {
                            label: opt.label,
                            onselect: function() {
                                that.element.val(opt.value+" ");
                                that._change(opt.value,true);
                            }
                        }
                    }),
                    onclose: function(cancelled) {
                        menuShown = false;
                        that.element.trigger("focus");
                    },
                    disposeOnClose: false
                });

                var showMenu = function() {
                    menuShown = true;
                    that.optsMenu.show({
                        target: that.optsButton,
                        align: "left",
                        offset: [that.optsButton.width()-2,-1],
                        dispose: false
                    })
                }
                this.optsButton.on("click",function(e) {
                    e.preventDefault();
                    if (!menuShown) {
                        showMenu();
                    } else {
                        // TODO: This doesn't quite work because the panel's own
                        // mousedown handler triggers a close before this click
                        // handler fires.
                        that.optsMenu.hide(true);
                    }
                });
                this.optsButton.on("keydown",function(e) {
                    if (!menuShown && e.keyCode === 40) {
                        //DOWN
                        showMenu();
                    }
                });
                this.element.on("keydown",function(e) {
                    if (!menuShown && e.keyCode === 40 && $(this).val() === '') {
                        //DOWN (only show menu if search field is emty)
                        showMenu();
                    }
                });
            }

            this.resultCount = $('<span>',{class:"red-ui-searchBox-resultCount hide"}).appendTo(this.uiContainer);

            this.element.val("");
            this.element.on("keydown",function(evt) {
                if (evt.keyCode === 27) {
                    that.element.val("");
                }
                if (evt.keyCode === 13) {
                    evt.preventDefault();
                }
            })
            this.element.on("keyup",function(evt) {
                that._change($(this).val());
            });

            this.element.on("focus",function() {
                $(document).one("mousedown",function() {
                    that.element.blur();
                });
            });

        },
        _change: function(val,instant) {
            var fireEvent = false;
            if (val === "") {
                this.clearButton.hide();
                fireEvent = true;
            } else {
                this.clearButton.show();
                fireEvent = (val.length >= (this.options.minimumLength||0));
            }
            var current = this.element.val();
            fireEvent = fireEvent && current !== this.lastSent;
            if (fireEvent) {
                if (!instant && this.options.delay > 0) {
                    clearTimeout(this.currentTimeout);
                    var that = this;
                    this.currentTimeout = setTimeout(function() {
                        that.lastSent = that.element.val();
                        that._trigger("change");
                    },this.options.delay);
                } else {
                    this.lastSent = this.element.val();
                    this._trigger("change");
                }
            }
        },
        value: function(val) {
            if (val === undefined) {
                return this.element.val();
            } else {
                this.element.val(val);
                this._change(val);
            }
        },
        count: function(val) {
            if (val === undefined || val === null || val === "") {
                this.resultCount.text("").hide();
            } else {
                this.resultCount.text(val).show();
            }
        },
        change: function() {
            this._trigger("change");
        }
    });
})(jQuery);
;/**
 * 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.
 **/



RED.tabs = (function() {

    var defaultTabIcon = "fa fa-lemon-o";
    var dragActive = false;
    var dblClickTime;
    var dblClickArmed = false;

    function createTabs(options) {
        var tabs = {};
        var pinnedTabsCount = 0;
        var currentTabWidth;
        var currentActiveTabWidth = 0;
        var collapsibleMenu;
        var mousedownTab;
        var preferredOrder = options.order;
        var ul = options.element || $("#"+options.id);
        var wrapper = ul.wrap( "<div>" ).parent();
        var scrollContainer = ul.wrap( "<div>" ).parent();
        wrapper.addClass("red-ui-tabs");
        if (options.vertical) {
            wrapper.addClass("red-ui-tabs-vertical");
        }

        if (options.addButton) {
            wrapper.addClass("red-ui-tabs-add");
            var addButton = $('<div class="red-ui-tab-button red-ui-tabs-add"><a href="#"><i class="fa fa-plus"></i></a></div>').appendTo(wrapper);
            addButton.find('a').on("click", function(evt) {
                evt.preventDefault();
                if (typeof options.addButton === 'function') {
                    options.addButton();
                } else if (typeof options.addButton === 'string') {
                    RED.actions.invoke(options.addButton);
                }
            })
            if (typeof options.addButton === 'string') {
                var l = options.addButton;
                if (options.addButtonCaption) {
                    l = options.addButtonCaption
                }
                RED.popover.tooltip(addButton,l,options.addButton);
            }
            ul.on("dblclick", function(evt) {
                var existingTabs = ul.children();
                var clickX = evt.clientX;
                var targetIndex = 0;
                existingTabs.each(function(index) {
                    var pos = $(this).offset();
                    if (pos.left > clickX) {
                        return false;
                    }
                    targetIndex = index+1;
                })
                if (typeof options.addButton === 'function') {
                    options.addButton({index:targetIndex});
                } else if (typeof options.addButton === 'string') {
                    RED.actions.invoke(options.addButton,{index:targetIndex});
                }
            });
        }
        if (options.searchButton) {
            // This is soft-deprecated as we don't use this in the core anymore
            // We no use the `menu` option to provide a drop-down list of actions
            wrapper.addClass("red-ui-tabs-search");
            var searchButton = $('<div class="red-ui-tab-button red-ui-tabs-search"><a href="#"><i class="fa fa-list-ul"></i></a></div>').appendTo(wrapper);
            searchButton.find('a').on("click", function(evt) {
                evt.preventDefault();
                if (typeof options.searchButton === 'function') {
                    options.searchButton()
                } else if (typeof options.searchButton === 'string') {
                    RED.actions.invoke(options.searchButton);
                }
            })
            if (typeof options.searchButton === 'string') {
                var l = options.searchButton;
                if (options.searchButtonCaption) {
                    l = options.searchButtonCaption
                }
                RED.popover.tooltip(searchButton,l,options.searchButton);
            }

        }
        if (options.menu) {
            wrapper.addClass("red-ui-tabs-menu");
            var menuButton = $('<div class="red-ui-tab-button red-ui-tabs-menu"><a href="#"><i class="fa fa-caret-down"></i></a></div>').appendTo(wrapper);
            var menuButtonLink = menuButton.find('a')
            var menuOpen = false;
            var menu;
            menuButtonLink.on("click", function(evt) {
                evt.stopPropagation();
                evt.preventDefault();
                if (menuOpen) {
                    menu.remove();
                    menuOpen = false;
                    return;
                }
                menuOpen = true;
                var menuOptions = [];
                if (typeof options.searchButton === 'function') {
                    menuOptions = options.menu()
                } else if (Array.isArray(options.menu)) {
                    menuOptions = options.menu;
                } else if (typeof options.menu === 'function') {
                    menuOptions = options.menu();
                }
                menu = RED.menu.init({options: menuOptions});
                menu.attr("id",options.id+"-menu");
                menu.css({
                    position: "absolute"
                })
                menu.appendTo("body");
                var elementPos = menuButton.offset();
                menu.css({
                    top: (elementPos.top+menuButton.height()-2)+"px",
                    left: (elementPos.left - menu.width() + menuButton.width())+"px"
                })
                $(".red-ui-menu.red-ui-menu-dropdown").hide();
                $(document).on("click.red-ui-tabmenu", function(evt) {
                    $(document).off("click.red-ui-tabmenu");
                    menuOpen = false;
                    menu.remove();
                });
                menu.show();
            })
        }

        if (options.contextmenu) {
            wrapper.on('contextmenu', function(evt) {
                let clickedTab
                let target = evt.target
                while(target.nodeName !== 'A' && target.nodeName !== 'UL' && target.nodeName !== 'BODY') {
                    target = target.parentNode
                }
                if (target.nodeName === 'A') {
                    const href = target.getAttribute('href')
                    if (href) {
                        clickedTab = tabs[href.slice(1)]
                    }
                }
                evt.preventDefault()
                evt.stopPropagation()
                RED.contextMenu.show({
                    x:evt.clientX-5,
                    y:evt.clientY-5,
                    options: options.contextmenu(clickedTab)
                })
                return false
            })
        }

        var scrollLeft;
        var scrollRight;

        if (options.scrollable) {
            wrapper.addClass("red-ui-tabs-scrollable");
            scrollContainer.addClass("red-ui-tabs-scroll-container");
            scrollContainer.on("scroll",function(evt) {
                // Generated by trackpads - not mousewheel
                updateScroll(evt);
            });
            scrollContainer.on("wheel", function(evt) {
                if (evt.originalEvent.deltaX === 0) {
                    // Prevent the scroll event from firing
                    evt.preventDefault();

                    // Assume this is wheel event which might not trigger
                    // the scroll event, so do things manually
                    var sl = scrollContainer.scrollLeft();
                    sl += evt.originalEvent.deltaY;
                    scrollContainer.scrollLeft(sl);
                }
            })
            scrollLeft = $('<div class="red-ui-tab-button red-ui-tab-scroll red-ui-tab-scroll-left"><a href="#" style="display:none;"><i class="fa fa-caret-left"></i></a></div>').appendTo(wrapper).find("a");
            scrollLeft.on('mousedown',function(evt) {scrollEventHandler(evt, evt.shiftKey?('-='+scrollContainer.scrollLeft()):'-=150') }).on('click',function(evt){ evt.preventDefault();});
            scrollRight = $('<div class="red-ui-tab-button red-ui-tab-scroll red-ui-tab-scroll-right"><a href="#" style="display:none;"><i class="fa fa-caret-right"></i></a></div>').appendTo(wrapper).find("a");
            scrollRight.on('mousedown',function(evt) { scrollEventHandler(evt,evt.shiftKey?('+='+(scrollContainer[0].scrollWidth - scrollContainer.width()-scrollContainer.scrollLeft())):'+=150') }).on('click',function(evt){ evt.preventDefault();});
        }

        if (options.collapsible) {
            // var dropDown = $('<div>',{class:"red-ui-tabs-select"}).appendTo(wrapper);
            // ul.hide();
            wrapper.addClass("red-ui-tabs-collapsible");

            var collapsedButtonsRow = $('<div class="red-ui-tab-link-buttons"></div>').appendTo(wrapper);

            if (options.menu !== false) {
                var selectButton = $('<a href="#"><i class="fa fa-caret-down"></i></a>').appendTo(collapsedButtonsRow);
                selectButton.addClass("red-ui-tab-link-button-menu")
                selectButton.on("click", function(evt) {
                    evt.stopPropagation();
                    evt.preventDefault();
                    if (!collapsibleMenu) {
                        var pinnedOptions = [];
                        var options = [];
                        ul.children().each(function(i,el) {
                            var id = $(el).data('tabId');
                            var opt = {
                                id:"red-ui-tabs-menu-option-"+id,
                                icon: tabs[id].iconClass || defaultTabIcon,
                                label: tabs[id].name,
                                onselect: function() {
                                    activateTab(id);
                                }
                            };
                            // if (tabs[id].pinned) {
                            //     pinnedOptions.push(opt);
                            // } else {
                                options.push(opt);
                            // }
                        });
                        options = pinnedOptions.concat(options);
                        collapsibleMenu = RED.menu.init({options: options});
                        collapsibleMenu.css({
                            position: "absolute"
                        })
                        collapsibleMenu.appendTo("body");
                    }
                    var elementPos = selectButton.offset();
                    collapsibleMenu.css({
                        top: (elementPos.top+selectButton.height()-2)+"px",
                        left: (elementPos.left - collapsibleMenu.width() + selectButton.width())+"px"
                    })
                    if (collapsibleMenu.is(":visible")) {
                        $(document).off("click.red-ui-tabmenu");
                    } else {
                        $(".red-ui-menu.red-ui-menu-dropdown").hide();
                        $(document).on("click.red-ui-tabmenu", function(evt) {
                            $(document).off("click.red-ui-tabmenu");
                            collapsibleMenu.hide();
                        });
                    }
                    collapsibleMenu.toggle();
                })
            }

        }

        function scrollEventHandler(evt,dir) {
            evt.preventDefault();
            if ($(this).hasClass('disabled')) {
                return;
            }
            var currentScrollLeft = scrollContainer.scrollLeft();
            scrollContainer.animate( { scrollLeft: dir }, 100);
            var interval = setInterval(function() {
                var newScrollLeft = scrollContainer.scrollLeft()
                if (newScrollLeft === currentScrollLeft) {
                    clearInterval(interval);
                    return;
                }
                currentScrollLeft = newScrollLeft;
                scrollContainer.animate( { scrollLeft: dir }, 100);
            },100);
            $(this).one('mouseup',function() {
                clearInterval(interval);
            })
        }


        ul.children().first().addClass("active");
        ul.children().addClass("red-ui-tab");

        function getSelection() {
            var selection = ul.find("li.red-ui-tab.selected");
            var selectedTabs = [];
            selection.each(function() {
                selectedTabs.push(tabs[$(this).find('a').attr('href').slice(1)])
            })
            return selectedTabs;
        }

        function selectionChanged() {
            options.onselect(getSelection());
        }

        function onTabClick(evt) {
            if (dragActive) {
                return
            }
            if (evt.currentTarget !== mousedownTab) {
                mousedownTab = null;
                return;
            }
            mousedownTab = null;
            if (dblClickTime && Date.now()-dblClickTime < 400) {
                dblClickTime = 0;
                dblClickArmed = true;
                return onTabDblClick.call(this,evt);
            }
            dblClickTime = Date.now();

            var currentTab = ul.find("li.red-ui-tab.active");
            var thisTab = $(this).parent();
            var fireSelectionChanged = false;
            if (options.onselect) {
                if (evt.metaKey || evt.ctrlKey) {
                    if (thisTab.hasClass("selected")) {
                        thisTab.removeClass("selected");
                        if (thisTab[0] !== currentTab[0]) {
                            // Deselect background tab
                            // - don't switch to it
                            selectionChanged();
                            return;
                        } else {
                            // Deselect current tab
                            // - if nothing remains selected, do nothing
                            // - otherwise switch to first selected tab
                            var selection = ul.find("li.red-ui-tab.selected");
                            if (selection.length === 0) {
                                selectionChanged();
                                return;
                            }
                            thisTab = selection.first();
                        }
                    } else {
                        if (!currentTab.hasClass("selected")) {
                            var currentTabObj = tabs[currentTab.find('a').attr('href').slice(1)];
                            // Auto select current tab
                            currentTab.addClass("selected");
                        }
                        thisTab.addClass("selected");
                    }
                    fireSelectionChanged = true;
                } else if (evt.shiftKey) {
                    if (currentTab[0] !== thisTab[0]) {
                        var firstTab,lastTab;
                        if (currentTab.index() < thisTab.index()) {
                            firstTab = currentTab;
                            lastTab = thisTab;
                        } else {
                            firstTab = thisTab;
                            lastTab = currentTab;
                        }
                        ul.find("li.red-ui-tab").removeClass("selected");
                        firstTab.addClass("selected");
                        lastTab.addClass("selected");
                        firstTab.nextUntil(lastTab).addClass("selected");
                    }
                    fireSelectionChanged = true;
                } else {
                    var selection = ul.find("li.red-ui-tab.selected");
                    if (selection.length > 0) {
                        selection.removeClass("selected");
                        fireSelectionChanged = true;
                    }
                }
            }

            var thisTabA = thisTab.find("a");
            if (options.onclick) {
                options.onclick(tabs[thisTabA.attr('href').slice(1)], evt);
                if (evt.isDefaultPrevented() && evt.isPropagationStopped()) {
                    return false
                }
            }
            activateTab(thisTabA);
            if (fireSelectionChanged) {
                selectionChanged();
            }
        }

        function updateScroll() {
            if (ul.children().length !== 0) {
                var sl = scrollContainer.scrollLeft();
                var scWidth = scrollContainer.width();
                var ulWidth = ul.width();
                if (sl === 0) {
                    scrollLeft.hide();
                } else {
                    scrollLeft.show();
                }
                if (sl === ulWidth-scWidth) {
                    scrollRight.hide();
                } else {
                    scrollRight.show();
                }
            }
        }
        function onTabDblClick(evt) {
            evt.preventDefault();
            if (evt.metaKey || evt.shiftKey) {
                return;
            }
            if (options.ondblclick) {
                options.ondblclick(tabs[$(this).attr('href').slice(1)]);
            }
            return false;
        }

        function activateTab(link) {
            if (typeof link === "string") {
                link = ul.find("a[href='#"+link+"']");
            }
            if (link.length === 0) {
                return;
            }
            if (link.parent().hasClass("hide-tab")) {
                link.parent().removeClass("hide-tab").removeClass("hide");
                if (options.onshow) {
                    options.onshow(tabs[link.attr('href').slice(1)])
                }
            }
            if (!link.parent().hasClass("active")) {
                ul.children().removeClass("active");
                ul.children().css({"transition": "width 100ms"});
                link.parent().addClass("active");
                var parentId = link.parent().attr('id');
                wrapper.find(".red-ui-tab-link-button").removeClass("active selected");
                $("#"+parentId+"-link-button").addClass("active selected");
                if (options.scrollable) {
                    var pos = link.parent().position().left;
                    if (pos-21 < 0) {
                        scrollContainer.animate( { scrollLeft: '+='+(pos-50) }, 300);
                    } else if (pos + 120 > scrollContainer.width()) {
                        scrollContainer.animate( { scrollLeft: '+='+(pos + 140-scrollContainer.width()) }, 300);
                    }
                }
                if (options.onchange) {
                    options.onchange(tabs[link.attr('href').slice(1)]);
                }
                updateTabWidths();
                setTimeout(function() {
                    ul.children().css({"transition": ""});
                },100);
            }
        }
        function activatePreviousTab() {
            var previous = findPreviousVisibleTab();
            if (previous.length > 0) {
                activateTab(previous.find("a"));
            }
        }
        function activateNextTab() {
            var next = findNextVisibleTab();
            if (next.length > 0) {
                activateTab(next.find("a"));
            }
        }

        function updateTabWidths() {
            if (options.vertical) {
                return;
            }
            var allTabs = ul.find("li.red-ui-tab");
            var tabs = allTabs.filter(":not(.hide-tab)");
            var hiddenTabs = allTabs.filter(".hide-tab");
            var width = wrapper.width();
            var tabCount = tabs.length;
            var tabWidth;

            if (options.collapsible) {
                var availableCount = collapsedButtonsRow.children().length;
                var visibleCount = collapsedButtonsRow.children(":visible").length;
                tabWidth = width - collapsedButtonsRow.width()-10;
                var maxTabWidth = 198;
                var minTabWidth = 120;
                if (tabWidth <= minTabWidth || (tabWidth < maxTabWidth && visibleCount > 5)) {
                    // The tab is too small. Hide the next button to make room
                    // Start at the end of the button row, -1 for the menu button
                    var b = collapsedButtonsRow.find("a:last").prev();
                    var index = collapsedButtonsRow.children().length - 2;
                    // Work backwards to find the first visible button
                    while (b.is(":not(:visible)")) {
                        b = b.prev();
                        index--;
                    }
                    // If it isn't a pinned button, hide it to get the room
                    if (tabWidth <= minTabWidth || visibleCount>6) {//}!b.hasClass("red-ui-tab-link-button-pinned")) {
                        b.hide();
                    }
                    tabWidth = Math.max(minTabWidth,width - collapsedButtonsRow.width()-10);
                } else {
                    if (visibleCount !== availableCount) {
                        if (visibleCount < 6) {
                            tabWidth = minTabWidth;
                        } else {
                            tabWidth = maxTabWidth;
                        }
                    }
                    var space = width - tabWidth - collapsedButtonsRow.width();
                    if (space > 40) {
                        collapsedButtonsRow.find("a:not(:visible):first").show();
                    }
                    tabWidth = width - collapsedButtonsRow.width()-10;
                }
                tabs.css({width:tabWidth});

            } else {
                var tabWidth = (width-12-(tabCount*6))/tabCount;
                currentTabWidth = (100*tabWidth/width)+"%";
                currentActiveTabWidth = currentTabWidth+"%";
                if (options.scrollable) {
                    tabWidth = Math.max(tabWidth,140);
                    currentTabWidth = tabWidth+"px";
                    currentActiveTabWidth = 0;
                    var listWidth = Math.max(wrapper.width(),12+(tabWidth+6)*tabCount);
                    ul.width(listWidth);
                    updateScroll();
                } else if (options.hasOwnProperty("minimumActiveTabWidth")) {
                    if (tabWidth < options.minimumActiveTabWidth) {
                        tabCount -= 1;
                        tabWidth = (width-12-options.minimumActiveTabWidth-(tabCount*6))/tabCount;
                        currentTabWidth = (100*tabWidth/width)+"%";
                        currentActiveTabWidth = options.minimumActiveTabWidth+"px";
                    } else {
                        currentActiveTabWidth = 0;
                    }
                }
                // if (options.collapsible) {
                //     console.log(currentTabWidth);
                // }

                tabs.css({width:currentTabWidth});
                hiddenTabs.css({width:"0px"});
                if (tabWidth < 50) {
                    // ul.find(".red-ui-tab-close").hide();
                    ul.find(".red-ui-tab-icon").hide();
                    ul.find(".red-ui-tab-label").css({paddingLeft:Math.min(12,Math.max(0,tabWidth-38))+"px"})
                } else {
                    // ul.find(".red-ui-tab-close").show();
                    ul.find(".red-ui-tab-icon").show();
                    ul.find(".red-ui-tab-label").css({paddingLeft:""})
                }
                if (currentActiveTabWidth !== 0) {
                    ul.find("li.red-ui-tab.active").css({"width":options.minimumActiveTabWidth});
                    // ul.find("li.red-ui-tab.active .red-ui-tab-close").show();
                    ul.find("li.red-ui-tab.active .red-ui-tab-icon").show();
                    ul.find("li.red-ui-tab.active .red-ui-tab-label").css({paddingLeft:""})
                }
            }

        }

        ul.find("li.red-ui-tab a")
            .on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
            .on("mouseup",onTabClick)
            // prevent browser-default middle-click behaviour
            .on("auxclick", function(evt) { evt.preventDefault() })
            .on("click", function(evt) {evt.preventDefault(); })
            .on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); })

        setTimeout(function() {
            updateTabWidths();
        },0);


        function removeTab(id) {
            if (options.onselect) {
                var selection = ul.find("li.red-ui-tab.selected");
                if (selection.length > 0) {
                    selection.removeClass("selected");
                    selectionChanged();
                }
            }
            var li = ul.find("a[href='#"+id+"']").parent();
            if (li.hasClass("active")) {
                var tab = findPreviousVisibleTab(li);
                if (tab.length === 0) {
                    tab = findNextVisibleTab(li);
                }
                if (tab.length > 0) {
                    activateTab(tab.find("a"));
                } else {
                    if (options.onchange) {
                        options.onchange(null);
                    }
                }
            }

            li.remove();
            if (tabs[id].pinned) {
                pinnedTabsCount--;
            }
            if (options.onremove) {
                options.onremove(tabs[id]);
            }
            delete tabs[id];
            updateTabWidths();
            if (collapsibleMenu) {
                collapsibleMenu.remove();
                collapsibleMenu = null;
            }
        }

        function findPreviousVisibleTab(li) {
            if (!li) {
                li = ul.find("li.active");
            }
            var previous = li.prev();
            while(previous.length > 0 && previous.hasClass("hide-tab")) {
                previous = previous.prev();
            }
            return previous;
        }
        function findNextVisibleTab(li) {
            if (!li) {
                li = ul.find("li.active");
            }
            var next = li.next();
            while(next.length > 0 && next.hasClass("hide-tab")) {
                next = next.next();
            }
            return next;
        }
        function showTab(id) {
            if (tabs[id]) {
                var li = ul.find("a[href='#"+id+"']").parent();
                if (li.hasClass("hide-tab")) {
                    li.removeClass("hide-tab").removeClass("hide");
                    if (ul.find("li.red-ui-tab:not(.hide-tab)").length === 1) {
                        activateTab(li.find("a"))
                    }
                    updateTabWidths();
                    if (options.onshow) {
                        options.onshow(tabs[id])
                    }
                }
            }
        }
        function hideTab(id) {
            if (tabs[id]) {
                var li = ul.find("a[href='#"+id+"']").parent();
                if (!li.hasClass("hide-tab")) {
                    if (li.hasClass("active")) {
                        var tab = findPreviousVisibleTab(li);
                        if (tab.length === 0) {
                            tab = findNextVisibleTab(li);
                        }
                        if (tab.length > 0) {
                            activateTab(tab.find("a"));
                        } else {
                            if (options.onchange) {
                                options.onchange(null);
                            }
                        }
                    }
                    li.removeClass("active");
                    li.one("transitionend", function(evt) {
                        li.addClass("hide");
                        updateTabWidths();
                        if (options.onhide) {
                            options.onhide(tabs[id])
                        }
                        setTimeout(function() {
                            updateScroll()
                        },200)
                    })
                    li.addClass("hide-tab");
                    li.css({width:0})
                }
            }
        }

        var tabAPI =  {
            addTab: function(tab,targetIndex) {
                if (options.onselect) {
                    var selection = ul.find("li.red-ui-tab.selected");
                    if (selection.length > 0) {
                        selection.removeClass("selected");
                        selectionChanged();
                    }
                }
                tabs[tab.id] = tab;
                var li = $("<li/>",{class:"red-ui-tab"});
                if (ul.children().length === 0) {
                    targetIndex = undefined;
                }
                if (targetIndex === 0) {
                    li.prependTo(ul);
                } else if (targetIndex > 0) {
                    li.insertAfter(ul.find("li:nth-child("+(targetIndex)+")"));
                } else {
                    li.appendTo(ul);
                }
                li.attr('id',"red-ui-tab-"+(tab.id.replace(".","-")));
                li.data("tabId",tab.id);

                if (options.maximumTabWidth || tab.maximumTabWidth) {
                    li.css("maxWidth",(options.maximumTabWidth || tab.maximumTabWidth) +"px");
                }
                var link = $("<a/>",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li);
                if (tab.icon) {
                    $('<i>',{class:"red-ui-tab-icon", style:"mask-image: url("+tab.icon+"); -webkit-mask-image: url("+tab.icon+");"}).appendTo(link);
                } else if (tab.iconClass) {
                    $('<i>',{class:"red-ui-tab-icon "+tab.iconClass}).appendTo(link);
                }
                var span = $('<span/>',{class:"red-ui-text-bidi-aware"}).text(tab.label).appendTo(link);
                span.attr('dir', RED.text.bidi.resolveBaseTextDir(tab.label));
                if (options.collapsible) {
                    li.addClass("red-ui-tab-pinned");
                    var pinnedLink = $('<a href="#'+tab.id+'" class="red-ui-tab-link-button"></a>');
                    if (tab.pinned) {
                        if (pinnedTabsCount === 0) {
                            pinnedLink.prependTo(collapsedButtonsRow)
                        } else {
                            pinnedLink.insertAfter(collapsedButtonsRow.find("a.red-ui-tab-link-button-pinned:last"));
                        }
                    } else {
                        if (options.menu !== false) {
                            pinnedLink.insertBefore(collapsedButtonsRow.find("a:last"));
                        } else {
                            pinnedLink.appendTo(collapsedButtonsRow);
                        }
                    }

                    pinnedLink.attr('id',li.attr('id')+"-link-button");
                    if (tab.iconClass) {
                        $('<i>',{class:tab.iconClass}).appendTo(pinnedLink);
                    } else {
                        $('<i>',{class:defaultTabIcon}).appendTo(pinnedLink);
                    }
                    pinnedLink.on("click", function(evt) {
                        evt.preventDefault();
                        activateTab(tab.id);
                    });
                    pinnedLink.data("tabId",tab.id)
                    if (tab.pinned) {
                        pinnedLink.addClass("red-ui-tab-link-button-pinned");
                        pinnedTabsCount++;
                    }
                    RED.popover.tooltip($(pinnedLink), tab.name, tab.action);
                    if (options.onreorder) {
                        var pinnedLinkIndex;
                        var pinnedLinks = [];
                        var startPinnedIndex;
                        pinnedLink.draggable({
                            distance: 10,
                            axis:"x",
                            containment: ".red-ui-tab-link-buttons",
                            start: function(event,ui) {
                                dragActive = true;
                                $(".red-ui-tab-link-buttons").width($(".red-ui-tab-link-buttons").width());
                                if (dblClickArmed) { dblClickArmed = false; return false }
                                collapsedButtonsRow.children().each(function(i) {
                                    pinnedLinks[i] = {
                                        el:$(this),
                                        text: $(this).text(),
                                        left: $(this).position().left,
                                        width: $(this).width(),
                                        menu: $(this).hasClass("red-ui-tab-link-button-menu")
                                    };
                                    if ($(this).is(pinnedLink)) {
                                        pinnedLinkIndex = i;
                                        startPinnedIndex = i;
                                    }
                                });
                                collapsedButtonsRow.children().each(function(i) {
                                    if (i!==pinnedLinkIndex) {
                                        $(this).css({
                                            position: 'absolute',
                                            left: pinnedLinks[i].left+"px",
                                            width: pinnedLinks[i].width+2,
                                            transition: "left 0.3s"
                                        });
                                    }
                                })
                                if (!pinnedLink.hasClass('active')) {
                                    pinnedLink.css({'zIndex':1});
                                }
                            },
                            drag: function(event,ui) {
                                ui.position.left += pinnedLinks[pinnedLinkIndex].left;
                                var tabCenter = ui.position.left + pinnedLinks[pinnedLinkIndex].width/2;
                                for (var i=0;i<pinnedLinks.length;i++) {
                                    if (i === pinnedLinkIndex || pinnedLinks[i].menu || pinnedLinks[i].el.is(":not(:visible)")) {
                                        continue;
                                    }
                                    if (tabCenter > pinnedLinks[i].left && tabCenter < pinnedLinks[i].left+pinnedLinks[i].width) {
                                        if (i < pinnedLinkIndex) {
                                            pinnedLinks[i].left += pinnedLinks[pinnedLinkIndex].width+8;
                                            pinnedLinks[pinnedLinkIndex].el.detach().insertBefore(pinnedLinks[i].el);
                                        } else {
                                            pinnedLinks[i].left -= pinnedLinks[pinnedLinkIndex].width+8;
                                            pinnedLinks[pinnedLinkIndex].el.detach().insertAfter(pinnedLinks[i].el);
                                        }
                                        pinnedLinks[i].el.css({left:pinnedLinks[i].left+"px"});

                                        pinnedLinks.splice(i, 0, pinnedLinks.splice(pinnedLinkIndex, 1)[0]);

                                        pinnedLinkIndex = i;
                                        break;
                                    }
                                }
                            },
                            stop: function(event,ui) {
                                dragActive = false;
                                collapsedButtonsRow.children().css({position:"relative",left:"",transition:""});
                                $(".red-ui-tab-link-buttons").width('auto');
                                pinnedLink.css({zIndex:""});
                                updateTabWidths();
                                if (startPinnedIndex !== pinnedLinkIndex) {
                                    if (collapsibleMenu) {
                                        collapsibleMenu.remove();
                                        collapsibleMenu = null;
                                    }
                                    var newOrder = $.makeArray(collapsedButtonsRow.children().map(function() { return $(this).data('tabId');}));
                                    tabAPI.order(newOrder);
                                    options.onreorder(newOrder);
                                }
                            }
                        });
                    }

                }
                link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
                link.on("mouseup",onTabClick);
                // prevent browser-default middle-click behaviour
                link.on("auxclick", function(evt) { evt.preventDefault() })
                link.on("click", function(evt) { evt.preventDefault(); })
                link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); })

                $('<span class="red-ui-tabs-fade"></span>').appendTo(li);

                if (tab.closeable) {
                    li.addClass("red-ui-tabs-closeable")
                    var closeLink = $("<a/>",{href:"#",class:"red-ui-tab-close"}).appendTo(li);
                    closeLink.append('<i class="fa fa-times" />');
                    closeLink.on("click",function(event) {
                        event.preventDefault();
                        removeTab(tab.id);
                    });
                    RED.popover.tooltip(closeLink,RED._("workspace.closeFlow"));
                }
                // if (tab.hideable) {
                //     li.addClass("red-ui-tabs-closeable")
                //     var closeLink = $("<a/>",{href:"#",class:"red-ui-tab-close red-ui-tab-hide"}).appendTo(li);
                //     closeLink.append('<i class="fa fa-eye" />');
                //     closeLink.append('<i class="fa fa-eye-slash" />');
                //     closeLink.on("click",function(event) {
                //         event.preventDefault();
                //         hideTab(tab.id);
                //     });
                //     RED.popover.tooltip(closeLink,RED._("workspace.hideFlow"));
                // }

                var badges = $('<span class="red-ui-tabs-badges"></span>').appendTo(li);
                if (options.onselect) {
                    $('<i class="red-ui-tabs-badge-selected fa fa-check-circle"></i>').appendTo(badges);
                }

                // link.attr("title",tab.label);
                RED.popover.tooltip(link,function() { return RED.utils.sanitize(tab.label); });

                if (options.onadd) {
                    options.onadd(tab);
                }
                if (ul.find("li.red-ui-tab").length == 1) {
                    activateTab(link);
                }
                if (options.onreorder && !options.collapsible) {
                    var originalTabOrder;
                    var tabDragIndex;
                    var tabElements = [];
                    var startDragIndex;

                    li.draggable({
                        axis:"x",
                        distance: 20,
                        start: function(event,ui) {
                            if (dblClickArmed) { dblClickArmed = false; return false }
                            dragActive = true;
                            originalTabOrder = [];
                            tabElements = [];
                            ul.children().each(function(i) {
                                tabElements[i] = {
                                    el:$(this),
                                    text: $(this).text(),
                                    left: $(this).position().left,
                                    width: $(this).width()
                                };
                                if ($(this).is(li)) {
                                    tabDragIndex = i;
                                    startDragIndex = i;
                                }
                                originalTabOrder.push($(this).data("tabId"));
                            });
                            ul.children().each(function(i) {
                                if (i!==tabDragIndex) {
                                    $(this).css({
                                        position: 'absolute',
                                        left: tabElements[i].left+"px",
                                        width: tabElements[i].width+2,
                                        transition: "left 0.3s"
                                    });
                                }

                            })
                            if (!li.hasClass('active')) {
                                li.css({'zIndex':1});
                            }
                        },
                        drag: function(event,ui) {
                            ui.position.left += tabElements[tabDragIndex].left+scrollContainer.scrollLeft();
                            var tabCenter = ui.position.left + tabElements[tabDragIndex].width/2 - scrollContainer.scrollLeft();
                            for (var i=0;i<tabElements.length;i++) {
                                if (i === tabDragIndex) {
                                    continue;
                                }
                                if (tabCenter > tabElements[i].left && tabCenter < tabElements[i].left+tabElements[i].width) {
                                    if (i < tabDragIndex) {
                                        tabElements[i].left += tabElements[tabDragIndex].width+8;
                                        tabElements[tabDragIndex].el.detach().insertBefore(tabElements[i].el);
                                    } else {
                                        tabElements[i].left -= tabElements[tabDragIndex].width+8;
                                        tabElements[tabDragIndex].el.detach().insertAfter(tabElements[i].el);
                                    }
                                    tabElements[i].el.css({left:tabElements[i].left+"px"});

                                    tabElements.splice(i, 0, tabElements.splice(tabDragIndex, 1)[0]);

                                    tabDragIndex = i;
                                    break;
                                }
                            }
                        },
                        stop: function(event,ui) {
                            dragActive = false;
                            ul.children().css({position:"relative",left:"",transition:""});
                            if (!li.hasClass('active')) {
                                li.css({zIndex:""});
                            }
                            updateTabWidths();
                            if (startDragIndex !== tabDragIndex) {
                                options.onreorder(originalTabOrder, $.makeArray(ul.children().map(function() { return $(this).data('tabId');})));
                            }
                            activateTab(tabElements[tabDragIndex].el.data('tabId'));
                        }
                    })
                }
                setTimeout(function() {
                    updateTabWidths();
                },10);
                if (collapsibleMenu) {
                    collapsibleMenu.remove();
                    collapsibleMenu = null;
                }
                if (preferredOrder) {
                    tabAPI.order(preferredOrder);
                }
            },
            removeTab: removeTab,
            activateTab: activateTab,
            nextTab: activateNextTab,
            previousTab: activatePreviousTab,
            resize: updateTabWidths,
            count: function() {
                return ul.find("li.red-ui-tab:not(.hide)").length;
            },
            activeIndex: function() {
                return ul.find("li.active").index()
            },
            getTabIndex: function (id) {
                return ul.find("a[href='#"+id+"']").parent().index()
            },
            contains: function(id) {
                return ul.find("a[href='#"+id+"']").length > 0;
            },
            showTab: showTab,
            hideTab: hideTab,

            renameTab: function(id,label) {
                tabs[id].label = label;
                var tab = ul.find("a[href='#"+id+"']");
                tab.find("span.red-ui-text-bidi-aware").text(label).attr('dir', RED.text.bidi.resolveBaseTextDir(label));
                updateTabWidths();
            },
            listTabs: function() {
                return $.makeArray(ul.children().map(function() { return $(this).data('tabId');}));
            },
            selection: getSelection,
            clearSelection: function() {
                if (options.onselect) {
                    var selection = ul.find("li.red-ui-tab.selected");
                    if (selection.length > 0) {
                        selection.removeClass("selected");
                        selectionChanged();
                    }
                }

            },
            order: function(order) {
                preferredOrder = order;
                var existingTabOrder = $.makeArray(ul.children().map(function() { return $(this).data('tabId');}));
                var i;
                var match = true;
                for (i=0;i<order.length;i++) {
                    if (order[i] !== existingTabOrder[i]) {
                        match = false;
                        break;
                    }
                }
                if (match) {
                    return;
                }
                var existingTabMap = {};
                var existingTabs = ul.children().detach().each(function() {
                    existingTabMap[$(this).data("tabId")] = $(this);
                });
                var pinnedButtons = {};
                if (options.collapsible) {
                    collapsedButtonsRow.children().detach().each(function() {
                        var id = $(this).data("tabId");
                        if (!id) {
                            id = "__menu__"
                        }
                        pinnedButtons[id] = $(this);
                    });
                }
                for (i=0;i<order.length;i++) {
                    if (existingTabMap[order[i]]) {
                        existingTabMap[order[i]].appendTo(ul);
                        if (options.collapsible) {
                            pinnedButtons[order[i]].appendTo(collapsedButtonsRow);
                        }
                        delete existingTabMap[order[i]];
                    }
                }
                // Add any tabs that aren't known in the order
                for (i in existingTabMap) {
                    if (existingTabMap.hasOwnProperty(i)) {
                        existingTabMap[i].appendTo(ul);
                        if (options.collapsible) {
                            pinnedButtons[i].appendTo(collapsedButtonsRow);
                        }
                    }
                }
                if (options.collapsible) {
                    pinnedButtons["__menu__"].appendTo(collapsedButtonsRow);
                    updateTabWidths();
                }
            }
        }
        return tabAPI;
    }

    return {
        create: createTabs
    }
})();
;/**
 * 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.
 **/

RED.stack = (function() {
    function createStack(options) {
        var container = options.container;
        container.addClass("red-ui-stack");
        var contentHeight = 0;
        var entries = [];

        var visible = true;
        // TODO: make this a singleton function - and watch out for stacks no longer
        //       in the DOM
        var resizeStack = function() {
            if (entries.length > 0) {
                var headerHeight = 0;
                entries.forEach(function(entry) {
                    headerHeight += entry.header.outerHeight();
                });

                var height = container.innerHeight();
                contentHeight = height - headerHeight - (entries.length-1);
                entries.forEach(function(e) {
                    e.contentWrap.height(contentHeight);
                });
            }
        }
        if (options.fill && options.singleExpanded) {
            $(window).on("resize", resizeStack);
            $(window).on("focus", resizeStack);
        }
        return {
            add: function(entry) {
                entries.push(entry);
                entry.container = $('<div class="red-ui-palette-category">').appendTo(container);
                if (!visible) {
                    entry.container.hide();
                }
                var header = $('<div class="red-ui-palette-header"></div>').appendTo(entry.container);
                entry.header = header;
                entry.contentWrap = $('<div></div>',{style:"position:relative"}).appendTo(entry.container);
                if (options.fill) {
                    entry.contentWrap.css("height",contentHeight);
                }
                entry.content = $('<div></div>').appendTo(entry.contentWrap);
                if (entry.collapsible !== false) {
                    header.on("click", function() {
                        if (options.singleExpanded) {
                            if (!entry.isExpanded()) {
                                for (var i=0;i<entries.length;i++) {
                                    if (entries[i].isExpanded()) {
                                        entries[i].collapse();
                                    }
                                }
                                entry.expand();
                            } else if (entries.length === 2) {
                                if (entries[0] === entry) {
                                    entries[0].collapse();
                                    entries[1].expand();
                                } else {
                                    entries[1].collapse();
                                    entries[0].expand();
                                }
                            }
                        } else {
                            entry.toggle();
                        }
                    });
                    var icon = $('<i class="fa fa-angle-down"></i>').appendTo(header);

                    if (entry.expanded) {
                        entry.container.addClass("expanded");
                        icon.addClass("expanded");
                    } else {
                        entry.contentWrap.hide();
                    }
                } else {
                    $('<i style="opacity: 0.5;" class="fa fa-angle-down expanded"></i>').appendTo(header);
                    header.css("cursor","default");
                }
                entry.title = $('<span></span>').html(entry.title).appendTo(header);



                entry.toggle = function() {
                    if (entry.isExpanded()) {
                        entry.collapse();
                        return false;
                    } else {
                        entry.expand();
                        return true;
                    }
                };
                entry.expand = function() {
                    if (!entry.isExpanded()) {
                        if (entry.onexpand) {
                            entry.onexpand.call(entry);
                        }
                        if (options.singleExpanded) {
                            entries.forEach(function(e) {
                                if (e !== entry) {
                                    e.collapse();
                                }
                            })
                        }

                        icon.addClass("expanded");
                        entry.container.addClass("expanded");
                        entry.contentWrap.slideDown(200);
                        return true;
                    }
                };
                entry.collapse = function() {
                    if (entry.isExpanded()) {
                        icon.removeClass("expanded");
                        entry.container.removeClass("expanded");
                        entry.contentWrap.slideUp(200);
                        return true;
                    }
                };
                entry.isExpanded = function() {
                    return entry.container.hasClass("expanded");
                };
                if (options.fill && options.singleExpanded) {
                    resizeStack();
                }
                return entry;
            },

            hide: function() {
                visible = false;
                entries.forEach(function(entry) {
                    entry.container.hide();
                });
                return this;
            },

            show: function() {
                visible = true;
                entries.forEach(function(entry) {
                    entry.container.show();
                });
                return this;
            },
            resize: function() {
                if (resizeStack) {
                    resizeStack();
                }
            }
        }
    }

    return {
        create: createStack
    }
})();
;/**
 * 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.
 **/
(function($) {
    var contextParse = function(v,defaultStore) {
        var parts = RED.utils.parseContextKey(v, defaultStore&&defaultStore.value);
        return {
            option: parts.store,
            value: parts.key
        }
    }
    var contextExport = function(v,opt) {
        if (!opt) {
            return v;
        }
        var store = ((typeof opt === "string")?opt:opt.value)
        if (store !== RED.settings.context.default) {
            return "#:("+store+")::"+v;
        } else {
            return v;
        }
    }
    var contextLabel =  function(container,value) {
        var that = this;
        container.css("pointer-events","none");
        container.css("flex-grow",0);
        container.css("position",'relative');
        container.css("overflow",'visible');
        $('<div></div>').text(value).css({
            position: "absolute",
            bottom:"-2px",
            right: "5px",
            "font-size": "0.7em",
            opacity: 0.3
        }).appendTo(container);
        this.elementDiv.show();
    }
    var mapDeprecatedIcon = function(icon) {
        if (/^red\/images\/typedInput\/.+\.png$/.test(icon)) {
            icon = icon.replace(/.png$/,".svg");
        }
        return icon;
    }

    function getMatch(value, searchValue) {
        const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
        const len = idx > -1 ? searchValue.length : 0;
        return {
            index: idx,
            found: idx > -1,
            pre: value.substring(0,idx),
            match: value.substring(idx,idx+len),
            post: value.substring(idx+len),
            exact: idx === 0 && value.length === searchValue.length
        }
    }
    function generateSpans(match) {
        const els = [];
        if(match.pre) { els.push($('<span/>').text(match.pre)); }
        if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
        if(match.post) { els.push($('<span/>').text(match.post)); }
        return els;
    }
    
    const msgAutoComplete = function(options) {
        return function(val) {
            var matches = [];
            options.forEach(opt => {
                const optVal = opt.value;
                const optSrc = (opt.source||[]).join(",");
                const valMatch = getMatch(optVal, val);
                const srcMatch = getMatch(optSrc, val);
                if (valMatch.found || srcMatch.found) {
                    const element = $('<div>',{style: "display: flex"});
                    const valEl = $('<div/>',{ class: "red-ui-autoComplete-completion" });
                    valEl.append(generateSpans(valMatch));
                    valEl.appendTo(element);
                    if (optSrc) {
                        const optEl = $('<div>').css({ "font-size": "0.8em" });
                        optEl.append(generateSpans(srcMatch));
                        optEl.appendTo(element);
                    }
                    matches.push({
                        value: optVal,
                        label: element,
                        i: (valMatch.found ? valMatch.index : srcMatch.index)
                    });
                }
            })
            matches.sort(function(A,B){return A.i-B.i})
            return matches;
        }
    }

    function getEnvVars (obj, envVars = {}) {
        contextKnownKeys.env = contextKnownKeys.env || {}
        if (contextKnownKeys.env[obj.id]) {
            return contextKnownKeys.env[obj.id]
        }
        let parent
        if (obj.type === 'tab' || obj.type === 'subflow') {
            RED.nodes.eachConfig(function (conf) {
                if (conf.type === "global-config") {
                    parent = conf;
                }
            })
        } else if (obj.g) {
            parent = RED.nodes.group(obj.g)
        } else if (obj.z) {
            parent = RED.nodes.workspace(obj.z) || RED.nodes.subflow(obj.z)
        }
        if (parent) {
            getEnvVars(parent, envVars)
        }
        if (obj.env) {
            obj.env.forEach(env => {
                envVars[env.name] = obj
            })
        }
        contextKnownKeys.env[obj.id] = envVars
        return envVars
    }

    const envAutoComplete = function (val) {
        const editStack = RED.editor.getEditStack()
        if (editStack.length === 0) {
            done([])
            return
        }
        const editingNode = editStack.pop()
        if (!editingNode) {
            return []
        }
        const envVarsMap = getEnvVars(editingNode)
        const envVars = Object.keys(envVarsMap)
        const matches = []
        const i = val.lastIndexOf('${')
        let searchKey = val
        let isSubkey = false
        if (i > -1) {
            if (val.lastIndexOf('}') < i) {
                searchKey = val.substring(i+2)
                isSubkey = true
            }
        }
        envVars.forEach(v => {
            let valMatch = getMatch(v, searchKey);
            if (valMatch.found) {
                const optSrc = envVarsMap[v]
                const element = $('<div>',{style: "display: flex"});
                const valEl = $('<div/>',{ class: "red-ui-autoComplete-completion" });
                valEl.append(generateSpans(valMatch))
                valEl.appendTo(element)

                if (optSrc) {
                    const optEl = $('<div>').css({ "font-size": "0.8em" });
                    let label
                    if (optSrc.type === 'global-config') {
                        label = RED._('sidebar.context.global')
                    } else if (optSrc.type === 'group') {
                        label = RED.utils.getNodeLabel(optSrc) || (RED._('sidebar.info.group') + ': '+optSrc.id)
                    } else {
                        label = RED.utils.getNodeLabel(optSrc) || optSrc.id
                    }

                    optEl.append(generateSpans({ match: label }));
                    optEl.appendTo(element);
                }
                matches.push({
                    value: isSubkey ? val + v + '}' : v,
                    label: element,
                    i: valMatch.index
                });
            }
        })
        matches.sort(function(A,B){return A.i-B.i})
        return matches
    }

    let contextKnownKeys = {}
    let contextCache = {}
    if (RED.events) {
        RED.events.on("editor:close", function () {
            contextCache = {}
            contextKnownKeys = {}
        });
    }

    const contextAutoComplete = function() {
        const that = this
        const getContextKeysFromRuntime = function(scope, store, searchKey, done) {
            contextKnownKeys[scope] = contextKnownKeys[scope] || {}
            contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Map()
            if (searchKey.length > 0) {
                try {
                    RED.utils.normalisePropertyExpression(searchKey)
                } catch (err) {
                    // Not a valid context key, so don't try looking up
                    done()
                    return
                }
            }
            const url = `context/${scope}/${encodeURIComponent(searchKey)}?store=${store}&keysOnly`
            if (contextCache[url]) {
                // console.log('CACHED', url)
                done()
            } else {
                // console.log('GET', url)
                $.getJSON(url, function(data) {
                    // console.log(data)
                    contextCache[url] = true
                    const result = data[store] || {}
                    const keys = result.keys || []
                    const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '')
                    keys.forEach(keyInfo => {
                        const key = keyInfo.key
                        if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) {
                            contextKnownKeys[scope][store].set(keyPrefix + key, keyInfo)
                        } else {
                            contextKnownKeys[scope][store].set(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]", keyInfo)
                        }                        
                    })
                    done()
                })
            }
        }
        const getContextKeys = function(key, done) {
            const keyParts = key.split('.')
            const partialKey = keyParts.pop()
            let scope = that.propertyType
            if (scope === 'flow') {
                // Get the flow id of the node we're editing
                const editStack = RED.editor.getEditStack()
                if (editStack.length === 0) {
                    done(new Map())
                    return
                }
                const editingNode = editStack.pop()
                if (editingNode.z) {
                    scope = `${scope}/${editingNode.z}`
                } else {
                    done(new Map())
                    return
                }
            }
            const store = (contextStoreOptions.length === 1) ? contextStoreOptions[0].value : that.optionValue
            const searchKey = keyParts.join('.')
           
            getContextKeysFromRuntime(scope, store, searchKey, function() {
                if (contextKnownKeys[scope][store].has(key) || key.endsWith(']')) {
                    getContextKeysFromRuntime(scope, store, key, function() {
                        done(contextKnownKeys[scope][store])
                    })
                }
                done(contextKnownKeys[scope][store])
            })
        }

        return function(val, done) {
            getContextKeys(val, function (keys) {
                const matches = []
                keys.forEach((keyInfo, v) => {
                    let optVal = v
                    let valMatch = getMatch(optVal, val);
                    if (!valMatch.found && val.length > 0) {
                        if (val.endsWith('.')) {
                            // Search key ends in '.' - but doesn't match. Check again
                            // with [" at the end instead so we match bracket notation
                            valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["')
                        // } else if (val.endsWith('[') && /^array/.test(keyInfo.format)) {
                        //     console.log('this case')
                        }
                    }
                    if (valMatch.found) {
                        const element = $('<div>',{style: "display: flex"});
                        const valEl = $('<div/>',{ class: "red-ui-autoComplete-completion" });
                        // if (keyInfo.format) {
                        //     valMatch.post += ' ' + keyInfo.format
                        // }
                        if (valMatch.exact && /^array/.test(keyInfo.format)) {
                            valMatch.post += `[0-${keyInfo.length}]`
                            optVal +=  '['

                        }
                        valEl.append(generateSpans(valMatch))
                        valEl.appendTo(element)
                        matches.push({
                            value: optVal,
                            label: element,
                        });
                    }
                })
                matches.sort(function(a, b) { return a.value.localeCompare(b.value) });
                done(matches);
            })
        }
    }

    // This is a hand-generated list of completions for the core nodes (based on the node help html).
    var msgCompletions = [
        { value: "payload" },
        { value: "topic", source: ["mqtt","inject","rbe"] },
        { value: "action", source: ["mqtt"] },
        { value: "complete", source: ["join"] },
        { value: "contentType", source: ["mqtt"] },
        { value: "cookies", source: ["http request","http response"] },
        { value: "correlationData", source: ["mqtt"] },
        { value: "delay", source: ["delay","trigger"] },
        { value: "encoding", source: ["file"] },
        { value: "error", source: ["catch"] },
        { value: "error.message", source: ["catch"] },
        { value: "error.source", source: ["catch"] },
        { value: "error.source.id", source: ["catch"] },
        { value: "error.source.type", source: ["catch"] },
        { value: "error.source.name", source: ["catch"] },
        { value: "filename", source: ["file","file in"] },
        { value: "flush", source: ["delay"] },
        { value: "followRedirects", source: ["http request"] },
        { value: "headers", source: ["http response","http request"] },
        { value: "host", source: ["tcp request","http request"] },
        { value: "ip", source: ["udp out"] },
        { value: "kill", source: ["exec"] },
        { value: "messageExpiryInterval", source: ["mqtt"] },
        { value: "method", source: ["http request"] },
        { value: "options", source: ["xml"] },
        { value: "parts", source: ["split","join","batch","sort"] },
        { value: "pid", source: ["exec"] },
        { value: "port", source: ["tcp request"," udp out"] },
        { value: "qos", source: ["mqtt"] },
        { value: "rate", source: ["delay"] },
        { value: "rejectUnauthorized", source: ["http request"] },
        { value: "req", source: ["http in"]},
        { value: "req.body", source: ["http in"]},
        { value: "req.headers", source: ["http in"]},
        { value: "req.query", source: ["http in"]},
        { value: "req.params", source: ["http in"]},
        { value: "req.cookies", source: ["http in"]},
        { value: "req.files", source: ["http in"]},
        { value: "requestTimeout", source: ["http request"] },
        { value: "reset", source: ["delay","trigger","join","rbe"] },
        { value: "responseCookies", source: ["http request"] },
        { value: "responseTopic", source: ["mqtt"] },
        { value: "responseUrl", source: ["http request"] },
        { value: "restartTimeout", source: ["join"] },
        { value: "retain", source: ["mqtt"] },
        { value: "schema", source: ["json"] },
        { value: "select", source: ["html"] },
        { value: "statusCode", source: ["http response","http request"] },
        { value: "status", source: ["status"] },
        { value: "status.text", source: ["status"] },
        { value: "status.source", source: ["status"] },
        { value: "status.source.type", source: ["status"] },
        { value: "status.source.id", source: ["status"] },
        { value: "status.source.name", source: ["status"] },
        { value: "target", source: ["link call"] },
        { value: "template", source: ["template"] },
        { value: "toFront", source: ["delay"] },
        { value: "url", source: ["http request"] },
        { value: "userProperties", source: ["mqtt"] },
        { value: "_session", source: ["websocket out","tcp out"] },
    ]
    var allOptions = {
        msg: { value: "msg", label: "msg.", validate: RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions) },
        flow: { value: "flow", label: "flow.", hasValue: true,
            options: [],
            validate: RED.utils.validatePropertyExpression,
            parse: contextParse,
            export: contextExport,
            valueLabel: contextLabel,
            autoComplete: contextAutoComplete
        },
        global: {
            value: "global", label: "global.", hasValue: true,
            options: [],
            validate: RED.utils.validatePropertyExpression,
            parse: contextParse,
            export: contextExport,
            valueLabel: contextLabel,
            autoComplete: contextAutoComplete
        },
        str: { value: "str", label: "string", icon: "red/images/typedInput/az.svg" },
        num: { value: "num", label: "number", icon: "red/images/typedInput/09.svg", validate: function (v, o) {
            return RED.utils.validateTypedProperty(v, "num", o);
        } },
        bool: { value: "bool", label: "boolean", icon: "red/images/typedInput/bool.svg", options: ["true", "false"] },
        json: {
            value: "json",
            label: "JSON",
            icon: "red/images/typedInput/json.svg",
            validate: function (v, o) {
                return RED.utils.validateTypedProperty(v, "json", o);
            },
            expand: function () {
                var that = this;
                var value = this.value();
                try {
                    value = JSON.stringify(JSON.parse(value), null, 4);
                } catch (err) {
                }
                RED.editor.editJSON({
                    value: value,
                    stateId: RED.editor.generateViewStateId("typedInput", that, "json"),
                    focus: true,
                    complete: function (v) {
                        var value = v;
                        try {
                            value = JSON.stringify(JSON.parse(v));
                        } catch (err) {
                        }
                        that.value(value);
                    }
                })
            }
        },
        re: { value: "re", label: "regular expression", icon: "red/images/typedInput/re.svg" },
        date: {
            value: "date",
            label: "timestamp",
            icon: "fa fa-clock-o",
            options: [
                {
                    label: 'milliseconds since epoch',
                    value: ''
                },
                {
                    label: 'YYYY-MM-DDTHH:mm:ss.sssZ',
                    value: 'iso'
                },
                {
                    label: 'JavaScript Date Object',
                    value: 'object'
                }
            ]
        },
        jsonata: {
            value: "jsonata",
            label: "expression",
            icon: "red/images/typedInput/expr.svg",
            validate: function (v, o) {
                return RED.utils.validateTypedProperty(v, "jsonata", o);
            },
            expand: function () {
                var that = this;
                RED.editor.editExpression({
                    value: this.value().replace(/\t/g, "\n"),
                    stateId: RED.editor.generateViewStateId("typedInput", that, "jsonata"),
                    focus: true,
                    complete: function (v) {
                        that.value(v.replace(/\n/g, "\t"));
                    }
                })
            }
        },
        bin: {
            value: "bin",
            label: "buffer",
            icon: "red/images/typedInput/bin.svg",
            expand: function () {
                var that = this;
                RED.editor.editBuffer({
                    value: this.value(),
                    stateId: RED.editor.generateViewStateId("typedInput", that, "bin"),
                    focus: true,
                    complete: function (v) {
                        that.value(v);
                    }
                })
            }
        },
        env: {
            value: "env",
            label: "env variable",
            icon: "red/images/typedInput/env.svg",
            autoComplete: envAutoComplete
        },
        node: {
            value: "node",
            label: "node",
            icon: "red/images/typedInput/target.svg",
            valueLabel: function (container, value) {
                var node = RED.nodes.node(value);
                var nodeDiv = $('<div>', { class: "red-ui-search-result-node" }).css({
                    "margin-top": "2px",
                    "margin-left": "3px"
                }).appendTo(container);
                var nodeLabel = $('<span>').css({
                    "line-height": "32px",
                    "margin-left": "6px"
                }).appendTo(container);
                if (node) {
                    var colour = RED.utils.getNodeColor(node.type, node._def);
                    var icon_url = RED.utils.getNodeIcon(node._def, node);
                    if (node.type === 'tab') {
                        colour = "#C0DEED";
                    }
                    nodeDiv.css('backgroundColor', colour);
                    var iconContainer = $('<div/>', { class: "red-ui-palette-icon-container" }).appendTo(nodeDiv);
                    RED.utils.createIconElement(icon_url, iconContainer, true);
                    var l = RED.utils.getNodeLabel(node, node.id);
                    nodeLabel.text(l);
                } else {
                    nodeDiv.css({
                        'backgroundColor': '#eee',
                        'border-style': 'dashed'
                    });

                }
            },
            expand: function () {
                var that = this;
                RED.tray.hide();
                RED.view.selectNodes({
                    single: true,
                    selected: [that.value()],
                    onselect: function (selection) {
                        that.value(selection.id);
                        RED.tray.show();
                    },
                    oncancel: function () {
                        RED.tray.show();
                    }
                })
            }
        },
        cred: {
            value: "cred",
            label: "credential",
            icon: "fa fa-lock",
            inputType: "password",
            valueLabel: function (container, value) {
                var that = this;
                container.css("pointer-events", "none");
                container.css("flex-grow", 0);
                this.elementDiv.hide();
                var buttons = $('<div>').css({
                    position: "absolute",
                    right: "6px",
                    top: "6px",
                    "pointer-events": "all"
                }).appendTo(container);
                var eyeButton = $('<button type="button" class="red-ui-button red-ui-button-small"></button>').css({
                    width: "20px"
                }).appendTo(buttons).on("click", function (evt) {
                    evt.preventDefault();
                    var cursorPosition = that.input[0].selectionStart;
                    var currentType = that.input.attr("type");
                    if (currentType === "text") {
                        that.input.attr("type", "password");
                        eyeCon.removeClass("fa-eye-slash").addClass("fa-eye");
                        setTimeout(function () {
                            that.input.focus();
                            that.input[0].setSelectionRange(cursorPosition, cursorPosition);
                        }, 50);
                    } else {
                        that.input.attr("type", "text");
                        eyeCon.removeClass("fa-eye").addClass("fa-eye-slash");
                        setTimeout(function () {
                            that.input.focus();
                            that.input[0].setSelectionRange(cursorPosition, cursorPosition);
                        }, 50);
                    }
                }).hide();
                var eyeCon = $('<i class="fa fa-eye"></i>').css("margin-left", "-2px").appendTo(eyeButton);

                if (value === "__PWRD__") {
                    var innerContainer = $('<div><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i></div>').css({
                        padding: "6px 6px",
                        borderRadius: "4px"
                    }).addClass("red-ui-typedInput-value-label-inactive").appendTo(container);
                    var editButton = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-pencil"></i></button>').appendTo(buttons).on("click", function (evt) {
                        evt.preventDefault();
                        innerContainer.hide();
                        container.css("background", "none");
                        container.css("pointer-events", "none");
                        that.input.val("");
                        that.element.val("");
                        that.elementDiv.show();
                        editButton.hide();
                        cancelButton.show();
                        eyeButton.show();
                        setTimeout(function () {
                            that.input.focus();
                        }, 50);
                    });
                    var cancelButton = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-times"></i></button>').css("margin-left", "3px").appendTo(buttons).on("click", function (evt) {
                        evt.preventDefault();
                        innerContainer.show();
                        container.css("background", "");
                        that.input.val("__PWRD__");
                        that.element.val("__PWRD__");
                        that.elementDiv.hide();
                        editButton.show();
                        cancelButton.hide();
                        eyeButton.hide();
                        that.input.attr("type", "password");
                        eyeCon.removeClass("fa-eye-slash").addClass("fa-eye");

                    }).hide();
                } else {
                    container.css("background", "none");
                    container.css("pointer-events", "none");
                    this.elementDiv.show();
                    eyeButton.show();
                }
            }
        },
        'conf-types': {
            value: "conf-types",
            label: "config",
            icon: "fa fa-cog",
            // hasValue: false,
            valueLabel: function (container, value) {
                // get the selected option (for access to the "name" and "module" properties)
                const _options = this._optionsCache || this.typeList.find(opt => opt.value === value)?.options || []
                const selectedOption = _options.find(opt => opt.value === value) || {
                    title: '',
                    name: '',
                    module: ''
                }
                container.attr("title", selectedOption.title) // set tooltip to the full path/id of the module/node
                container.text(selectedOption.name) // apply the "name" of the selected option
                // set "line-height" such as to make the "name" appear further up, giving room for the "module" to be displayed below the value
                container.css("line-height", "1.4em")
                // add the module name in smaller, lighter font below the value
                $('<div></div>').text(selectedOption.module).css({
                    // "font-family": "var(--red-ui-monospace-font)",
                    color: "var(--red-ui-tertiary-text-color)",
                    "font-size": "0.8em",
                    "line-height": "1em",
                    opacity: 0.8
                }).appendTo(container);
            },
            // hasValue: false,
            options: function () {
                if (this._optionsCache) {
                    return this._optionsCache
                }
                const configNodes = RED.nodes.registry.getNodeDefinitions({configOnly: true, filter: (def) => def.type !== "global-config"}).map((def) => {
                    // create a container with with 2 rows (row 1 for the name, row 2 for the module name in smaller, lighter font)
                    const container = $('<div style="display: flex; flex-direction: column; justify-content: space-between; row-gap: 1px;">')
                    const row1Name = $('<div>').text(def.type)
                    const row2Module = $('<div style="font-size: 0.8em; color: var(--red-ui-tertiary-text-color);">').text(def.set.module)
                    container.append(row1Name, row2Module)
        
                    return {
                        value: def.type,
                        name: def.type,
                        enabled: def.set.enabled ?? true,
                        local: def.set.local,
                        title: def.set.id, // tooltip e.g. "node-red-contrib-foo/bar"
                        module: def.set.module,
                        icon: container[0].outerHTML.trim(), // the typeInput will interpret this as html text and render it in the anchor
                    }
                })
                this._optionsCache = configNodes
                return configNodes
            }
        }
    };

    
    // For a type with options, check value is a valid selection
    // If !opt.multiple, returns the valid option object
    // if opt.multiple, returns an array of valid option objects
    // If not valid, returns null;

    function isOptionValueValid(opt, currentVal) {
        let _options = opt.options
        if (typeof _options === "function") {
            _options = _options.call(this)
        }
        if (!opt.multiple) {
            for (var i=0;i<_options.length;i++) {
                op = _options[i];
                if (typeof op === "string" && op === currentVal) {
                    return {value:currentVal}
                } else if (op.value === currentVal) {
                    return op;
                }
            }
        } else {
            // Check to see if value is a valid csv of
            // options.
            var currentValues = {};
            var selected = [];
            currentVal.split(",").forEach(function(v) {
                if (v) {
                    currentValues[v] = true;
                }
            });
            for (var i=0;i<_options.length;i++) {
                op = _options[i];
                var val = typeof op === "string" ? op : op.value;
                if (currentValues.hasOwnProperty(val)) {
                    delete currentValues[val];
                    selected.push(typeof op === "string" ? {value:op} : op.value)
                }
            }
            if (!$.isEmptyObject(currentValues)) {
                return null;
            }
            return selected
        }
    }

    var nlsd = false;
    let contextStoreOptions;

    $.widget( "nodered.typedInput", {
        _create: function() {
            try {
            if (!nlsd && RED && RED._) {
                for (var i in allOptions) {
                    if (allOptions.hasOwnProperty(i)) {
                        allOptions[i].label = RED._("typedInput.type."+i,{defaultValue:allOptions[i].label});
                    }
                }
                var contextStores = RED.settings.context.stores;
                contextStoreOptions = contextStores.map(function(store) {
                    return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'}
                }).sort(function(A,B) {
                    if (A.value === RED.settings.context.default) {
                        return -1;
                    } else if (B.value === RED.settings.context.default) {
                        return 1;
                    } else {
                        return A.value.localeCompare(B.value);
                    }
                })
                if (contextStoreOptions.length < 2) {
                    allOptions.flow.options = [];
                    allOptions.global.options = [];
                } else {
                    allOptions.flow.options = contextStoreOptions;
                    allOptions.global.options = contextStoreOptions;
                }
                // Translate timestamp options
                allOptions.date.options.forEach(opt => {
                    opt.label = RED._("typedInput.date.format." + (opt.value || 'timestamp'), {defaultValue: opt.label})
                })
            }
            nlsd = true;
            var that = this;
            this.identifier = this.element.attr('id') || "TypedInput-"+Math.floor(Math.random()*100);
            if (this.options.debug) { console.log(this.identifier,"Create",{defaultType:this.options.default, value:this.element.val()}) }
            this.disarmClick = false;
            this.input = $('<input class="red-ui-typedInput-input" type="text"></input>');
            this.input.insertAfter(this.element);
            this.input.val(this.element.val());
            this.element.addClass('red-ui-typedInput');
            this.uiWidth = this.element.outerWidth();
            this.elementDiv = this.input.wrap("<div>").parent().addClass('red-ui-typedInput-input-wrap');
            this.uiSelect = this.elementDiv.wrap( "<div>" ).parent();
            var attrStyle = this.element.attr('style');
            var m;
            if ((m = /width\s*:\s*(calc\s*\(.*\)|\d+(%|px))/i.exec(attrStyle)) !== null) {
                this.input.css('width','100%');
                this.uiSelect.width(m[1]);
                this.uiWidth = null;
            } else if (this.uiWidth !== 0){
                this.uiSelect.width(this.uiWidth);
            }
            ["Right","Left"].forEach(function(d) {
                var m = that.element.css("margin"+d);
                that.uiSelect.css("margin"+d,m);
                that.input.css("margin"+d,0);
            });

            ["type","placeholder","autocomplete","data-i18n"].forEach(function(d) {
                var m = that.element.attr(d);
                that.input.attr(d,m);
            });

            this.defaultInputType = this.input.attr('type');
            // Used to remember selections per-type to restore them when switching between types
            this.oldValues = {};

            this.uiSelect.addClass("red-ui-typedInput-container");

            this.element.attr('type','hidden');

            if (!this.options.types && this.options.type) {
                this.options.types = [this.options.type]
            } else {
                this.options.types = this.options.types||Object.keys(allOptions);
            }

            this.selectTrigger = $('<button type="button" class="red-ui-typedInput-type-select" tabindex="0"></button>').prependTo(this.uiSelect);
            $('<i class="red-ui-typedInput-icon fa fa-caret-down"></i>').toggle(this.options.types.length > 1).appendTo(this.selectTrigger);

            this.selectLabel = $('<span class="red-ui-typedInput-type-label"></span>').appendTo(this.selectTrigger);

            this.valueLabelContainer = $('<div class="red-ui-typedInput-value-label">').appendTo(this.uiSelect)

            this.types(this.options.types);

            if (this.options.typeField) {
                this.typeField = $(this.options.typeField).hide();
                var t = this.typeField.val();
                if (t && this.typeMap[t]) {
                    this.options.default = t;
                }
            } else {
                this.typeField = $("<input>",{type:'hidden'}).appendTo(this.uiSelect);
            }

            this.input.on('focus', function() {
                that.uiSelect.addClass('red-ui-typedInput-focus');
            });
            this.input.on('blur', function() {
                that.uiSelect.removeClass('red-ui-typedInput-focus');
            });
            this.input.on('change', function() {
                that.validate();
                that.element.val(that.value());
                that.element.trigger('change',[that.propertyType,that.value()]);
            });
            this.input.on('keyup', function(evt) {
                that.validate();
                that.element.val(that.value());
                that.element.trigger('keyup',evt);
            });
            this.input.on('paste', function(evt) {
                that.validate();
                that.element.val(that.value());
                that.element.trigger('paste',evt);
            });
            this.input.on('keydown', function(evt) {
                if (that.typeMap[that.propertyType].autoComplete || that.input.hasClass('red-ui-autoComplete')) {
                    return
                }
                if (evt.keyCode >= 37 && evt.keyCode <= 40) {
                    evt.stopPropagation();
                }
            })
            this.selectTrigger.on("click", function(event) {
                event.preventDefault();
                event.stopPropagation();
                that._showTypeMenu();
            });
            this.selectTrigger.on('keydown',function(evt) {
                if (evt.keyCode === 40) {
                    // Down
                    that._showTypeMenu();
                }
                evt.stopPropagation();
            }).on('focus', function() {
                that.uiSelect.addClass('red-ui-typedInput-focus');
            }).on('blur', function() {
                var opt = that.typeMap[that.propertyType];
                if (opt.hasValue === false) {
                    that.uiSelect.removeClass('red-ui-typedInput-focus');
                }
            })

            // explicitly set optionSelectTrigger display to inline-block otherwise jQ sets it to 'inline'
            this.optionSelectTrigger = $('<button type="button" tabindex="0" class="red-ui-typedInput-option-trigger" style="display:inline-block"><span class="red-ui-typedInput-option-caret"><i class="red-ui-typedInput-icon fa fa-caret-down"></i></span></button>').appendTo(this.uiSelect);
            this.optionSelectLabel = $('<span class="red-ui-typedInput-option-label"></span>').prependTo(this.optionSelectTrigger);
            // RED.popover.tooltip(this.optionSelectLabel,function() {
            //     return that.optionValue;
            // });
            this.optionSelectTrigger.on("click", function(event) {
                event.preventDefault();
                event.stopPropagation();
                that._showOptionSelectMenu();
            }).on('keydown', function(evt) {
                if (evt.keyCode === 40) {
                    // Down
                    that._showOptionSelectMenu();
                }
                evt.stopPropagation();
            }).on('blur', function() {
                that.uiSelect.removeClass('red-ui-typedInput-focus');
            }).on('focus', function() {
                that.uiSelect.addClass('red-ui-typedInput-focus');
            });

            this.optionExpandButton = $('<button type="button" tabindex="0" class="red-ui-typedInput-option-expand" style="display:inline-block"></button>').appendTo(this.uiSelect);
            this.optionExpandButtonIcon = $('<i class="red-ui-typedInput-icon fa fa-ellipsis-h"></i>').appendTo(this.optionExpandButton);

            this.type(this.typeField.val() || this.options.default||this.typeList[0].value);
            this.typeChanged = !!this.options.default;
        }catch(err) {
            console.log(err.stack);
        }
        },
        _showTypeMenu: function() {
            if (this.typeList.length > 1) {
                this._showMenu(this.menu,this.selectTrigger);
                var selected = this.menu.find("[value='"+this.propertyType+"']");
                setTimeout(function() {
                    selected.trigger("focus");
                },120);
            } else {
                this.input.trigger("focus");
            }
        },
        _showOptionSelectMenu: function() {
            if (this.optionMenu) {
                this.optionMenu.css({
                    minWidth:this.optionSelectLabel.width()
                });

                this._showMenu(this.optionMenu,this.optionSelectTrigger);
                var targetValue = this.optionValue;
                if (this.optionValue === null || this.optionValue === undefined) {
                    targetValue = this.value();
                }
                var selectedOption = this.optionMenu.find("[value='"+targetValue+"']");
                if (selectedOption.length === 0) {
                    selectedOption = this.optionMenu.children(":first");
                }
                selectedOption.trigger("focus");

            }
        },
        _hideMenu: function(menu) {
            $(document).off("mousedown.red-ui-typedInput-close-property-select");
            menu.hide();
            menu.css({
                height: "auto"
            });

            if (menu.opts.multiple) {
                var selected = [];
                menu.find('input[type="checkbox"]').each(function() {
                    if ($(this).prop("checked")) {
                        selected.push($(this).data('value'))
                    }
                })
                menu.callback(selected);
            }

            if (this.elementDiv.is(":visible")) {
                this.input.trigger("focus");
            } else if (this.optionSelectTrigger.is(":visible")){
                this.optionSelectTrigger.trigger("focus");
            } else {
                this.selectTrigger.trigger("focus");
            }
        },
        _createMenu: function(menuOptions,opts,callback) {
            var that = this;
            var menu = $("<div>").addClass("red-ui-typedInput-options red-ui-editor-dialog");
            menu.opts = opts;
            menu.callback = callback;
            menuOptions.forEach(function(opt) {
                if (typeof opt === 'string') {
                    opt = {value:opt,label:opt};
                }
                var op = $('<a href="#"></a>').attr("value",opt.value).appendTo(menu);
                if (opt.label) {
                    op.text(opt.label);
                }
                if (opt.title) {
                    op.prop('title', opt.title)
                }
                if (opt.icon) {
                    if (opt.icon.indexOf("<") === 0) {
                        $(opt.icon).prependTo(op);
                    } else if (opt.icon.indexOf("/") !== -1) {
                        $('<i>',{class:"red-ui-typedInput-icon", style:"mask-image: url("+opt.icon+"); -webkit-mask-image: url("+opt.icon+");"}).prependTo(op);
                    } else {
                        $('<i>',{class:"red-ui-typedInput-icon "+opt.icon}).prependTo(op);
                    }
                } else {
                    op.css({paddingLeft: "18px"});
                }
                if (!opt.icon && !opt.label) {
                    op.text(opt.value);
                }
                var cb;
                if (opts.multiple) {
                    cb = $('<input type="checkbox">').css("pointer-events","none").data('value',opt.value).prependTo(op).on("mousedown", function(evt) { evt.preventDefault() });
                }

                op.on("click", function(event) {
                    event.preventDefault();
                    event.stopPropagation();
                    if (!opts.multiple) {
                        callback(opt.value);
                        that._hideMenu(menu);
                    } else {
                        cb.prop("checked",!cb.prop("checked"));
                    }
                });
            });
            menu.css({
                display: "none"
            });
            menu.appendTo(document.body);

            menu.on('keydown', function(evt) {
                if (evt.keyCode === 40) {
                    evt.preventDefault();
                    // DOWN
                    $(this).children(":focus").next().trigger("focus");
                } else if (evt.keyCode === 38) {
                    evt.preventDefault();
                    // UP
                    $(this).children(":focus").prev().trigger("focus");
                } else if (evt.keyCode === 27) {
                    // ESCAPE
                    evt.preventDefault();
                    that._hideMenu(menu);
                }
                evt.stopPropagation();
            })
            return menu;

        },
        _showMenu: function(menu,relativeTo) {
            if (this.disarmClick) {
                this.disarmClick = false;
                return
            }
            if (menu.opts.multiple) {
                var selected = {};
                this.value().split(",").forEach(function(f) {
                    selected[f] = true;
                });
                menu.find('input[type="checkbox"]').each(function() {
                    $(this).prop("checked", selected[$(this).data('value')] || false);
                });
            }


            var that = this;
            var pos = relativeTo.offset();
            var height = relativeTo.height();
            var menuHeight = menu.height();
            var top = (height+pos.top);
            if (top+menuHeight-$(document).scrollTop() > $(window).height()) {
                top -= (top+menuHeight)-$(window).height()+5;
            }
            if (top < 0) {
                menu.height(menuHeight+top)
                top = 0;
            }
            menu.css({
                top: top+"px",
                left: (pos.left)+"px",
            });
            menu.slideDown(100);
            this._delay(function() {
                that.uiSelect.addClass('red-ui-typedInput-focus');
                $(document).on("mousedown.red-ui-typedInput-close-property-select", function(event) {
                    if(!$(event.target).closest(menu).length) {
                        that._hideMenu(menu);
                    }
                    if ($(event.target).closest(relativeTo).length) {
                        that.disarmClick = true;
                        event.preventDefault();
                    }
                })
            });
        },
        _getLabelWidth: function(label, done) {
            var labelWidth = label.outerWidth();
            if (labelWidth === 0) {
                var wrapper = $('<div class="red-ui-editor"></div>').css({
                    position:"absolute",
                    "white-space": "nowrap",
                    top:-2000
                }).appendTo(document.body);
                var container = $('<div class="red-ui-typedInput-container"></div>').appendTo(wrapper);
                var newTrigger = label.clone().appendTo(container);
                setTimeout(function() {
                    labelWidth = newTrigger.outerWidth();
                    wrapper.remove();
                    done(labelWidth);
                },50)
            } else {
                done(labelWidth);
            }
        },
        _updateOptionSelectLabel: function(o) {
            var opt = this.typeMap[this.propertyType];
            this.optionSelectLabel.empty();
            if (opt.hasValue) {
                this.valueLabelContainer.empty();
                this.valueLabelContainer.show();
            } else {
                this.valueLabelContainer.hide();
            }
            if (this.typeMap[this.propertyType].valueLabel) {
                if (opt.multiple) {
                    this.typeMap[this.propertyType].valueLabel.call(this,opt.hasValue?this.valueLabelContainer:this.optionSelectLabel,o);
                } else {
                    this.typeMap[this.propertyType].valueLabel.call(this,opt.hasValue?this.valueLabelContainer:this.optionSelectLabel,o.value);
                }
            }
            if (!this.typeMap[this.propertyType].valueLabel || opt.hasValue) {
                if (!opt.multiple) {
                    if (o.icon) {
                        if (o.icon.indexOf("<") === 0) {
                            $(o.icon).prependTo(this.optionSelectLabel);
                        } else if (o.icon.indexOf("/") !== -1) {
                            // url
                            $('<img>',{src:mapDeprecatedIcon(o.icon),style:"height: 18px;"}).prependTo(this.optionSelectLabel);
                        } else {
                            // icon class
                            $('<i>',{class:"red-ui-typedInput-icon "+o.icon}).prependTo(this.optionSelectLabel);
                        }
                    } else if (o.label) {
                        this.optionSelectLabel.text(o.label);
                    } else {
                        this.optionSelectLabel.text(o.value);
                    }
                    if (opt.hasValue) {
                        this.optionValue = o.value;
                        this.input.trigger('change',[this.propertyType,this.value()]);
                    }
                } else {
                    this.optionSelectLabel.text(RED._("typedInput.selected", { count: o.length }));
                }
            }
        },
        _destroy: function() {
            if (this.optionMenu) {
                this.optionMenu.remove();
            }
            if (this.menu) {
                this.menu.remove();
            }
            this.uiSelect.remove();
        },
        types: function(types) {
            var that = this;
            var currentType = this.type();
            this.typeMap = {};
            var firstCall = (this.typeList === undefined);
            this.typeList = types.map(function(opt) {
                var result;
                if (typeof opt === 'string') {
                    result = allOptions[opt];
                } else {
                    result = opt;
                }
                that.typeMap[result.value] = result;
                return result;
            });
            if (this.typeList.length < 2) {
                this.selectTrigger.attr("tabindex", -1)
                this.selectTrigger.on("mousedown.red-ui-typedInput-focus-block", function(evt) { evt.preventDefault(); })
            } else {
                this.selectTrigger.attr("tabindex", 0)
                this.selectTrigger.off("mousedown.red-ui-typedInput-focus-block")
            }
            this.selectTrigger.toggleClass("disabled", this.typeList.length === 1);
            this.selectTrigger.find(".fa-caret-down").toggle(this.typeList.length > 1)
            if (this.menu) {
                this.menu.remove();
            }
            this.menu = this._createMenu(this.typeList,{},function(v) { that.type(v) });
            if (currentType && !this.typeMap.hasOwnProperty(currentType)) {
                if (!firstCall) {
                    this.type(this.typeList[0]?.value || ""); // permit empty typeList
                }
            } else {
                this.propertyType = null;
                if (!firstCall) {
                    this.type(currentType);
                }
            }
            if (this.typeList.length === 1 && !this.typeList[0].icon && (!this.typeList[0].label || this.typeList[0].showLabel === false)) {
                this.selectTrigger.hide()
            } else {
                this.selectTrigger.show()
            }
        },
        width: function(desiredWidth) {
            this.uiWidth = desiredWidth;
            if (this.uiWidth !== null) {
                this.uiSelect.width(this.uiWidth);
            }
        },
        value: function(value) {
            var that = this;
            // If the default type has been set to an invalid type, then on first
            // creation, the current propertyType will not exist. Default to an
            // empty object on the assumption the corrent type will be set shortly
            var opt = this.typeMap[this.propertyType] || {};
            if (!arguments.length) {
                var v = this.input.val();
                if (opt.export) {
                    v = opt.export(v,this.optionValue)
                }
                return v;
            } else {
                if (this.options.debug) { console.log(this.identifier,"----- SET VALUE ------",value) }
                var selectedOption = [];
                var valueToCheck = value;
                if (opt.options) {
                    let _options = opt.options
                    if (typeof opt.options === "function") {
                        _options = opt.options.call(this)
                    }

                    if (opt.hasValue && opt.parse) {
                        var parts = opt.parse(value);
                        if (this.options.debug) { console.log(this.identifier,"new parse",parts) }
                        value = parts.value;
                        valueToCheck = parts.option || parts.value;
                    }

                    var checkValues = [valueToCheck];
                    if (opt.multiple) {
                        selectedOption = [];
                        checkValues = valueToCheck.split(",");
                    }
                    checkValues.forEach(function(valueToCheck) {
                        for (var i=0;i<_options.length;i++) {
                            var op = _options[i];
                            if (typeof op === "string") {
                                if (op === valueToCheck || op === ""+valueToCheck) {
                                    selectedOption.push(that.activeOptions[op]);
                                    break;
                                }
                            } else if (op.value === valueToCheck) {
                                selectedOption.push(op);
                                break;
                            }
                        }
                    })
                    if (this.options.debug) { console.log(this.identifier,"set value to",value) }

                    this.input.val(value);
                    if (!opt.multiple) {
                        if (selectedOption.length === 0) {
                            selectedOption = [{value:""}];
                        }
                        this._updateOptionSelectLabel(selectedOption[0])
                    } else {
                        this._updateOptionSelectLabel(selectedOption)
                    }
                } else {
                    this.input.val(value);
                    if (opt.valueLabel) {
                        this.valueLabelContainer.empty();
                        opt.valueLabel.call(this,this.valueLabelContainer,value);
                    }
                }
                this.input.trigger('change',[this.type(),value]);
            }
        },
        type: function(type) {
            if (!arguments.length) {
                return this.propertyType || this.options?.default || '';
            } else {
                var that = this;
                if (this.options.debug) { console.log(this.identifier,"----- SET TYPE -----",type) }
                var previousValue = null;
                var opt = this.typeMap[type];
                if (opt && this.propertyType !== type) {
                    // If previousType is !null, then this is a change of the type, rather than the initialisation
                    var previousType = this.typeMap[this.propertyType];
                    previousValue = this.input.val();
                    if (this.input.hasClass('red-ui-autoComplete')) {
                        this.input.autoComplete("destroy");
                    }

                    if (previousType && this.typeChanged) {
                        if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) }
                        if (previousType.options && opt.hasValue !== true) {
                            this.oldValues[previousType.value] = previousValue;
                        } else if (previousType.hasValue === false) {
                            this.oldValues[previousType.value] = previousValue;
                        } else {
                            this.oldValues["_"] = previousValue;
                        }
                        if ((opt.options && opt.hasValue !== true) || opt.hasValue === false) {
                            if (this.oldValues.hasOwnProperty(opt.value)) {
                                if (this.options.debug) { console.log(this.identifier,"restored previous (1)",this.oldValues[opt.value]) }
                                this.input.val(this.oldValues[opt.value]);
                            } else if (opt.options) {
                                // No old value for the option type.
                                // It is possible code has called 'value' then 'type'
                                // to set the selected option. This is what the Inject/Switch/Change
                                // nodes did before 2.1.
                                // So we need to be careful to not reset the value if it is a valid option.
                                var validOptions = isOptionValueValid(opt,previousValue);
                                if (this.options.debug) { console.log(this.identifier,{previousValue,opt,validOptions}) }
                                if ((previousValue || previousValue === '') && validOptions) {
                                    if (this.options.debug) { console.log(this.identifier,"restored previous (2)") }
                                    this.input.val(previousValue);
                                } else {
                                    if (typeof opt.default === "string") {
                                        if (this.options.debug) { console.log(this.identifier,"restored previous (3)",opt.default) }
                                        this.input.val(opt.default);
                                    } else if (Array.isArray(opt.default)) {
                                        if (this.options.debug) { console.log(this.identifier,"restored previous (4)",opt.default.join(",")) }
                                        this.input.val(opt.default.join(","))
                                    } else {
                                        if (this.options.debug) { console.log(this.identifier,"restored previous (5)") }
                                        this.input.val("");
                                    }
                                }
                            } else {
                                if (this.options.debug) { console.log(this.identifier,"restored default/blank",opt.default||"") }
                                this.input.val(opt.default||"")
                            }
                        } else {
                            if (this.options.debug) { console.log(this.identifier,"restored old/default/blank") }
                            this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||""))
                        }
                        if (previousType.autoComplete) {
                            if (this.input.hasClass('red-ui-autoComplete')) {
                                this.input.autoComplete("destroy");
                            }
                        }
                    }
                    this.propertyType = type;
                    this.typeChanged = true;
                    if (this.typeField) {
                        this.typeField.val(type);
                    }
                    this.selectLabel.empty();
                    var image;
                    if (opt.icon && opt.showLabel !== false) {
                        if (opt.icon.indexOf("<") === 0) {
                            $(opt.icon).prependTo(this.selectLabel);
                        }
                        else if (opt.icon.indexOf("/") !== -1) {
                            $('<i>',{class:"red-ui-typedInput-icon", style:"mask-image: url("+opt.icon+"); -webkit-mask-image: url("+opt.icon+"); margin-right: 4px;height: 18px;width:13px"}).prependTo(this.selectLabel);
                        }
                        else {
                            $('<i>',{class:"red-ui-typedInput-icon "+opt.icon,style:"min-width: 13px; margin-right: 4px;"}).prependTo(this.selectLabel);
                        }
                    }
                    if (opt.hasValue === false || (opt.showLabel !== false && !opt.icon)) {
                        this.selectLabel.text(opt.label);
                    }
                    if (opt.label) {
                        this.selectTrigger.attr("title",opt.label);
                    } else {
                        this.selectTrigger.attr("title","");
                    }
                    if (opt.hasValue === false) {
                        this.selectTrigger.addClass("red-ui-typedInput-full-width");
                    } else {
                        this.selectTrigger.removeClass("red-ui-typedInput-full-width");
                    }

                    if (this.optionMenu) {
                        this.optionMenu.remove();
                        this.optionMenu = null;
                    }
                    if (opt.options) {
                        let _options = opt.options
                        if (typeof _options === "function") {
                            _options = opt.options.call(this);
                        }
                        if (this.optionExpandButton) {
                            this.optionExpandButton.hide();
                            this.optionExpandButton.shown = false;
                        }
                        if (this.optionSelectTrigger) {
                            this.optionSelectTrigger.css({"display":"inline-flex"});
                            if (!opt.hasValue) {
                                this.optionSelectTrigger.css({"flex-grow":1})
                                this.elementDiv.hide();
                                this.valueLabelContainer.hide();
                            } else {
                                this.optionSelectTrigger.css({"flex-grow":0})
                                this.elementDiv.show();
                                this.valueLabelContainer.hide();
                            }
                            this.activeOptions = {};
                            _options.forEach(function(o) {
                                if (typeof o === 'string') {
                                    that.activeOptions[o] = {label:o,value:o};
                                } else {
                                    that.activeOptions[o.value] = o;
                                }
                            });

                            if (!that.activeOptions.hasOwnProperty(that.optionValue)) {
                                that.optionValue = null;
                            }

                            var op;
                            if (!opt.hasValue) {
                                // Check the value is valid for the available options
                                var validValues = isOptionValueValid(opt,this.input.val());
                                if (!opt.multiple) {
                                    if (validValues) {
                                        that._updateOptionSelectLabel(validValues)
                                    } else {
                                        op = _options[0] || {value:""}; // permit zero options
                                        if (typeof op === "string") {
                                            this.value(op);
                                            that._updateOptionSelectLabel({value:op});
                                        } else {
                                            this.value(op.value);
                                            that._updateOptionSelectLabel(op);
                                        }
                                    }
                                } else {
                                    if (!validValues) {
                                        validValues = (opt.default || []).map(function(v) {
                                            return typeof v === "string"?v:v.value
                                        });
                                        this.value(validValues.join(","));
                                    }
                                    that._updateOptionSelectLabel(validValues);
                                }
                            } else {
                                var selectedOption = this.optionValue||_options[0];
                                if (opt.parse) {
                                    var selectedOptionObj = typeof selectedOption === "string"?{value:selectedOption}:selectedOption
                                    var parts = opt.parse(this.input.val(),selectedOptionObj);
                                    if (parts.option) {
                                        selectedOption = parts.option;
                                        if (!this.activeOptions.hasOwnProperty(selectedOption)) {
                                            parts.option = Object.keys(this.activeOptions)[0];
                                            selectedOption = parts.option
                                        }
                                    }
                                    this.input.val(parts.value);
                                    if (opt.export) {
                                        this.element.val(opt.export(parts.value,parts.option||selectedOption));
                                    }
                                }
                                if (typeof selectedOption === "string") {
                                    this.optionValue = selectedOption;
                                    if (!this.activeOptions.hasOwnProperty(selectedOption)) {
                                        selectedOption = Object.keys(this.activeOptions)[0];
                                    }
                                    if (!selectedOption) {
                                        this.optionSelectTrigger.hide();
                                    } else {
                                        this._updateOptionSelectLabel(this.activeOptions[selectedOption]);
                                    }
                                } else if (selectedOption) {
                                    if (this.options.debug) { console.log(this.identifier,"HERE",{optionValue:selectedOption.value}) }
                                    this.optionValue = selectedOption.value;
                                    this._updateOptionSelectLabel(selectedOption);
                                } else {
                                    this.optionSelectTrigger.hide();
                                }
                                if (opt.autoComplete) {
                                    let searchFunction = opt.autoComplete
                                    if (searchFunction.length === 0) {
                                        searchFunction = opt.autoComplete.call(this)
                                    }
                                    this.input.autoComplete({
                                        search: searchFunction,
                                        minLength: 0
                                    })
                                }
                            }
                            this.optionMenu = this._createMenu(_options,opt,function(v){
                                if (!opt.multiple) {
                                    that._updateOptionSelectLabel(that.activeOptions[v]);
                                    if (!opt.hasValue) {
                                        that.value(that.activeOptions[v].value)
                                    }
                                } else {
                                    that._updateOptionSelectLabel(v);
                                    if (!opt.hasValue) {
                                        that.value(v.join(","))
                                    }
                                }
                            });
                        }
                        this._trigger("typechange",null,this.propertyType);
                        this.input.trigger('change',[this.propertyType,this.value()]);
                    } else {
                        if (this.optionSelectTrigger) {
                            this.optionSelectTrigger.hide();
                        }
                        if (opt.inputType) {
                            this.input.attr('type',opt.inputType)
                        } else {
                            this.input.attr('type',this.defaultInputType)
                        }
                        if (opt.hasValue === false) {
                            this.elementDiv.hide();
                            this.valueLabelContainer.hide();
                        } else if (opt.valueLabel) {
                            // Reset any CSS the custom label may have set
                            this.valueLabelContainer.css("pointer-events","");
                            this.valueLabelContainer.css("flex-grow",1);
                            this.valueLabelContainer.css("overflow","hidden");
                            this.valueLabelContainer.show();
                            this.valueLabelContainer.empty();
                            this.elementDiv.hide();
                            opt.valueLabel.call(this,this.valueLabelContainer,this.input.val());
                        } else {
                            this.valueLabelContainer.hide();
                            this.elementDiv.show();
                            if (opt.autoComplete) {
                                let searchFunction = opt.autoComplete
                                if (searchFunction.length === 0) {
                                    searchFunction = opt.autoComplete.call(this)
                                }
                                this.input.autoComplete({
                                    search: searchFunction,
                                    minLength: 0
                                })
                            }
                        }
                        if (this.optionExpandButton) {
                            if (opt.expand) {
                                if (opt.expand.icon) {
                                    this.optionExpandButtonIcon.removeClass().addClass("red-ui-typedInput-icon fa "+opt.expand.icon)
                                } else {
                                    this.optionExpandButtonIcon.removeClass().addClass("red-ui-typedInput-icon fa fa-ellipsis-h")
                                }
                                this.optionExpandButton.shown = true;
                                this.optionExpandButton.show();
                                this.optionExpandButton.off('click');
                                this.optionExpandButton.on('click',function(evt) {
                                    evt.preventDefault();
                                    if (typeof opt.expand === 'function') {
                                        opt.expand.call(that);
                                    } else {
                                        var container = $('<div>');
                                        var content = opt.expand.content.call(that,container);
                                        var panel = RED.popover.panel(container);
                                        panel.container.css({
                                            width:that.valueLabelContainer.width()
                                        });
                                        if (opt.expand.minWidth) {
                                            panel.container.css({
                                                minWidth: opt.expand.minWidth+"px"
                                            });
                                        }
                                        panel.show({
                                            target:that.optionExpandButton,
                                            onclose:content.onclose,
                                            align: "left"
                                        });
                                    }
                                })
                            } else {
                                this.optionExpandButton.shown = false;
                                this.optionExpandButton.hide();
                            }
                        }
                        this._trigger("typechange",null,this.propertyType);
                        this.input.trigger('change',[this.propertyType,this.value()]);
                    }
                }
            }
        },
        validate: function(options) {
            let valid = true;
            const value = this.value();
            const type = this.type();
            if (this.typeMap[type] && this.typeMap[type].validate) {
                const validate = this.typeMap[type].validate;
                if (typeof validate === 'function') {
                    valid = validate(value, {});
                } else {
                    // Regex
                    valid = validate.test(value);
                    if (!valid) {
                        valid = RED._("validator.errors.invalid-regexp");
                    }
                }
            }
            if ((typeof valid === "string") || !valid) {
                this.element.addClass("input-error");
                this.uiSelect.addClass("input-error");
                if (typeof valid === "string") {
                    let tooltip = this.element.data("tooltip");
                    if (tooltip) {
                        tooltip.setContent(valid);
                    } else {
                        const target = this.typeMap[type]?.options ? this.optionSelectLabel : this.elementDiv;
                        tooltip = RED.popover.tooltip(target, valid);
                        this.element.data("tooltip", tooltip);
                    }
                }
            } else {
                this.element.removeClass("input-error");
                this.uiSelect.removeClass("input-error");
                const tooltip = this.element.data("tooltip");
                if (tooltip) {
                    this.element.data("tooltip", null);
                    tooltip.delete();
                }
            }
            if (options?.returnErrorMessage === true) {
                return valid;
            }
            // Must return a boolean for no 3.x validator
            return (typeof valid === "string") ? false : valid;
        },
        show: function() {
            this.uiSelect.show();
        },
        hide: function() {
            this.uiSelect.hide();
        },
        disable: function(val) {
            if(val === undefined || !!val ) {
                this.uiSelect.attr("disabled", "disabled");
            } else {
                this.uiSelect.attr("disabled", null); //remove attr
            }
        },
        enable: function() {
            this.uiSelect.attr("disabled", null); //remove attr
        },
        disabled: function() {
            return this.uiSelect.attr("disabled") === "disabled";
        },
        focus: function() {
            this.input.focus();
        }
    });
})(jQuery);
;/**
 * 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.
 **/
(function($) {

/**
 * options:
 *   - invertState   : boolean - if "true" the button will show "enabled" when the
 *                             checkbox is not selected and vice versa.
 *   - enabledIcon   : string - the icon for "enabled" state, default "fa-check-square-o"
 *   - enabledLabel  : string - the label for "enabled" state, default "Enabled" ("editor:workspace.enabled")
 *   - disabledIcon  : string - the icon for "disabled" state, default "fa-square-o"
 *   - disabledLabel : string - the label for "disabled" state, default "Disabled" ("editor:workspace.disabled")
 *   - baseClass     : string - the base css class to apply, default "red-ui-button" (alternative eg "red-ui-sidebar-header-button")
 *   - class         : string - additional classes to apply to the button - eg "red-ui-button-small"
 * methods:
 *   -
 */
    $.widget( "nodered.toggleButton", {
        _create: function() {
            var that = this;

            var invertState = false;
            if (this.options.hasOwnProperty("invertState")) {
                invertState = this.options.invertState;
            }
            var baseClass = this.options.baseClass || "red-ui-button";
            var enabledIcon = this.options.hasOwnProperty('enabledIcon')?this.options.enabledIcon : "fa-check-square-o";
            var disabledIcon = this.options.hasOwnProperty('disabledIcon')?this.options.disabledIcon : "fa-square-o";
            var enabledLabel = this.options.hasOwnProperty('enabledLabel') ? this.options.enabledLabel : RED._("editor:workspace.enabled");
            var disabledLabel = this.options.hasOwnProperty('disabledLabel') ? this.options.disabledLabel : RED._("editor:workspace.disabled");

            this.element.css("display","none");
            this.element.on("focus", function() {
                that.button.focus();
            });
            this.button = $('<button type="button" class="red-ui-toggleButton '+baseClass+' toggle single"></button>');
            if (enabledLabel || disabledLabel) {
                this.buttonLabel = $("<span>").appendTo(this.button).css("margin-left", "5px");
            }

            if (this.options.class) {
                this.button.addClass(this.options.class)
            }
            this.element.after(this.button);

            if (enabledIcon && disabledIcon) {
                this.buttonIcon = $('<i class="fa"></i>').prependTo(this.button);
            }

            // Quick hack to find the maximum width of the button
            this.button.addClass("selected");
            if (this.buttonIcon) {
                this.buttonIcon.addClass(enabledIcon);
            }
            if (this.buttonLabel) {
                this.buttonLabel.text(enabledLabel);
            }
            var width = this.button.width();
            this.button.removeClass("selected");
            if (this.buttonIcon) {
                this.buttonIcon.removeClass(enabledIcon);
                that.buttonIcon.addClass(disabledIcon);
            }
            if (this.buttonLabel) {
                that.buttonLabel.text(disabledLabel);
            }
            width = Math.max(width,this.button.width());
            if (this.buttonIcon) {
                this.buttonIcon.removeClass(disabledIcon);
            }

            // Fix the width of the button so it doesn't jump around when toggled
            if (width > 0) {
                this.button.width(Math.ceil(width));
            }

            this.button.on("click",function(e) {
                e.stopPropagation();
                if (!that.state) {
                    that.element.prop("checked",!invertState);
                } else {
                    that.element.prop("checked",invertState);
                }
                that.element.trigger("change");
            })

            this.element.on("change", function(e) {
                if ($(this).prop("checked") !== invertState) {
                    that.button.addClass("selected");
                    that.state = true;
                    if (that.buttonIcon) {
                        that.buttonIcon.addClass(enabledIcon);
                        that.buttonIcon.removeClass(disabledIcon);
                    }
                    if (that.buttonLabel) {
                        that.buttonLabel.text(enabledLabel);
                    }
                } else {
                    that.button.removeClass("selected");
                    that.state = false;
                    if (that.buttonIcon) {
                        that.buttonIcon.addClass(disabledIcon);
                        that.buttonIcon.removeClass(enabledIcon);
                    }
                    if (that.buttonLabel) {
                        that.buttonLabel.text(disabledLabel);
                    }
                }
            })
            this.element.trigger("change");
        }
    });
})(jQuery);
;(function($) {

/**
 * Attach to an <input type="text"> to provide auto-complete
 *
 * $("#node-red-text").autoComplete({
 *     search: function(value) { return ['a','b','c'] }
 * })
 *
 * options:
 *
 *  search:    function(value, [done])
 *             A function that is passed the current contents of the input whenever
 *             it changes.
 *             The function must either return auto-complete options, or pass them
 *             to the optional 'done' parameter.
 *             If the function signature includes 'done', it must be used
 *  minLength: number
 *             If `minLength` is 0, pressing down arrow will show the list
 *
 * The auto-complete options should be an array of objects in the form:
 *  {
 *      value: String : the value to insert if selected
 *      label: String|DOM Element : the label to display in the dropdown.
 *  }
 *
 */

    $.widget( "nodered.autoComplete", {
        _create: function() {
            const that = this;
            this.completionMenuShown = false;
            this.options.minLength = parseInteger(this.options.minLength, 1, 0);
            this.options.search = this.options.search || function() { return [] };
            this.element.addClass("red-ui-autoComplete");
            this.element.on("keydown.red-ui-autoComplete", function(evt) {
                if ((evt.keyCode === 13 || evt.keyCode === 9) && that.completionMenuShown) {
                    var opts = that.menu.options();
                    that.element.val(opts[0].value);
                    that.menu.hide();
                    evt.preventDefault();
                }
            })
            this.element.on("keyup.red-ui-autoComplete", function(evt) {
                if (evt.keyCode === 13 || evt.keyCode === 9 || evt.keyCode === 27) {
                    // ENTER / TAB / ESCAPE
                    return
                }
                if (evt.keyCode === 8 || evt.keyCode === 46) {
                    // Delete/Backspace
                    if (!that.completionMenuShown) {
                        return;
                    }
                }
                that._updateCompletions(this.value);
            });
        },
        _showCompletionMenu: function(completions) {
            if (this.completionMenuShown) {
                return;
            }
            this.menu = RED.popover.menu({
                tabSelect: true,
                width: Math.max(300, this.element.width()),
                maxHeight: 200,
                class: "red-ui-autoComplete-container",
                options: completions,
                onselect: (opt) => { this.element.val(opt.value); this.element.focus(); this.element.trigger("change") },
                onclose: () => { this.completionMenuShown = false; delete this.menu; this.element.focus()}
            });
            this.menu.show({
                target: this.element
            })
            this.completionMenuShown = true;
        },
        _updateCompletions: function(val) {
            const that = this;
            if (val.trim().length < this.options.minLength) {
                if (this.completionMenuShown) {
                    this.menu.hide();
                }
                return;
            }
            function displayResults(completions,requestId) {
                if (requestId && requestId !== that.pendingRequest) {
                    // This request has been superseded
                    return
                }
                if (!completions || completions.length === 0) {
                    if (that.completionMenuShown) {
                        that.menu.hide();
                    }
                    return
                }
                if (that.completionMenuShown) {
                    that.menu.options(completions);
                } else {
                    that._showCompletionMenu(completions);
                }
            }
            if (this.options.search.length === 2) {
                const requestId = 1+Math.floor(Math.random()*10000);
                this.pendingRequest = requestId;
                this.options.search(val,function(completions) { displayResults(completions,requestId);})
            } else {
                displayResults(this.options.search(val))
            }
        },
        _destroy: function() {
            this.element.removeClass("red-ui-autoComplete")
            this.element.off("keydown.red-ui-autoComplete")
            this.element.off("keyup.red-ui-autoComplete")
            if (this.completionMenuShown) {
                this.menu.hide();
            }
        }
    });
    function parseInteger(input, def, min, max) {
        if(input == null) { return (def || 0); }
        min = min == null ? Number.NEGATIVE_INFINITY : min; 
        max = max == null ? Number.POSITIVE_INFINITY : max; 
        let n = parseInt(input);
        if(isNaN(n) || n < min || n > max) { n = def || 0; }
        return n;
    }
})(jQuery);
;RED.actions = (function() {
    var actions = {

    };

    function addAction(name,handler,options) {
        if (typeof handler !== 'function') {
            throw new Error("Action handler not a function");
        }
        if (actions[name]) {
            throw new Error("Cannot override existing action");
        }
        actions[name] = {
            handler: handler,
            options: options,
        };
    }
    function removeAction(name) {
        delete actions[name];
    }
    function getAction(name) {
        return actions[name].handler;
    }
    function getActionLabel(name) {
        let def = actions[name]
        if (!def) {
            return ''
        }
        if (!def.label) {
            var options = def.options;
            var key = options ? options.label : undefined;
            if (!key) {
                key = "action-list." +name.replace(/^.*:/,"");
            }
            var label = RED._(key);
            if (label === key) {
                // no translation. convert `name` to description
                label = name.replace(/(^.+:([a-z]))|(-([a-z]))/g, function() {
                    if (arguments[5] === 0) {
                        return arguments[2].toUpperCase();
                    } else {
                        return " "+arguments[4].toUpperCase();
                    }
                });
            }
            def.label = label;
        }
        return def.label
    }


    function invokeAction() {
        var args = Array.prototype.slice.call(arguments);
        var name = args.shift();
        if (actions.hasOwnProperty(name)) {
            var handler = actions[name].handler;
            handler.apply(null, args);
        }
    }
    function listActions() {
        var result = [];

        Object.keys(actions).forEach(function(action) {
            var def = actions[action];
            var shortcut = RED.keyboard.getShortcut(action);
            var isUser = false;
            if (shortcut) {
                isUser = shortcut.user;
            } else {
                isUser = !!RED.keyboard.getUserShortcut(action);
            }
            if (!def.label) {
                def.label = getActionLabel(action)
            }
            result.push({
                id:action,
                scope:shortcut?shortcut.scope:undefined,
                key:shortcut?shortcut.key:undefined,
                user:isUser,
                label: def.label,
                options: def.options,
            });
        });
        return result;
    }
    return {
        add: addAction,
        remove: removeAction,
        get: getAction,
        getLabel: getActionLabel,
        invoke: invokeAction,
        list: listActions
    }
})();
;/**
 * 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.
 **/

RED.deploy = (function() {

    var deploymentTypes = {
        "full":{img:"red/images/deploy-full-o.svg"},
        "nodes":{img:"red/images/deploy-nodes-o.svg"},
        "flows":{img:"red/images/deploy-flows-o.svg"}
    }

    var ignoreDeployWarnings = {
        unknown: false,
        unusedConfig: false,
        invalid: false
    }

    var deploymentType = "full";

    var deployInflight = false;

    var currentDiff = null;

    var activeBackgroundDeployNotification;

    function changeDeploymentType(type) {
        deploymentType = type;
        $("#red-ui-header-button-deploy-icon").attr("src",deploymentTypes[type].img);
    }

    /**
     * options:
     *   type: "default" - Button with drop-down options - no further customisation available
     *      label: the text to display - default: "Deploy"
     *   type: "simple"  - Button without dropdown. Customisations:
     *      label: the text to display - default: "Deploy"
     *      icon : the icon to use. Null removes the icon. default: "red/images/deploy-full-o.svg"
     */
    function init(options) {
        options = options || {};
        var type = options.type || "default";
        var label = options.label || RED._("deploy.deploy");

        if (type == "default") {
            $('<li><span class="red-ui-deploy-button-group button-group">'+
              '<a id="red-ui-header-button-deploy" class="red-ui-deploy-button disabled" href="#">'+
                '<span class="red-ui-deploy-button-content">'+
                 '<img id="red-ui-header-button-deploy-icon" src="red/images/deploy-full-o.svg"> '+
                 '<span>'+label+'</span>'+
                '</span>'+
                '<span class="red-ui-deploy-button-spinner hide">'+
                 '<img src="red/images/spin.svg"/>'+
                '</span>'+
              '</a>'+
              '<a id="red-ui-header-button-deploy-options" class="red-ui-deploy-button" href="#"><i class="fa fa-caret-down"></i><i class="fa fa-lock"></i></a>'+
              '</span></li>').prependTo(".red-ui-header-toolbar");
            const mainMenuItems = [
                    {id:"deploymenu-item-full",toggle:"deploy-type",icon:"red/images/deploy-full.svg",label:RED._("deploy.full"),sublabel:RED._("deploy.fullDesc"),selected: true, onselect:function(s) { if(s){changeDeploymentType("full")}}},
                    {id:"deploymenu-item-flow",toggle:"deploy-type",icon:"red/images/deploy-flows.svg",label:RED._("deploy.modifiedFlows"),sublabel:RED._("deploy.modifiedFlowsDesc"), onselect:function(s) {if(s){changeDeploymentType("flows")}}},
                    {id:"deploymenu-item-node",toggle:"deploy-type",icon:"red/images/deploy-nodes.svg",label:RED._("deploy.modifiedNodes"),sublabel:RED._("deploy.modifiedNodesDesc"),onselect:function(s) { if(s){changeDeploymentType("nodes")}}},
                    null
            ]
            if (RED.settings.runtimeState && RED.settings.runtimeState.ui === true) {
                mainMenuItems.push({id:"deploymenu-item-runtime-start", icon:"red/images/start.svg",label:RED._("deploy.startFlows"),sublabel:RED._("deploy.startFlowsDesc"),onselect:"core:start-flows", visible:false})
                mainMenuItems.push({id:"deploymenu-item-runtime-stop", icon:"red/images/stop.svg",label:RED._("deploy.stopFlows"),sublabel:RED._("deploy.stopFlowsDesc"),onselect:"core:stop-flows", visible:false})
            }
            mainMenuItems.push({id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"})
            RED.menu.init({id:"red-ui-header-button-deploy-options", options: mainMenuItems });
        } else if (type == "simple") {
            var icon = 'red/images/deploy-full-o.svg';
            if (options.hasOwnProperty('icon')) {
                icon = options.icon;
            }

            $('<li><span class="red-ui-deploy-button-group button-group">'+
              '<a id="red-ui-header-button-deploy" class="red-ui-deploy-button disabled" href="#">'+
                '<span class="red-ui-deploy-button-content">'+
                  (icon?'<img id="red-ui-header-button-deploy-icon" src="'+icon+'"> ':'')+
                  '<span>'+label+'</span>'+
                '</span>'+
                '<span class="red-ui-deploy-button-spinner hide">'+
                 '<img src="red/images/spin.svg"/>'+
                '</span>'+
              '</a>'+
              '</span></li>').prependTo(".red-ui-header-toolbar");
        }

        $('#red-ui-header-button-deploy').on("click", function(event) {
            event.preventDefault();
            save();
        });

        RED.actions.add("core:deploy-flows",save);
        if (type === "default") {
            if (RED.settings.runtimeState && RED.settings.runtimeState.ui === true) {
                RED.actions.add("core:stop-flows",function() { stopStartFlows("stop") });
                RED.actions.add("core:start-flows",function() { stopStartFlows("start") });
            }
            RED.actions.add("core:restart-flows",restart);
            RED.actions.add("core:set-deploy-type-to-full",function() { RED.menu.setSelected("deploymenu-item-full",true);});
            RED.actions.add("core:set-deploy-type-to-modified-flows",function() { RED.menu.setSelected("deploymenu-item-flow",true); });
            RED.actions.add("core:set-deploy-type-to-modified-nodes",function() { RED.menu.setSelected("deploymenu-item-node",true); });
        }

        window.addEventListener('beforeunload', function (event) {
            if (RED.nodes.dirty()) {
                event.preventDefault();
                event.stopImmediatePropagation()
                event.returnValue = RED._("deploy.confirm.undeployedChanges");
                return
            }
        })

        RED.events.on('workspace:dirty',function(state) {
            if (RED.settings.user?.permissions === 'read') {
                return
            }
            if (state.dirty) {
                // window.onbeforeunload = function() {
                //     return 
                // }
                $("#red-ui-header-button-deploy").removeClass("disabled");
            } else {
                // window.onbeforeunload = null;
                $("#red-ui-header-button-deploy").addClass("disabled");
            }
        });

        RED.comms.subscribe("notification/runtime-deploy",function(topic,msg) {
            var currentRev = RED.nodes.version();
            if (currentRev === null || deployInflight || currentRev === msg.revision) {
                return;
            }
            if (activeBackgroundDeployNotification?.hidden && !activeBackgroundDeployNotification?.closed) {
                activeBackgroundDeployNotification.showNotification()
                return
            }
            const message = $('<p>').text(RED._('deploy.confirm.backgroundUpdate'));
            const options = {
                id: 'background-update',
                type: 'compact',
                modal: false,
                fixed: true,
                timeout: 10000,
                buttons: [
                    {
                        text: RED._('deploy.confirm.button.review'),
                        class: "primary",
                        click: function() {
                            activeBackgroundDeployNotification.hideNotification();
                            var nns = RED.nodes.createCompleteNodeSet();
                            resolveConflict(nns,false);
                        }
                    }
                ]
            }
            if (!activeBackgroundDeployNotification || activeBackgroundDeployNotification.closed) {
                activeBackgroundDeployNotification = RED.notify(message, options)
            } else {
                activeBackgroundDeployNotification.update(message, options)
            }
        });


        updateLockedState()
        RED.events.on('login', updateLockedState)
    }

    function updateLockedState() {
        if (RED.settings.user?.permissions === 'read') {
            $(".red-ui-deploy-button-group").addClass("readOnly");
            $("#red-ui-header-button-deploy").addClass("disabled");
        } else {
            $(".red-ui-deploy-button-group").removeClass("readOnly");
            if (RED.nodes.dirty()) {
                $("#red-ui-header-button-deploy").removeClass("disabled");
            }
        }
    }

    function getNodeInfo(node) {
        var tabLabel = "";
        if (node.z) {
            var tab = RED.nodes.workspace(node.z);
            if (!tab) {
                tab = RED.nodes.subflow(node.z);
                tabLabel = tab.name;
            } else {
                tabLabel = tab.label;
            }
        }
        var label = RED.utils.getNodeLabel(node,node.id);
        return {tab:tabLabel,type:node.type,label:label};
    }
    function sortNodeInfo(A,B) {
        if (A.tab < B.tab) { return -1;}
        if (A.tab > B.tab) { return 1;}
        if (A.type < B.type) { return -1;}
        if (A.type > B.type) { return 1;}
        if (A.name < B.name) { return -1;}
        if (A.name > B.name) { return 1;}
        return 0;
    }

    function resolveConflict(currentNodes, activeDeploy) {
        var message = $('<div>');
        $('<p data-i18n="deploy.confirm.conflict"></p>').appendTo(message);
        var conflictCheck = $('<div class="red-ui-deploy-dialog-confirm-conflict-row">'+
            '<img src="red/images/spin.svg"/><div data-i18n="deploy.confirm.conflictChecking"></div>'+
        '</div>').appendTo(message);
        var conflictAutoMerge = $('<div class="red-ui-deploy-dialog-confirm-conflict-row">'+
            '<i class="fa fa-check"></i><div data-i18n="deploy.confirm.conflictAutoMerge"></div>'+
            '</div>').hide().appendTo(message);
        var conflictManualMerge = $('<div class="red-ui-deploy-dialog-confirm-conflict-row">'+
            '<i class="fa fa-exclamation"></i><div data-i18n="deploy.confirm.conflictManualMerge"></div>'+
            '</div>').hide().appendTo(message);

        message.i18n();
        currentDiff = null;
        var buttons = [
            {
                text: RED._("common.label.cancel"),
                click: function() {
                    conflictNotification.close();
                }
            },
            {
                id: "red-ui-deploy-dialog-confirm-deploy-review",
                text: RED._("deploy.confirm.button.review"),
                class: "primary disabled",
                click: function() {
                    if (!$("#red-ui-deploy-dialog-confirm-deploy-review").hasClass('disabled')) {
                        RED.diff.showRemoteDiff(null, {
                            onmerge: function () {
                                activeBackgroundDeployNotification.close()
                            }
                        });
                        conflictNotification.close();
                    }
                }
            },
            {
                id: "red-ui-deploy-dialog-confirm-deploy-merge",
                text: RED._("deploy.confirm.button.merge"),
                class: "primary disabled",
                click: function() {
                    if (!$("#red-ui-deploy-dialog-confirm-deploy-merge").hasClass('disabled')) {
                        RED.diff.mergeDiff(currentDiff);
                        conflictNotification.close();
                        activeBackgroundDeployNotification.close()
                    }
                }
            }
        ];
        if (activeDeploy) {
            buttons.push({
                id: "red-ui-deploy-dialog-confirm-deploy-overwrite",
                text: RED._("deploy.confirm.button.overwrite"),
                class: "primary",
                click: function() {
                    save(true,activeDeploy);
                    conflictNotification.close();
                    activeBackgroundDeployNotification.close()
                }
            })
        }
        var conflictNotification = RED.notify(message,{
            modal: true,
            fixed: true,
            width: 600,
            buttons: buttons
        });

        RED.diff.getRemoteDiff(function(diff) {
            currentDiff = diff;
            conflictCheck.hide();
            var d = Object.keys(diff.conflicts);
            if (d.length === 0) {
                conflictAutoMerge.show();
                $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled')
            } else {
                conflictManualMerge.show();
            }
            $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled')
        })
    }
    function cropList(list) {
        if (list.length > 5) {
            var remainder = list.length - 5;
            list = list.slice(0,5);
            list.push(RED._("deploy.confirm.plusNMore",{count:remainder}));
        }
        return list;
    }
    function sanitize(html) {
        return html.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")
    }

    function shadeShow() {
        $("#red-ui-header-shade").show();
        $("#red-ui-editor-shade").show();
        $("#red-ui-palette-shade").show();
        $("#red-ui-sidebar-shade").show();
    }
    function shadeHide() {
        $("#red-ui-header-shade").hide();
        $("#red-ui-editor-shade").hide();
        $("#red-ui-palette-shade").hide();
        $("#red-ui-sidebar-shade").hide();
    }
    function deployButtonSetBusy(){
        $(".red-ui-deploy-button-content").css('opacity',0);
        $(".red-ui-deploy-button-spinner").show();
        $("#red-ui-header-button-deploy").addClass("disabled");
    }
    function deployButtonClearBusy(){
        $(".red-ui-deploy-button-content").css('opacity',1);
        $(".red-ui-deploy-button-spinner").hide();
    }
    function stopStartFlows(state) {
        const startTime = Date.now()
        const deployWasEnabled = !$("#red-ui-header-button-deploy").hasClass("disabled")
        deployInflight = true
        deployButtonSetBusy()
        shadeShow()
        $.ajax({
            url:"flows/state",
            type: "POST",
            data: {state: state}
        }).done(function(data,textStatus,xhr) {
            if (deployWasEnabled) {
                $("#red-ui-header-button-deploy").removeClass("disabled")
            }
        }).fail(function(xhr,textStatus,err) {
            if (deployWasEnabled) {
                $("#red-ui-header-button-deploy").removeClass("disabled")
            }
            if (xhr.status === 401) {
                RED.notify(RED._("notification.error", { message: RED._("user.notAuthorized") }), "error")
            } else if (xhr.responseText) {
                const errorDetail = { message: err ? (err + "") : "" }
                try {
                    errorDetail.message = JSON.parse(xhr.responseText).message
                } finally {
                    errorDetail.message = errorDetail.message || xhr.responseText
                }
                RED.notify(RED._("notification.error", errorDetail), "error")
            } else {
                RED.notify(RED._("notification.error", { message: RED._("deploy.errors.noResponse") }), "error")
            }
        }).always(function() {
            const delta = Math.max(0, 300 - (Date.now() - startTime))
            setTimeout(function () {
                deployButtonClearBusy()
                shadeHide()
                deployInflight = false
            }, delta);
        });
    }
    function restart() {
        var startTime = Date.now();
        var deployWasEnabled = !$("#red-ui-header-button-deploy").hasClass("disabled");
        deployInflight = true;
        deployButtonSetBusy();
        $.ajax({
            url:"flows",
            type: "POST",
            headers: {
                "Node-RED-Deployment-Type":"reload"
            }
        }).done(function(data,textStatus,xhr) {
            if (deployWasEnabled) {
                $("#red-ui-header-button-deploy").removeClass("disabled");
            }
            RED.notify('<p>'+RED._("deploy.successfulRestart")+'</p>',"success");
        }).fail(function(xhr,textStatus,err) {
            if (deployWasEnabled) {
                $("#red-ui-header-button-deploy").removeClass("disabled");
            }
            if (xhr.status === 401) {
                RED.notify(RED._("deploy.deployFailed",{message:RED._("user.notAuthorized")}),"error");
            } else if (xhr.status === 409) {
                resolveConflict(nns, true);
            } else if (xhr.responseText) {
                RED.notify(RED._("deploy.deployFailed",{message:xhr.responseText}),"error");
            } else {
                RED.notify(RED._("deploy.deployFailed",{message:RED._("deploy.errors.noResponse")}),"error");
            }
        }).always(function() {
            var delta = Math.max(0,300-(Date.now()-startTime));
            setTimeout(function() {
                deployButtonClearBusy();
                deployInflight = false;
            },delta);
        });
    }
    function save(skipValidation, force) {
        if ($("#red-ui-header-button-deploy").hasClass("disabled")) {
            return; //deploy is disabled
        }
        if ($("#red-ui-header-shade").is(":visible")) {
            return; //deploy is shaded
        }
        if (!RED.user.hasPermission("flows.write")) {
            RED.notify(RED._("user.errors.deploy"), "error");
            return;
        }
        let hasUnusedConfig = false;
        if (!skipValidation) {
            let hasUnknown = false;
            let hasInvalid = false;
            const unknownNodes = [];
            const invalidNodes = [];

            RED.nodes.eachConfig(function (node) {
                if (node.valid === undefined) {
                    RED.editor.validateNode(node);
                }
                if (!node.valid && !node.d) {
                    invalidNodes.push(getNodeInfo(node));
                }
                if (node.type === "unknown") {
                    if (unknownNodes.indexOf(node.name) == -1) {
                        unknownNodes.push(node.name);
                    }
                }
            });
            RED.nodes.eachNode(function (node) {
                if (!node.valid && !node.d) {
                    invalidNodes.push(getNodeInfo(node));
                }
                if (node.type === "unknown") {
                    if (unknownNodes.indexOf(node.name) == -1) {
                        unknownNodes.push(node.name);
                    }
                }
            });
            hasUnknown = unknownNodes.length > 0;
            hasInvalid = invalidNodes.length > 0;

            const unusedConfigNodes = [];
            RED.nodes.eachConfig(function (node) {
                if ((node._def.hasUsers !== false) && (node.users.length === 0)) {
                    unusedConfigNodes.push(getNodeInfo(node));
                    hasUnusedConfig = true;
                }
            });

            let showWarning = false;
            let notificationMessage;
            let notificationButtons = [];
            let notification;
            if (hasUnknown && !ignoreDeployWarnings.unknown) {
                showWarning = true;
                notificationMessage = "<p>" + RED._('deploy.confirm.unknown') + "</p>" +
                    '<ul class="red-ui-deploy-dialog-confirm-list"><li>' + cropList(unknownNodes).map(function (n) { return sanitize(n) }).join("</li><li>") + "</li></ul><p>" +
                    RED._('deploy.confirm.confirm') +
                    "</p>";

                notificationButtons = [
                    {
                        text: RED._("deploy.unknownNodesButton"),
                        class: "pull-left",
                        click: function() {
                            notification.close();
                            RED.actions.invoke("core:search","type:unknown ");
                        }
                    },
                    {
                        id: "red-ui-deploy-dialog-confirm-deploy-deploy",
                        text: RED._("deploy.confirm.button.confirm"),
                        class: "primary",
                        click: function () {
                            save(true);
                            notification.close();
                        }
                    }
                ];
            } else if (hasInvalid && !ignoreDeployWarnings.invalid) {
                showWarning = true;
                invalidNodes.sort(sortNodeInfo);

                notificationMessage = "<p>" + RED._('deploy.confirm.improperlyConfigured') + "</p>" +
                    '<ul class="red-ui-deploy-dialog-confirm-list"><li>' + cropList(invalidNodes.map(function (A) { return sanitize((A.tab ? "[" + A.tab + "] " : "") + A.label + " (" + A.type + ")") })).join("</li><li>") + "</li></ul><p>" +
                    RED._('deploy.confirm.confirm') +
                    "</p>";
                notificationButtons = [
                    {
                        text: RED._("deploy.invalidNodesButton"),
                        class: "pull-left",
                        click: function() {
                            notification.close();
                            RED.actions.invoke("core:search","is:invalid ");
                        }
                    },
                    {
                        id: "red-ui-deploy-dialog-confirm-deploy-deploy",
                        text: RED._("deploy.confirm.button.confirm"),
                        class: "primary",
                        click: function () {
                            save(true);
                            notification.close();
                        }
                    }
                ];
            }
            if (showWarning) {
                notificationButtons.unshift(
                    {
                        text: RED._("common.label.cancel"),
                        click: function () {
                            notification.close();
                        }
                    }
                );
                notification = RED.notify(notificationMessage, {
                    modal: true,
                    fixed: true,
                    buttons: notificationButtons
                });
                return;
            }
        }

        const nns = RED.nodes.createCompleteNodeSet();
        const startTime = Date.now();

        deployButtonSetBusy();
        const data = { flows: nns };
        if (!force) {
            data.rev = RED.nodes.version();
        }

        deployInflight = true;
        shadeShow();
        $.ajax({
            url: "flows",
            type: "POST",
            data: JSON.stringify(data),
            contentType: "application/json; charset=utf-8",
            headers: {
                "Node-RED-Deployment-Type": deploymentType
            }
        }).done(function (data, textStatus, xhr) {
            RED.nodes.dirty(false);
            RED.nodes.version(data.rev);
            RED.nodes.originalFlow(nns);
            if (hasUnusedConfig) {
                let notification;
                const opts = {
                    type: "success",
                    fixed: false,
                    timeout: 6000,
                    buttons: [
                        {
                            text: RED._("deploy.unusedConfigNodesButton"),
                            class: "pull-left",
                            click: function() {
                                notification.close();
                                RED.actions.invoke("core:search","is:config is:unused ");
                            }
                        },
                        {
                            text: RED._("common.label.close"),
                            class: "primary",
                            click: function () {
                                save(true);
                                notification.close();
                            }
                        }
                    ]
                }
                notification = RED.notify(
                    '<p>' + RED._("deploy.successfulDeploy") + '</p>' +
                    '<p>' + RED._("deploy.unusedConfigNodes") + '</p>', opts);
            } else {
                RED.notify('<p>' + RED._("deploy.successfulDeploy") + '</p>', "success");
            }
            const flowsToLock = new Set()
            // Node's properties cannot be modified if its workspace is locked.
            function ensureUnlocked(id) {
                // TODO: `RED.nodes.subflow` is useless
                const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null);
                const isLocked = flow ? flow.locked : false;
                if (flow && isLocked) {
                    flow.locked = false;
                    flowsToLock.add(flow)
                }
            }
            RED.nodes.eachNode(function (node) {
                ensureUnlocked(node.z)
                if (node.changed) {
                    node.dirty = true;
                    node.changed = false;
                }
                if (node.moved) {
                    node.dirty = true;
                    node.moved = false;
                }
                if (node.credentials) {
                    delete node.credentials;
                }
            });
            RED.nodes.eachGroup(function (node) {
                ensureUnlocked(node.z)
                if (node.changed) {
                    node.dirty = true;
                    node.changed = false;
                }
                if (node.moved) {
                    node.dirty = true;
                    node.moved = false;
                }
            })
            RED.nodes.eachJunction(function (node) {
                ensureUnlocked(node.z)
                if (node.changed) {
                    node.dirty = true;
                    node.changed = false;
                }
                if (node.moved) {
                    node.dirty = true;
                    node.moved = false;
                }
            })
            RED.nodes.eachConfig(function (confNode) {
                if (confNode.z) {
                    ensureUnlocked(confNode.z)
                }
                confNode.changed = false;
                if (confNode.credentials) {
                    delete confNode.credentials;
                }
            });
            // Subflow cannot be locked
            RED.nodes.eachSubflow(function (subflow) {
                if (subflow.changed) {
                    subflow.changed = false;
                    RED.events.emit("subflows:change", subflow);
                }
            });
            RED.nodes.eachWorkspace(function (ws) {
                if (ws.changed || ws.added) {
                    // Ensure the Workspace is unlocked to modify its properties.
                    ensureUnlocked(ws.id);
                    ws.changed = false;
                    delete ws.added
                    if (flowsToLock.has(ws)) {
                        ws.locked = true;
                        flowsToLock.delete(ws);
                    }
                    RED.events.emit("flows:change", ws)
                }
            });
            // Ensures all workspaces to be locked have been locked.
            flowsToLock.forEach(flow => {
                flow.locked = true
            })
            // Once deployed, cannot undo back to a clean state
            RED.history.markAllDirty();
            RED.view.redraw();
            RED.sidebar.config.refresh();
            RED.events.emit("deploy");
        }).fail(function (xhr, textStatus, err) {
            RED.nodes.dirty(true);
            $("#red-ui-header-button-deploy").removeClass("disabled");
            if (xhr.status === 401) {
                RED.notify(RED._("deploy.deployFailed", { message: RED._("user.notAuthorized") }), "error");
            } else if (xhr.status === 409) {
                resolveConflict(nns, true);
            } else if (xhr.responseText) {
                RED.notify(RED._("deploy.deployFailed", { message: xhr.responseText }), "error");
            } else {
                RED.notify(RED._("deploy.deployFailed", { message: RED._("deploy.errors.noResponse") }), "error");
            }
        }).always(function () {
            const delta = Math.max(0, 300 - (Date.now() - startTime));
            setTimeout(function () {
                deployInflight = false;
                deployButtonClearBusy()
                shadeHide()
            }, delta);
        });
    }
    return {
        init: init,
        setDeployInflight: function(state) {
            deployInflight = state;
        }

    }
})();
;
RED.diagnostics = (function () {

    function init() {
        if (RED.settings.get('diagnostics.ui', true) === false) {
            return;
        }
        RED.actions.add("core:show-system-info", function () { show(); });
    }

    function show() {
        $.ajax({
            headers: {
                "Accept": "application/json"
            },
            cache: false,
            url: 'diagnostics',
            success: function (data) {
                var json = JSON.stringify(data || {}, "", 4);
                if (json === "{}") {
                    json = "{\n\n}";
                }
                RED.editor.editJSON({
                    title: RED._('diagnostics.title'),
                    value: json,
                    requireValid: true,
                    readOnly: true,
                    toolbarButtons: [
                        {
                            text: RED._('clipboard.export.copy'),
                            icon: 'fa fa-copy',
                            click: function () {
                                RED.clipboard.copyText(json, $(this), RED._('clipboard.copyMessageValue'))
                            }
                        },
                        {
                            text: RED._('clipboard.download'),
                            icon: 'fa fa-download',
                            click: function () {
                                var element = document.createElement('a');
                                element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(json));
                                element.setAttribute('download', "system-info.json");
                                element.style.display = 'none';
                                document.body.appendChild(element);
                                element.click();
                                document.body.removeChild(element);
                            }
                        },
                    ]
                });
            },
            error: function (jqXHR, textStatus, errorThrown) {
                console.log("Unexpected error loading system info:", jqXHR.status, textStatus, errorThrown);
            }
        });
    }

    return {
        init: init,
    };
})();
;RED.diff = (function() {
    var currentDiff = {};
    var diffVisible = false;
    var diffList;

    function init() {

        // RED.actions.add("core:show-current-diff",showLocalDiff);
        RED.actions.add("core:show-remote-diff",showRemoteDiff);
        // RED.keyboard.add("*","ctrl-shift-l","core:show-current-diff");
        // RED.keyboard.add("*","ctrl-shift-r","core:show-remote-diff");


        // RED.actions.add("core:show-test-flow-diff-1",function(){showTestFlowDiff(1)});
        // RED.keyboard.add("*","ctrl-shift-f 1","core:show-test-flow-diff-1");
        //
        // RED.actions.add("core:show-test-flow-diff-2",function(){showTestFlowDiff(2)});
        // RED.keyboard.add("*","ctrl-shift-f 2","core:show-test-flow-diff-2");
        // RED.actions.add("core:show-test-flow-diff-3",function(){showTestFlowDiff(3)});
        // RED.keyboard.add("*","ctrl-shift-f 3","core:show-test-flow-diff-3");

    }
    function createDiffTable(container,CurrentDiff) {
        var diffList = $('<ol class="red-ui-diff-list"></ol>').appendTo(container);
        diffList.editableList({
            addButton: false,
            height: "auto",
            scrollOnAdd: false,
            addItem: function(container,i,object) {
                var localDiff = object.diff;
                var remoteDiff = object.remoteDiff;
                var tab = object.tab.n;
                var def = object.def;
                var conflicts = CurrentDiff.conflicts;

                var tabDiv = $('<div>',{class:"red-ui-diff-list-flow"}).appendTo(container);
                tabDiv.addClass('collapsed');
                var titleRow = $('<div>',{class:"red-ui-diff-list-flow-title"}).appendTo(tabDiv);
                var nodesDiv = $('<div>').appendTo(tabDiv);
                var originalCell = $('<div>',{class:"red-ui-diff-list-node-cell"}).appendTo(titleRow);
                var localCell = $('<div>',{class:"red-ui-diff-list-node-cell red-ui-diff-list-node-local"}).appendTo(titleRow);
                var remoteCell;
                var selectState;

                if (remoteDiff) {
                    remoteCell = $('<div>',{class:"red-ui-diff-list-node-cell red-ui-diff-list-node-remote"}).appendTo(titleRow);
                }
                $('<span class="red-ui-diff-list-chevron"><i class="fa fa-angle-down"></i></span>').appendTo(originalCell);
                createNodeIcon(tab,def).appendTo(originalCell);
                var tabForLabel = (object.newTab || object.tab).n;
                var titleSpan = $('<span>',{class:"red-ui-diff-list-flow-title-meta"}).appendTo(originalCell);
                if (tabForLabel.type === 'tab') {
                    titleSpan.text(tabForLabel.label||tabForLabel.id);
                } else if (tab.type === 'subflow') {
                    titleSpan.text((tabForLabel.name||tabForLabel.id));
                } else {
                    titleSpan.text(RED._("diff.globalNodes"));
                }
                var flowStats = {
                    local: {
                        addedCount:0,
                        deletedCount:0,
                        changedCount:0,
                        movedCount:0,
                        unchangedCount: 0
                    },
                    remote: {
                        addedCount:0,
                        deletedCount:0,
                        changedCount:0,
                        movedCount:0,
                        unchangedCount: 0
                    },
                    conflicts: 0
                }
                if (object.newTab || object.remoteTab) {
                    var localTabNode = {
                        node: localDiff.newConfig.all[tab.id],
                        all: localDiff.newConfig.all,
                        diff: localDiff
                    }
                    var remoteTabNode;
                    if (remoteDiff) {
                        remoteTabNode = {
                            node:remoteDiff.newConfig.all[tab.id]||null,
                            all: remoteDiff.newConfig.all,
                            diff: remoteDiff
                        }
                    }
                    if (tab.type !== undefined) {
                        var div = $("<div>",{class:"red-ui-diff-list-node red-ui-diff-list-node-props collapsed"}).appendTo(nodesDiv);
                        var row = $("<div>",{class:"red-ui-diff-list-node-header"}).appendTo(div);
                        var originalNodeDiv = $("<div>",{class:"red-ui-diff-list-node-cell"}).appendTo(row);
                        var localNodeDiv = $("<div>",{class:"red-ui-diff-list-node-cell red-ui-diff-list-node-local"}).appendTo(row);
                        var localChanged = false;
                        var remoteChanged = false;

                        if (!localDiff.newConfig.all[tab.id]) {
                            localNodeDiv.addClass("red-ui-diff-empty");
                        } else if (localDiff.added[tab.id]) {
                            localNodeDiv.addClass("red-ui-diff-status-added");
                            localChanged = true;
                            $('<span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(localNodeDiv);
                        } else if (localDiff.changed[tab.id]) {
                            localNodeDiv.addClass("red-ui-diff-status-changed");
                            localChanged = true;
                            $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv);
                        } else {
                            localNodeDiv.addClass("red-ui-diff-status-unchanged");
                            $('<span class="red-ui-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(localNodeDiv);
                        }

                        var remoteNodeDiv;
                        if (remoteDiff) {
                            remoteNodeDiv = $("<div>",{class:"red-ui-diff-list-node-cell red-ui-diff-list-node-remote"}).appendTo(row);
                            if (!remoteDiff.newConfig.all[tab.id]) {
                                remoteNodeDiv.addClass("red-ui-diff-empty");
                                if (remoteDiff.deleted[tab.id]) {
                                    remoteChanged = true;
                                }
                            } else if (remoteDiff.added[tab.id]) {
                                remoteNodeDiv.addClass("red-ui-diff-status-added");
                                remoteChanged = true;
                                $('<span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(remoteNodeDiv);
                            } else if (remoteDiff.changed[tab.id]) {
                                remoteNodeDiv.addClass("red-ui-diff-status-changed");
                                remoteChanged = true;
                                $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv);
                            } else {
                                remoteNodeDiv.addClass("red-ui-diff-status-unchanged");
                                $('<span class="red-ui-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(remoteNodeDiv);
                            }
                        }
                        $('<span class="red-ui-diff-list-chevron"><i class="fa fa-angle-down"></i></span>').appendTo(originalNodeDiv);
                        $('<span>').text(RED._("diff.flowProperties")).appendTo(originalNodeDiv);

                        row.on("click", function(evt) {
                            evt.preventDefault();
                            $(this).parent().toggleClass('collapsed');
                        });

                        createNodePropertiesTable(def,tab,localTabNode,remoteTabNode).appendTo(div);
                        selectState = "";
                        if (conflicts[tab.id]) {
                            flowStats.conflicts++;

                            if (!localNodeDiv.hasClass("red-ui-diff-empty")) {
                                $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(localNodeDiv);
                            }
                            if (!remoteNodeDiv.hasClass("red-ui-diff-empty")) {
                                $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(remoteNodeDiv);
                            }
                            div.addClass("red-ui-diff-list-node-conflict");
                        } else {
                            selectState = CurrentDiff.resolutions[tab.id];
                        }
                        // Tab properties row
                        createNodeConflictRadioBoxes(tab,div,localNodeDiv,remoteNodeDiv,true,!conflicts[tab.id],selectState,CurrentDiff);
                    }
                }
                // var stats = $('<span>',{class:"red-ui-diff-list-flow-stats"}).appendTo(titleRow);
                var localNodeCount = 0;
                var remoteNodeCount = 0;
                var seen = {};
                object.tab.nodes.forEach(function(node) {
                    seen[node.id] = true;
                    createNodeDiffRow(node,flowStats,CurrentDiff).appendTo(nodesDiv)
                });
                if (object.newTab) {
                    localNodeCount = object.newTab.nodes.length;
                    object.newTab.nodes.forEach(function(node) {
                        if (!seen[node.id]) {
                            seen[node.id] = true;
                            createNodeDiffRow(node,flowStats,CurrentDiff).appendTo(nodesDiv)
                        }
                    });
                }
                if (object.remoteTab) {
                    remoteNodeCount = object.remoteTab.nodes.length;
                    object.remoteTab.nodes.forEach(function(node) {
                        if (!seen[node.id]) {
                            createNodeDiffRow(node,flowStats,CurrentDiff).appendTo(nodesDiv)
                        }
                    });
                }
                titleRow.on("click", function(evt) {
                    // if (titleRow.parent().find(".red-ui-diff-list-node:not(.hide)").length > 0) {
                    titleRow.parent().toggleClass('collapsed');
                    if ($(this).parent().hasClass('collapsed')) {
                        $(this).parent().find('.red-ui-diff-list-node').addClass('collapsed');
                        $(this).parent().find('.red-ui-debug-msg-element').addClass('collapsed');
                    }
                    // }
                })

                if (localDiff.deleted[tab.id]) {
                    $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.flowDeleted"></span></span></span>').appendTo(localCell);
                } else if (object.newTab) {
                    if (localDiff.added[tab.id]) {
                        $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.flowAdded"></span></span></span>').appendTo(localCell);
                    } else {
                        if (tab.id) {
                            if (localDiff.changed[tab.id]) {
                                flowStats.local.changedCount++;
                            } else {
                                flowStats.local.unchangedCount++;
                            }
                        }
                        var localStats = $('<span>',{class:"red-ui-diff-list-flow-stats"}).appendTo(localCell);
                        $('<span class="red-ui-diff-status"></span>').text(RED._('diff.nodeCount',{count:localNodeCount})).appendTo(localStats);

                        if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.movedCount + flowStats.local.deletedCount > 0) {
                            $('<span class="red-ui-diff-status"> [ </span>').appendTo(localStats);
                            if (flowStats.conflicts > 0) {
                                $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(localStats);
                            }
                            if (flowStats.local.addedCount > 0) {
                                const cell = $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.local.addedCount+'</span></span>').appendTo(localStats);
                                RED.popover.tooltip(cell, RED._('diff.type.added'))
                            }
                            if (flowStats.local.changedCount > 0) {
                                const cell = $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.local.changedCount+'</span></span>').appendTo(localStats);
                                RED.popover.tooltip(cell, RED._('diff.type.changed'))
                            }
                            if (flowStats.local.movedCount > 0) {
                                const cell = $('<span class="red-ui-diff-status-moved"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.local.movedCount+'</span></span>').appendTo(localStats);
                                RED.popover.tooltip(cell, RED._('diff.type.moved'))
                            }
                            if (flowStats.local.deletedCount > 0) {
                                const cell = $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.local.deletedCount+'</span></span>').appendTo(localStats);
                                RED.popover.tooltip(cell, RED._('diff.type.deleted'))
                            }
                            $('<span class="red-ui-diff-status"> ] </span>').appendTo(localStats);
                        }

                    }
                } else {
                    localCell.addClass("red-ui-diff-empty");
                }

                if (remoteDiff) {
                    if (remoteDiff.deleted[tab.id]) {
                        $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.flowDeleted"></span></span></span>').appendTo(remoteCell);
                    } else if (object.remoteTab) {
                        if (remoteDiff.added[tab.id]) {
                            $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.flowAdded"></span></span></span>').appendTo(remoteCell);
                        } else {
                            if (tab.id) {
                                if (remoteDiff.changed[tab.id]) {
                                    flowStats.remote.changedCount++;
                                } else {
                                    flowStats.remote.unchangedCount++;
                                }
                            }
                            var remoteStats = $('<span>',{class:"red-ui-diff-list-flow-stats"}).appendTo(remoteCell);
                            $('<span class="red-ui-diff-status"></span>').text(RED._('diff.nodeCount',{count:remoteNodeCount})).appendTo(remoteStats);
                            if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.movedCount + flowStats.remote.deletedCount > 0) {
                                $('<span class="red-ui-diff-status"> [ </span>').appendTo(remoteStats);
                                if (flowStats.conflicts > 0) {
                                    $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(remoteStats);
                                }
                                if (flowStats.remote.addedCount > 0) {
                                    const cell = $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.remote.addedCount+'</span></span>').appendTo(remoteStats);
                                    RED.popover.tooltip(cell, RED._('diff.type.added'))
                                }
                                if (flowStats.remote.changedCount > 0) {
                                    const cell = $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.changedCount+'</span></span>').appendTo(remoteStats);
                                    RED.popover.tooltip(cell, RED._('diff.type.changed'))
                                }
                                if (flowStats.remote.movedCount > 0) {
                                    const cell = $('<span class="red-ui-diff-status-moved"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.movedCount+'</span></span>').appendTo(remoteStats);
                                    RED.popover.tooltip(cell, RED._('diff.type.moved'))
                                }
                                if (flowStats.remote.deletedCount > 0) {
                                    const cell = $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.remote.deletedCount+'</span></span>').appendTo(remoteStats);
                                    RED.popover.tooltip(cell, RED._('diff.type.deleted'))
                                }
                                $('<span class="red-ui-diff-status"> ] </span>').appendTo(remoteStats);
                            }
                        }
                    } else {
                        remoteCell.addClass("red-ui-diff-empty");
                    }
                    selectState = "";
                    if (flowStats.conflicts > 0) {
                        titleRow.addClass("red-ui-diff-list-node-conflict");
                    } else {
                        selectState = CurrentDiff.resolutions[tab.id];
                    }
                    if (tab.id) {
                        var hide = !(flowStats.conflicts > 0 &&(localDiff.deleted[tab.id] || remoteDiff.deleted[tab.id]));
                        // Tab parent row
                        createNodeConflictRadioBoxes(tab,titleRow,localCell,remoteCell, false, hide, selectState, CurrentDiff);
                    }
                }

                if (tabDiv.find(".red-ui-diff-list-node").length === 0) {
                    tabDiv.addClass("red-ui-diff-list-flow-empty");
                }
                container.i18n();
            }
        });
        return diffList;
    }
    function buildDiffPanel(container,diff,options) {
        var diffPanel = $('<div class="red-ui-diff-panel"></div>').appendTo(container);
        var diffHeaders = $('<div class="red-ui-diff-panel-headers"></div>').appendTo(diffPanel);
        if (options.mode === "merge") {
            diffPanel.addClass("red-ui-diff-panel-merge");
        }
        var diffList = createDiffTable(diffPanel, diff, options);

        var localDiff = diff.localDiff;
        var remoteDiff = diff.remoteDiff;
        var conflicts = diff.conflicts;

        var currentConfig = localDiff.currentConfig;
        var newConfig = localDiff.newConfig;


        if (remoteDiff !== undefined) {
            diffPanel.addClass('red-ui-diff-three-way');
            var localTitle = options.oldRevTitle || RED._('diff.local');
            var remoteTitle = options.newRevTitle || RED._('diff.remote');
            $('<div></div>').text(localTitle).appendTo(diffHeaders);
            $('<div></div>').text(remoteTitle).appendTo(diffHeaders);
        } else {
            diffPanel.removeClass('red-ui-diff-three-way');
        }

        return {
            list: diffList,
            finish: function() {
                var el = {
                    diff: localDiff,
                    def: {
                        category: 'config',
                        color: '#f0f0f0'
                    },
                    tab: {
                        n: {},
                        nodes: currentConfig.globals
                    },
                    newTab: {
                        n: {},
                        nodes: newConfig.globals
                    }
                };
                if (remoteDiff !== undefined) {
                    el.remoteTab = {
                        n:{},
                        nodes:remoteDiff.newConfig.globals
                    };
                    el.remoteDiff = remoteDiff;
                }
                diffList.editableList('addItem',el);

                var seenTabs = {};

                currentConfig.tabOrder.forEach(function(tabId) {
                    var tab = currentConfig.tabs[tabId];
                    var el = {
                        diff: localDiff,
                        def: RED.nodes.getType('tab'),
                        tab:tab
                    };
                    if (newConfig.tabs.hasOwnProperty(tabId)) {
                        el.newTab = newConfig.tabs[tabId];
                    }
                    if (remoteDiff !== undefined) {
                        el.remoteTab = remoteDiff.newConfig.tabs[tabId];
                        el.remoteDiff = remoteDiff;
                    }
                    seenTabs[tabId] = true;
                    diffList.editableList('addItem',el)
                });
                newConfig.tabOrder.forEach(function(tabId) {
                    if (!seenTabs[tabId]) {
                        seenTabs[tabId] = true;
                        var tab = newConfig.tabs[tabId];
                        var el = {
                            diff: localDiff,
                            def: RED.nodes.getType('tab'),
                            tab:tab,
                            newTab: tab
                        };
                        if (remoteDiff !== undefined) {
                            el.remoteDiff = remoteDiff;
                        }
                        diffList.editableList('addItem',el)
                    }
                });
                if (remoteDiff !== undefined) {
                    remoteDiff.newConfig.tabOrder.forEach(function(tabId) {
                        if (!seenTabs[tabId]) {
                            var tab = remoteDiff.newConfig.tabs[tabId];
                            // TODO how to recognise this is a remotely added flow
                            var el = {
                                diff: localDiff,
                                remoteDiff: remoteDiff,
                                def: RED.nodes.getType('tab'),
                                tab:tab,
                                remoteTab:tab
                            };
                            diffList.editableList('addItem',el)
                        }
                    });
                }
                var subflowId;
                for (subflowId in currentConfig.subflows) {
                    if (currentConfig.subflows.hasOwnProperty(subflowId)) {
                        seenTabs[subflowId] = true;
                        el = {
                            diff: localDiff,
                            def: {
                                defaults:{},
                                icon:"subflow.svg",
                                category: "subflows",
                                color: "#DDAA99"
                            },
                            tab:currentConfig.subflows[subflowId]
                        }
                        if (newConfig.subflows.hasOwnProperty(subflowId)) {
                            el.newTab = newConfig.subflows[subflowId];
                        }
                        if (remoteDiff !== undefined) {
                            el.remoteTab = remoteDiff.newConfig.subflows[subflowId];
                            el.remoteDiff = remoteDiff;
                        }
                        diffList.editableList('addItem',el)
                    }
                }
                for (subflowId in newConfig.subflows) {
                    if (newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) {
                        seenTabs[subflowId] = true;
                        el = {
                            diff: localDiff,
                            def: {
                                defaults:{},
                                icon:"subflow.svg",
                                category: "subflows",
                                color: "#DDAA99"
                            },
                            tab:newConfig.subflows[subflowId],
                            newTab:newConfig.subflows[subflowId]
                        }
                        if (remoteDiff !== undefined) {
                            el.remoteDiff = remoteDiff;
                        }
                        diffList.editableList('addItem',el)
                    }
                }
                if (remoteDiff !== undefined) {
                    for (subflowId in remoteDiff.newConfig.subflows) {
                        if (remoteDiff.newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) {
                            el = {
                                diff: localDiff,
                                remoteDiff: remoteDiff,
                                def: {
                                    defaults:{},
                                    icon:"subflow.svg",
                                    category: "subflows",
                                    color: "#DDAA99"
                                },
                                tab:remoteDiff.newConfig.subflows[subflowId],
                                remoteTab: remoteDiff.newConfig.subflows[subflowId]
                            }
                            diffList.editableList('addItem',el)
                        }
                    }
                }
            }
        };
    }
    function formatWireProperty(wires,allNodes) {
        var result = $("<div>",{class:"red-ui-diff-list-wires"})
        var list = $("<ol></ol>");
        var c = 0;
        wires.forEach(function(p,i) {
            var port = $("<li>").appendTo(list);
            if (p && p.length > 0) {
                $("<span>").text(i+1).appendTo(port);
                var links = $("<ul>").appendTo(port);
                p.forEach(function(d) {
                    c++;
                    var entry = $("<li>").appendTo(links);
                    var node = allNodes[d];
                    if (node) {
                        var def = RED.nodes.getType(node.type)||{};
                        createNode(node,def).appendTo(entry);
                    } else {
                        entry.text(d);
                    }
                })
            } else {
                port.text('none');
            }
        })
        if (c === 0) {
            result.text(RED._("diff.type.none"));
        } else {
            list.appendTo(result);
        }
        return result;
    }
    function createNodeIcon(node,def) {
        var nodeDiv = $("<div>",{class:"red-ui-diff-list-node-icon"});
        var colour = RED.utils.getNodeColor(node.type,def);
        var icon_url = RED.utils.getNodeIcon(def,node);
        if (node.type === 'tab') {
            colour = "#C0DEED";
        }
        nodeDiv.css('backgroundColor',colour);

        var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
        RED.utils.createIconElement(icon_url, iconContainer, false);

        return nodeDiv;
    }
    function createNode(node,def) {
        var nodeTitleDiv = $("<div>",{class:"red-ui-diff-list-node-title"})
        createNodeIcon(node,def).appendTo(nodeTitleDiv);
        var nodeLabel = node.label || node.name || node.id;
        $('<div>',{class:"red-ui-diff-list-node-description"}).text(nodeLabel).appendTo(nodeTitleDiv);
        return nodeTitleDiv;
    }
    function createNodeDiffRow(node,stats,CurrentDiff) {
        var localDiff = CurrentDiff.localDiff;
        var remoteDiff = CurrentDiff.remoteDiff;
        var conflicted = CurrentDiff.conflicts[node.id];

        var hasChanges = false; // exists in original and local/remote but with changes
        var unChanged = true; // existing in original,local,remote unchanged

        if (localDiff.added[node.id]) {
            stats.local.addedCount++;
            unChanged = false;
        }
        if (remoteDiff && remoteDiff.added[node.id]) {
            stats.remote.addedCount++;
            unChanged = false;
        }
        if (localDiff.deleted[node.id]) {
            stats.local.deletedCount++;
            unChanged = false;
        }
        if (remoteDiff && remoteDiff.deleted[node.id]) {
            stats.remote.deletedCount++;
            unChanged = false;
        }
        if (localDiff.changed[node.id]) {
            if (localDiff.positionChanged[node.id]) {
                stats.local.movedCount++
            } else {
                stats.local.changedCount++;
            }
            hasChanges = true;
            unChanged = false;
        }
        if (remoteDiff && remoteDiff.changed[node.id]) {
            if (remoteDiff.positionChanged[node.id]) {
                stats.remote.movedCount++
            } else {
                stats.remote.changedCount++;
            }
            hasChanges = true;
            unChanged = false;
        }
        // console.log(node.id,localDiff.added[node.id],remoteDiff.added[node.id],localDiff.deleted[node.id],remoteDiff.deleted[node.id],localDiff.changed[node.id],remoteDiff.changed[node.id])
        var def = RED.nodes.getType(node.type);
        if (def === undefined) {
            if (/^subflow:/.test(node.type)) {
                def = {
                    icon:"subflow.svg",
                    category: "subflows",
                    color: "#DDAA99",
                    defaults:{name:{value:""}}
                }
            } else if (node.type === "group") {
                def = RED.group.def;
            } else {
                def = {};
            }
        }
        var div = $("<div>",{class:"red-ui-diff-list-node collapsed"});
        var row = $("<div>",{class:"red-ui-diff-list-node-header"}).appendTo(div);

        var originalNodeDiv = $("<div>",{class:"red-ui-diff-list-node-cell"}).appendTo(row);
        var localNodeDiv = $("<div>",{class:"red-ui-diff-list-node-cell red-ui-diff-list-node-local"}).appendTo(row);
        var remoteNodeDiv;
        var chevron;
        if (remoteDiff) {
            remoteNodeDiv = $("<div>",{class:"red-ui-diff-list-node-cell red-ui-diff-list-node-remote"}).appendTo(row);
        }
        $('<span class="red-ui-diff-list-chevron"><i class="fa fa-angle-down"></i></span>').appendTo(originalNodeDiv);

        if (unChanged) {
            stats.local.unchangedCount++;
            createNode(node,def).appendTo(originalNodeDiv);
            localNodeDiv.addClass("red-ui-diff-status-unchanged");
            $('<span class="red-ui-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(localNodeDiv);
            if (remoteDiff) {
                stats.remote.unchangedCount++;
                remoteNodeDiv.addClass("red-ui-diff-status-unchanged");
                $('<span class="red-ui-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(remoteNodeDiv);
            }
            div.addClass("red-ui-diff-status-unchanged");
        } else if (localDiff.added[node.id]) {
            localNodeDiv.addClass("red-ui-diff-status-added");
            if (remoteNodeDiv) {
                remoteNodeDiv.addClass("red-ui-diff-empty");
            }
            $('<span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(localNodeDiv);
            createNode(node,def).appendTo(originalNodeDiv);
        } else if (remoteDiff && remoteDiff.added[node.id]) {
            localNodeDiv.addClass("red-ui-diff-empty");
            remoteNodeDiv.addClass("red-ui-diff-status-added");
            $('<span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(remoteNodeDiv);
            createNode(node,def).appendTo(originalNodeDiv);
        } else {
            createNode(node,def).appendTo(originalNodeDiv);
            if (localDiff.moved[node.id]) {
                var localN = localDiff.newConfig.all[node.id];
                if (!localDiff.deleted[node.z] && node.z !== localN.z && node.z !== "" && !localDiff.newConfig.all[node.z]) {
                    localNodeDiv.addClass("red-ui-diff-empty");
                } else {
                    localNodeDiv.addClass("red-ui-diff-status-moved");
                    var localMovedMessage = "";
                    if (node.z === localN.z) {
                        const movedFromNodeTab = localDiff.currentConfig.all[localDiff.currentConfig.all[node.id].z]
                        const movedFromLabel = `'${movedFromNodeTab ? (movedFromNodeTab.label || movedFromNodeTab.id) : 'global'}'`
                        localMovedMessage = RED._("diff.type.movedFrom",{id: movedFromLabel});
                    } else {
                        const movedToNodeTab = localDiff.newConfig.all[localN.z]
                        const movedToLabel = `'${movedToNodeTab ? (movedToNodeTab.label || movedToNodeTab.id) : 'global'}'`
                        localMovedMessage = RED._("diff.type.movedTo",{id:movedToLabel});
                    }
                    $('<span class="red-ui-diff-status"><i class="fa fa-caret-square-o-right"></i> '+localMovedMessage+'</span>').appendTo(localNodeDiv);
                }
            } else if (localDiff.deleted[node.z]) {
                localNodeDiv.addClass("red-ui-diff-empty");
            } else if (localDiff.deleted[node.id]) {
                localNodeDiv.addClass("red-ui-diff-status-deleted");
                $('<span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.deleted"></span></span>').appendTo(localNodeDiv);
            } else if (localDiff.changed[node.id]) {
                if (localDiff.newConfig.all[node.id].z !== node.z) {
                    localNodeDiv.addClass("red-ui-diff-empty");
                } else {
                    if (localDiff.positionChanged[node.id]) {
                        localNodeDiv.addClass("red-ui-diff-status-moved");
                        $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.moved"></span></span>').appendTo(localNodeDiv);
                    } else {
                        localNodeDiv.addClass("red-ui-diff-status-changed");
                        $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv);
                    }
                }
            } else {
                if (localDiff.newConfig.all[node.id].z !== node.z) {
                    localNodeDiv.addClass("red-ui-diff-empty");
                } else {
                    stats.local.unchangedCount++;
                    localNodeDiv.addClass("red-ui-diff-status-unchanged");
                    $('<span class="red-ui-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(localNodeDiv);
                }
            }

            if (remoteDiff) {
                if (remoteDiff.moved[node.id]) {
                    var remoteN = remoteDiff.newConfig.all[node.id];
                    if (!remoteDiff.deleted[node.z] && node.z !== remoteN.z && node.z !== "" && !remoteDiff.newConfig.all[node.z]) {
                        remoteNodeDiv.addClass("red-ui-diff-empty");
                    } else {
                        remoteNodeDiv.addClass("red-ui-diff-status-moved");
                        var remoteMovedMessage = "";
                        if (node.z === remoteN.z) {
                            const movedFromNodeTab = remoteDiff.currentConfig.all[remoteDiff.currentConfig.all[node.id].z]
                            const movedFromLabel = `'${movedFromNodeTab ? (movedFromNodeTab.label || movedFromNodeTab.id) : 'global'}'`
                            remoteMovedMessage = RED._("diff.type.movedFrom",{id:movedFromLabel});
                        } else {
                            const movedToNodeTab = remoteDiff.newConfig.all[remoteN.z]
                            const movedToLabel = `'${movedToNodeTab ? (movedToNodeTab.label || movedToNodeTab.id) : 'global'}'`
                            remoteMovedMessage = RED._("diff.type.movedTo",{id:movedToLabel});
                        }
                        $('<span class="red-ui-diff-status"><i class="fa fa-caret-square-o-right"></i> '+remoteMovedMessage+'</span>').appendTo(remoteNodeDiv);
                    }
                } else if (remoteDiff.deleted[node.z]) {
                    remoteNodeDiv.addClass("red-ui-diff-empty");
                } else if (remoteDiff.deleted[node.id]) {
                    remoteNodeDiv.addClass("red-ui-diff-status-deleted");
                    $('<span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.deleted"></span></span>').appendTo(remoteNodeDiv);
                } else if (remoteDiff.changed[node.id]) {
                    if (remoteDiff.newConfig.all[node.id].z !== node.z) {
                        remoteNodeDiv.addClass("red-ui-diff-empty");
                    } else {
                        if (remoteDiff.positionChanged[node.id]) {
                            remoteNodeDiv.addClass("red-ui-diff-status-moved");
                            $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.moved"></span></span>').appendTo(remoteNodeDiv);
                        } else {
                            remoteNodeDiv.addClass("red-ui-diff-status-changed");
                            $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv);
                        }
                    }
                } else {
                    if (remoteDiff.newConfig.all[node.id].z !== node.z) {
                        remoteNodeDiv.addClass("red-ui-diff-empty");
                    } else {
                        stats.remote.unchangedCount++;
                        remoteNodeDiv.addClass("red-ui-diff-status-unchanged");
                        $('<span class="red-ui-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(remoteNodeDiv);
                    }
                }
            }
        }
        var localNode = {
            node: localDiff.newConfig.all[node.id],
            all: localDiff.newConfig.all,
            diff: localDiff
        };
        var remoteNode;
        if (remoteDiff) {
            remoteNode = {
                node:remoteDiff.newConfig.all[node.id]||null,
                all: remoteDiff.newConfig.all,
                diff: remoteDiff
            }
        }

        var selectState = "";

        if (conflicted) {
            stats.conflicts++;
            if (!localNodeDiv.hasClass("red-ui-diff-empty")) {
                $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(localNodeDiv);
            }
            if (!remoteNodeDiv.hasClass("red-ui-diff-empty")) {
                $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(remoteNodeDiv);
            }
            div.addClass("red-ui-diff-list-node-conflict");
        } else {
            selectState = CurrentDiff.resolutions[node.id];
        }
        // Node row
        createNodeConflictRadioBoxes(node,div,localNodeDiv,remoteNodeDiv,false,!conflicted,selectState,CurrentDiff);
        row.on("click", function(evt) {
            $(this).parent().toggleClass('collapsed');

            if($(this).siblings('.red-ui-diff-list-node-properties').length === 0) {
                createNodePropertiesTable(def,node,localNode,remoteNode).appendTo(div);
            }
        });

        return div;
    }
    function createNodePropertiesTable(def,node,localNodeObj,remoteNodeObj) {
        var propertyElements = {};
        var localNode = localNodeObj.node;
        var remoteNode;
        if (remoteNodeObj) {
            remoteNode = remoteNodeObj.node;
        }

        var nodePropertiesDiv = $("<div>",{class:"red-ui-diff-list-node-properties"});
        var nodePropertiesTable = $("<table>").appendTo(nodePropertiesDiv);
        var nodePropertiesTableCols = $('<colgroup><col/><col/></colgroup>').appendTo(nodePropertiesTable);
        if (remoteNode !== undefined) {
            $("<col/>").appendTo(nodePropertiesTableCols);
        }
        var nodePropertiesTableBody = $("<tbody>").appendTo(nodePropertiesTable);

        var row;
        var localCell, remoteCell;
        var element;
        var currentValue, localValue, remoteValue;
        var localChanged = false;
        var remoteChanged = false;
        var localChanges = 0;
        var remoteChanges = 0;
        var conflict = false;
        var status;

        row = $("<tr>").appendTo(nodePropertiesTableBody);
        $("<td>",{class:"red-ui-diff-list-cell-label"}).text("id").appendTo(row);
        localCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row);
        if (localNode) {
            localCell.addClass("red-ui-diff-status-unchanged");
            $('<span class="red-ui-diff-status"></span>').appendTo(localCell);
            element = $('<span class="red-ui-diff-list-element"></span>').appendTo(localCell);
            propertyElements['local.id'] = RED.utils.createObjectElement(localNode.id).appendTo(element);
        } else {
            localCell.addClass("red-ui-diff-empty");
        }
        if (remoteNode !== undefined) {
            remoteCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-remote"}).appendTo(row);
            remoteCell.addClass("red-ui-diff-status-unchanged");
            if (remoteNode) {
                $('<span class="red-ui-diff-status"></span>').appendTo(remoteCell);
                element = $('<span class="red-ui-diff-list-element"></span>').appendTo(remoteCell);
                propertyElements['remote.id'] = RED.utils.createObjectElement(remoteNode.id).appendTo(element);
            } else {
                remoteCell.addClass("red-ui-diff-empty");
            }
        }

        if (node.hasOwnProperty('x')) {
            if (localNode) {
                if (localNode.x !== node.x || localNode.y !== node.y || localNode.w !== node.w || localNode.h !== node.h ) {
                    localChanged = true;
                    localChanges++;
                }
            }
            if (remoteNode) {
                if (remoteNode.x !== node.x || remoteNode.y !== node.y|| remoteNode.w !== node.w || remoteNode.h !== node.h) {
                    remoteChanged = true;
                    remoteChanges++;
                }
            }
            if ( (remoteChanged && localChanged && (localNode.x !== remoteNode.x || localNode.y !== remoteNode.y)) ||
                (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) ||
                (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id])
            ) {
                conflict = true;
            }
            row = $("<tr>").appendTo(nodePropertiesTableBody);
            $("<td>",{class:"red-ui-diff-list-cell-label"}).text(RED._("diff.type.position")).appendTo(row);
            localCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row);
            if (localNode) {
                localCell.addClass("red-ui-diff-status-"+(localChanged?"moved":"unchanged"));
                $('<span class="red-ui-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell);
                element = $('<span class="red-ui-diff-list-element"></span>').appendTo(localCell);
                var localPosition = {x:localNode.x,y:localNode.y};
                if (localNode.hasOwnProperty('w')) {
                    localPosition.w = localNode.w;
                    localPosition.h = localNode.h;
                }
                propertyElements['local.position'] = RED.utils.createObjectElement(localPosition,
                    {
                        path: "position",
                        exposeApi: true,
                        ontoggle: function(path,state) {
                            if (propertyElements['remote.'+path]) {
                                propertyElements['remote.'+path].prop('expand')(path,state)
                            }
                        }
                    }
                ).appendTo(element);
            } else {
                localCell.addClass("red-ui-diff-empty");
            }

            if (remoteNode !== undefined) {
                remoteCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-remote"}).appendTo(row);
                remoteCell.addClass("red-ui-diff-status-"+(remoteChanged?"moved":"unchanged"));
                if (remoteNode) {
                    $('<span class="red-ui-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell);
                    element = $('<span class="red-ui-diff-list-element"></span>').appendTo(remoteCell);
                    var remotePosition = {x:remoteNode.x,y:remoteNode.y};
                    if (remoteNode.hasOwnProperty('w')) {
                        remotePosition.w = remoteNode.w;
                        remotePosition.h = remoteNode.h;
                    }
                    propertyElements['remote.position'] = RED.utils.createObjectElement(remotePosition,
                        {
                            path: "position",
                            exposeApi: true,
                            ontoggle: function(path,state) {
                                if (propertyElements['local.'+path]) {
                                    propertyElements['local.'+path].prop('expand')(path,state);
                                }
                            }
                        }
                    ).appendTo(element);
                } else {
                    remoteCell.addClass("red-ui-diff-empty");
                }
            }
        }
        //
        localChanged = remoteChanged = conflict = false;
        if (node.hasOwnProperty('wires')) {
            currentValue = JSON.stringify(node.wires);
            if (localNode) {
                localValue = JSON.stringify(localNode.wires);
                if (currentValue !== localValue) {
                    localChanged = true;
                    localChanges++;
                }
            }
            if (remoteNode) {
                remoteValue = JSON.stringify(remoteNode.wires);
                if (currentValue !== remoteValue) {
                    remoteChanged = true;
                    remoteChanges++;
                }
            }
            if ( (remoteChanged && localChanged && (localValue !== remoteValue)) ||
                (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) ||
                (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id])
            ){
                conflict = true;
            }
            row = $("<tr>").appendTo(nodePropertiesTableBody);
            $("<td>",{class:"red-ui-diff-list-cell-label"}).text(RED._("diff.type.wires")).appendTo(row);
            localCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row);
            if (localNode) {
                if (!conflict) {
                    localCell.addClass("red-ui-diff-status-"+(localChanged?"changed":"unchanged"));
                    $('<span class="red-ui-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell);
                } else {
                    localCell.addClass("red-ui-diff-status-conflict");
                    $('<span class="red-ui-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(localCell);
                }
                formatWireProperty(localNode.wires,localNodeObj.all).appendTo(localCell);
            } else {
                localCell.addClass("red-ui-diff-empty");
            }

            if (remoteNode !== undefined) {
                remoteCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-remote"}).appendTo(row);
                if (remoteNode) {
                    if (!conflict) {
                        remoteCell.addClass("red-ui-diff-status-"+(remoteChanged?"changed":"unchanged"));
                        $('<span class="red-ui-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell);
                    } else {
                        remoteCell.addClass("red-ui-diff-status-conflict");
                        $('<span class="red-ui-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(remoteCell);
                    }
                    formatWireProperty(remoteNode.wires,remoteNodeObj.all).appendTo(remoteCell);
                } else {
                    remoteCell.addClass("red-ui-diff-empty");
                }
            }
        }
        var properties = Object.keys(node).filter(function(p) { return p!='inputLabels'&&p!='outputLabels'&&p!='z'&&p!='wires'&&p!=='x'&&p!=='y'&&p!=='w'&&p!=='h'&&p!=='id'&&p!=='type'&&(!def.defaults||!def.defaults.hasOwnProperty(p))});
        if (def.defaults) {
            properties = properties.concat(Object.keys(def.defaults));
        }
        if (node.type !== 'tab' && node.type !== "group") {
            properties = properties.concat(['inputLabels','outputLabels']);
        }
        if ( ((localNode && localNode.hasOwnProperty('icon')) || (remoteNode && remoteNode.hasOwnProperty('icon'))) &&
            properties.indexOf('icon') === -1
        ) {
            properties.unshift('icon');
        }


        properties.forEach(function(d) {
            localChanged = false;
            remoteChanged = false;
            conflict = false;
            currentValue = JSON.stringify(node[d]);
            if (localNode) {
                localValue = JSON.stringify(localNode[d]);
                if (currentValue !== localValue) {
                    localChanged = true;
                    localChanges++;
                }
            }
            if (remoteNode) {
                remoteValue = JSON.stringify(remoteNode[d]);
                if (currentValue !== remoteValue) {
                    remoteChanged = true;
                    remoteChanges++;
                }
            }

            if ( (remoteChanged && localChanged && (localValue !== remoteValue)) ||
                (!localChanged &&  remoteChanged && localNodeObj.diff.deleted[node.id]) ||
                (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id])
            ){
                conflict = true;
            }

            row = $("<tr>").appendTo(nodePropertiesTableBody);
            var propertyNameCell = $("<td>",{class:"red-ui-diff-list-cell-label"}).text(d).appendTo(row);
            localCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row);
            if (localNode) {
                if (!conflict) {
                    localCell.addClass("red-ui-diff-status-"+(localChanged?"changed":"unchanged"));
                    $('<span class="red-ui-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell);
                } else {
                    localCell.addClass("red-ui-diff-status-conflict");
                    $('<span class="red-ui-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(localCell);
                }
                element = $('<span class="red-ui-diff-list-element"></span>').appendTo(localCell);
                propertyElements['local.'+d] = RED.utils.createObjectElement(localNode[d],
                    {
                        path: d,
                        exposeApi: true,
                        ontoggle: function(path,state) {
                            if (propertyElements['remote.'+d]) {
                                propertyElements['remote.'+d].prop('expand')(path,state)
                            }
                        }
                    }
                ).appendTo(element);
            } else {
                localCell.addClass("red-ui-diff-empty");
            }
            if (remoteNode !== undefined) {
                remoteCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-remote"}).appendTo(row);
                if (remoteNode) {
                    if (!conflict) {
                        remoteCell.addClass("red-ui-diff-status-"+(remoteChanged?"changed":"unchanged"));
                        $('<span class="red-ui-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell);
                    } else {
                        remoteCell.addClass("red-ui-diff-status-conflict");
                        $('<span class="red-ui-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(remoteCell);
                    }
                    element = $('<span class="red-ui-diff-list-element"></span>').appendTo(remoteCell);
                    propertyElements['remote.'+d] = RED.utils.createObjectElement(remoteNode[d],
                        {
                            path: d,
                            exposeApi: true,
                            ontoggle: function(path,state) {
                                if (propertyElements['local.'+d]) {
                                    propertyElements['local.'+d].prop('expand')(path,state)
                                }
                            }
                        }
                    ).appendTo(element);
                } else {
                    remoteCell.addClass("red-ui-diff-empty");
                }
            }
            if (localNode && remoteNode && typeof localNode[d] === "string") {
                if (/\n/.test(localNode[d]) || /\n/.test(remoteNode[d])) {
                    var textDiff = $('<button class="red-ui-button red-ui-button-small red-ui-diff-text-diff-button"><i class="fa fa-file-o"> <i class="fa fa-caret-left"></i> <i class="fa fa-caret-right"></i> <i class="fa fa-file-o"></i></button>').on("click", function() {
                        showTextDiff(localNode[d],remoteNode[d]);
                    }).appendTo(propertyNameCell);
                    RED.popover.tooltip(textDiff, RED._("diff.compareChanges"));
                }
            }


        });
        return nodePropertiesDiv;
    }
    function createNodeConflictRadioBoxes(node,row,localDiv,remoteDiv,propertiesTable,hide,state,diff) {
        var safeNodeId = "red-ui-diff-selectbox-"+node.id.replace(/\./g,'-')+(propertiesTable?"-props":"");
        var className = "";
        if (node.z||propertiesTable) {
            className = "red-ui-diff-selectbox-tab-"+(propertiesTable?node.id:node.z).replace(/\./g,'-');
        }
        var titleRow = !propertiesTable && (node.type === 'tab' || node.type === 'subflow');
        var changeHandler = function(evt) {
            var className;
            if (node.type === undefined) {
                // TODO: handle globals
            } else if (titleRow) {
                className = "red-ui-diff-selectbox-tab-"+node.id.replace(/\./g,'-');
                $("."+className+"-"+this.value).prop('checked',true);
                if (this.value === 'local') {
                    $("."+className+"-"+this.value).closest(".red-ui-diff-list-node").addClass("red-ui-diff-select-local");
                    $("."+className+"-"+this.value).closest(".red-ui-diff-list-node").removeClass("red-ui-diff-select-remote");
                } else {
                    $("."+className+"-"+this.value).closest(".red-ui-diff-list-node").removeClass("red-ui-diff-select-local");
                    $("."+className+"-"+this.value).closest(".red-ui-diff-list-node").addClass("red-ui-diff-select-remote");
                }
            } else {
                // Individual node or properties table
                var parentId = "red-ui-diff-selectbox-"+(propertiesTable?node.id:node.z).replace(/\./g,'-');
                $('#'+parentId+"-local").prop('checked',false);
                $('#'+parentId+"-remote").prop('checked',false);
                var titleRowDiv = $('#'+parentId+"-local").closest(".red-ui-diff-list-flow").find(".red-ui-diff-list-flow-title");
                titleRowDiv.removeClass("red-ui-diff-select-local");
                titleRowDiv.removeClass("red-ui-diff-select-remote");
            }
            if (this.value === 'local') {
                row.removeClass("red-ui-diff-select-remote");
                row.addClass("red-ui-diff-select-local");
            } else if (this.value === 'remote') {
                row.addClass("red-ui-diff-select-remote");
                row.removeClass("red-ui-diff-select-local");
            }
            refreshConflictHeader(diff);
        }

        var localSelectDiv = $('<label>',{class:"red-ui-diff-selectbox",for:safeNodeId+"-local"}).on("click", function(e) { e.stopPropagation();}).appendTo(localDiv);
        var localRadio = $('<input>',{class:"red-ui-diff-selectbox-input "+className+"-local",id:safeNodeId+"-local",type:'radio',value:"local",name:safeNodeId}).data('node-id',node.id).on("change", changeHandler).appendTo(localSelectDiv);
        var remoteSelectDiv = $('<label>',{class:"red-ui-diff-selectbox",for:safeNodeId+"-remote"}).on("click", function(e) { e.stopPropagation();}).appendTo(remoteDiv);
        var remoteRadio = $('<input>',{class:"red-ui-diff-selectbox-input "+className+"-remote",id:safeNodeId+"-remote",type:'radio',value:"remote",name:safeNodeId}).data('node-id',node.id).on("change", changeHandler).appendTo(remoteSelectDiv);
        if (state === 'local') {
            localRadio.prop('checked',true);
        } else if (state === 'remote') {
            remoteRadio.prop('checked',true);
        }
        if (hide||localDiv.hasClass("red-ui-diff-empty") || remoteDiv.hasClass("red-ui-diff-empty")) {
            localSelectDiv.hide();
            remoteSelectDiv.hide();
        }

    }
    function refreshConflictHeader(currentDiff) {
        var resolutionCount = 0;
        $(".red-ui-diff-selectbox>input:checked").each(function() {
            if (currentDiff.conflicts[$(this).data('node-id')]) {
                resolutionCount++;
            }
            currentDiff.resolutions[$(this).data('node-id')] = $(this).val();
        })
        var conflictCount = Object.keys(currentDiff.conflicts).length;
        if (conflictCount - resolutionCount === 0) {
            $("#red-ui-diff-dialog-toolbar-resolved-conflicts").html('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-check"></i></span></span> '+RED._("diff.unresolvedCount",{count:conflictCount - resolutionCount}));
        } else {
            $("#red-ui-diff-dialog-toolbar-resolved-conflicts").html('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i></span></span> '+RED._("diff.unresolvedCount",{count:conflictCount - resolutionCount}));
        }
        if (conflictCount === resolutionCount) {
            $("#red-ui-diff-view-diff-merge").removeClass('disabled');
            $("#red-ui-diff-view-resolve-diff").removeClass('disabled');
        }
    }
    function getRemoteDiff(callback) {
        $.ajax({
            headers: {
                "Accept":"application/json",
            },
            cache: false,
            url: 'flows',
            success: function(nodes) {
                var localFlow = RED.nodes.createCompleteNodeSet();
                var originalFlow = RED.nodes.originalFlow();
                var remoteFlow = nodes.flows;
                var localDiff = generateDiff(originalFlow,localFlow);
                var remoteDiff = generateDiff(originalFlow,remoteFlow);
                remoteDiff.rev = nodes.rev;
                callback(resolveDiffs(localDiff,remoteDiff))
            }
        });

    }
    // function showLocalDiff() {
    //     var nns = RED.nodes.createCompleteNodeSet();
    //     var originalFlow = RED.nodes.originalFlow();
    //     var diff = generateDiff(originalFlow,nns);
    //     showDiff(diff);
    // }
    function showRemoteDiff(diff, options = {}) {
        if (!diff) {
            getRemoteDiff((remoteDiff) => showRemoteDiff(remoteDiff, options));
        } else {
            showDiff(diff,{...options, mode:'merge'});
        }
    }
    function parseNodes(nodeList) {
        var tabOrder = [];
        var tabs = {};
        var subflows = {};
        var globals = [];
        var all = {};

        nodeList.forEach(function(node) {
            all[node.id] = node;
            if (node.type === 'tab') {
                tabOrder.push(node.id);
                tabs[node.id] = {n:node,nodes:[]};
            } else if (node.type === 'subflow') {
                subflows[node.id] = {n:node,nodes:[]};
            }
        });

        nodeList.forEach(function(node) {
            if (node.type !== 'tab' && node.type !== 'subflow') {
                if (tabs[node.z]) {
                    tabs[node.z].nodes.push(node);
                } else if (subflows[node.z]) {
                    subflows[node.z].nodes.push(node);
                } else {
                    globals.push(node);
                }
            }
        });

        return {
            all: all,
            tabOrder: tabOrder,
            tabs: tabs,
            subflows: subflows,
            globals: globals
        }
    }
    function generateDiff(currentNodes,newNodes) {
        const currentConfig = parseNodes(currentNodes);
        const newConfig = parseNodes(newNodes);
        const added = {};
        const deleted = {};
        const changed = {};
        const positionChanged = {};
        const moved = {};

        Object.keys(currentConfig.all).forEach(function(id) {
            const node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id);
            if (!newConfig.all.hasOwnProperty(id)) {
                deleted[id] = true;
                return
            }
            const currentConfigJSON = JSON.stringify(currentConfig.all[id])
            const newConfigJSON = JSON.stringify(newConfig.all[id])
            
            if (currentConfigJSON !== newConfigJSON) {
                changed[id] = true;
                if (currentConfig.all[id].z !== newConfig.all[id].z) {
                    moved[id] = true;
                } else if (
                    currentConfig.all[id].x !== newConfig.all[id].x ||
                    currentConfig.all[id].y !== newConfig.all[id].y ||
                    currentConfig.all[id].w !== newConfig.all[id].w ||
                    currentConfig.all[id].h !== newConfig.all[id].h
                ) {
                    // This node's position on its parent has changed. We want to
                    // check if this is the *only* change for this given node
                    const currentNodeClone = JSON.parse(currentConfigJSON)
                    const newNodeClone = JSON.parse(newConfigJSON)

                    delete currentNodeClone.x
                    delete currentNodeClone.y
                    delete currentNodeClone.w
                    delete currentNodeClone.h
                    delete newNodeClone.x
                    delete newNodeClone.y
                    delete newNodeClone.w
                    delete newNodeClone.h
                    
                    if (JSON.stringify(currentNodeClone) === JSON.stringify(newNodeClone)) {
                        // Only the position has changed - everything else is the same
                        positionChanged[id] = true
                    }
                }

            }
        });
        Object.keys(newConfig.all).forEach(function(id) {
            if (!currentConfig.all.hasOwnProperty(id)) {
                added[id] = true;
            }
        });

        const diff = {
            currentConfig,
            newConfig,
            added,
            deleted,
            changed,
            positionChanged,
            moved
        };
        return diff;
    }
    function resolveDiffs(localDiff,remoteDiff) {
        var conflicted = {};
        var resolutions = {};
        var diff = {
            localDiff: localDiff,
            remoteDiff: remoteDiff,
            conflicts: conflicted,
            resolutions: resolutions
        }
        var seen = {};
        var id,node;
        for (id in localDiff.currentConfig.all) {
            if (localDiff.currentConfig.all.hasOwnProperty(id)) {
                seen[id] = true;
                var localNode = localDiff.newConfig.all[id];
                if (localDiff.changed[id] && remoteDiff.deleted[id]) {
                    conflicted[id] = true;
                } else if (localDiff.deleted[id] && remoteDiff.changed[id]) {
                    conflicted[id] = true;
                } else if (localDiff.changed[id] && remoteDiff.changed[id]) {
                    var remoteNode = remoteDiff.newConfig.all[id];
                    if (JSON.stringify(localNode) !== JSON.stringify(remoteNode)) {
                        conflicted[id] = true;
                    }
                }
                if (!conflicted[id]) {
                    if (remoteDiff.added[id]||remoteDiff.changed[id]||remoteDiff.deleted[id]) {
                        resolutions[id] = 'remote';
                    } else {
                        resolutions[id] = 'local';
                    }
                }
            }
        }
        for (id in localDiff.added) {
            if (localDiff.added.hasOwnProperty(id)) {
                node = localDiff.newConfig.all[id];
                if (remoteDiff.deleted[node.z]) {
                    conflicted[id] = true;
                    // conflicted[node.z] = true;
                } else {
                    resolutions[id] = 'local';
                }
            }
        }
        for (id in remoteDiff.added) {
            if (remoteDiff.added.hasOwnProperty(id)) {
                node = remoteDiff.newConfig.all[id];
                if (localDiff.deleted[node.z]) {
                    conflicted[id] = true;
                    // conflicted[node.z] = true;
                } else {
                    resolutions[id] = 'remote';
                }
            }
        }
        // console.log(diff.resolutions);
        // console.log(conflicted);
        return diff;
    }

    function showDiff(diff, options) {
        if (diffVisible) {
            return;
        }
        options = options || {};
        var mode = options.mode || 'merge';
        
        options.hidePositionChanges = true

        var localDiff = diff.localDiff;
        var remoteDiff = diff.remoteDiff;
        var conflicts = diff.conflicts;
        // currentDiff = diff;

        var trayOptions = {
            title: options.title||RED._("diff.reviewChanges"),
            width: Infinity,
            overlay: true,
            buttons: [
                {
                    text: RED._((options.mode === 'merge')?"common.label.cancel":"common.label.close"),
                    click: function() {
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                // trayWidth = dimensions.width;
            },
            open: function(tray) {
                var trayBody = tray.find('.red-ui-tray-body');
                var toolbar = $('<div class="red-ui-diff-dialog-toolbar">'+
                    '<span><span id="red-ui-diff-dialog-toolbar-resolved-conflicts"></span></span> '+
                    '</div>').prependTo(trayBody);
                var diffContainer = $('<div class="red-ui-diff-container"></div>').appendTo(trayBody);
                var diffTable = buildDiffPanel(diffContainer,diff,options);
                diffTable.list.hide();
                if (remoteDiff) {
                    $("#red-ui-diff-view-diff-merge").show();
                    if (Object.keys(conflicts).length === 0) {
                        $("#red-ui-diff-view-diff-merge").removeClass('disabled');
                    } else {
                        $("#red-ui-diff-view-diff-merge").addClass('disabled');
                    }
                } else {
                    $("#red-ui-diff-view-diff-merge").hide();
                }
                refreshConflictHeader(diff);
                // console.log("--------------");
                // console.log(localDiff);
                // console.log(remoteDiff);

                setTimeout(function() {
                    diffTable.finish();
                    diffTable.list.show();
                },300);
                $("#red-ui-sidebar-shade").show();
            },
            close: function() {
                diffVisible = false;
                $("#red-ui-sidebar-shade").hide();

            },
            show: function() {

            }
        }
        if (options.mode === 'merge') {
            trayOptions.buttons.push(
                {
                    id: "red-ui-diff-view-diff-merge",
                    text: RED._("deploy.confirm.button.merge"),
                    class: "primary disabled",
                    click: function() {
                        if (!$("#red-ui-diff-view-diff-merge").hasClass('disabled')) {
                            refreshConflictHeader(diff);
                            mergeDiff(diff);
                            if (options.onmerge) {
                                options.onmerge()
                            }
                            RED.tray.close();
                        }
                    }
                }
            );
        }

        RED.tray.show(trayOptions);
    }

    function applyDiff(diff) {
        var currentConfig = diff.localDiff.currentConfig;
        var localDiff = diff.localDiff;
        var remoteDiff = diff.remoteDiff;
        var conflicts = diff.conflicts;
        var resolutions = diff.resolutions;
        var id;

        for (id in conflicts) {
            if (conflicts.hasOwnProperty(id)) {
                if (!resolutions.hasOwnProperty(id)) {
                    console.log(diff);
                    throw new Error("No resolution for conflict on node",id);
                }
            }
        }

        var newConfig = [];
        var node;
        var nodeChangedStates = {};
        var nodeMovedStates = {};
        var localChangedStates = {};
        for (id in localDiff.newConfig.all) {
            if (localDiff.newConfig.all.hasOwnProperty(id)) {
                node = RED.nodes.node(id);
                if (resolutions[id] === 'local') {
                    if (node) {
                        nodeChangedStates[id] = node.changed;
                        nodeMovedStates[id] = node.moved;
                    }
                    newConfig.push(localDiff.newConfig.all[id]);
                } else if (resolutions[id] === 'remote') {
                    if (!remoteDiff.deleted[id] && remoteDiff.newConfig.all.hasOwnProperty(id)) {
                        if (node) {
                            nodeChangedStates[id] = node.changed;
                            nodeMovedStates[id] = node.moved;
                        }
                        localChangedStates[id] = 1;
                        newConfig.push(remoteDiff.newConfig.all[id]);
                    }
                } else {
                    console.log("Unresolved",id)
                }
            }
        }
        for (id in remoteDiff.added) {
            if (remoteDiff.added.hasOwnProperty(id)) {
                node = RED.nodes.node(id);
                if (node) {
                    nodeChangedStates[id] = node.changed;
                }
                if (!localDiff.added.hasOwnProperty(id)) {
                    localChangedStates[id] = 2;
                    newConfig.push(remoteDiff.newConfig.all[id]);
                }
            }
        }
        return {
            config: newConfig,
            nodeChangedStates,
            nodeMovedStates,
            localChangedStates
        }
    }

    function mergeDiff(diff) {
        //console.log(diff);
        var selectedTab = RED.workspaces.active();
        var appliedDiff = applyDiff(diff);

        var newConfig = appliedDiff.config;
        var nodeChangedStates = appliedDiff.nodeChangedStates;
        var nodeMovedStates = appliedDiff.nodeMovedStates;
        var localChangedStates = appliedDiff.localChangedStates;

        var isDirty = RED.nodes.dirty();

        var historyEvent = {
            t:"replace",
            config: RED.nodes.createCompleteNodeSet(),
            changed: nodeChangedStates,
            moved: nodeMovedStates,
            complete: true,
            dirty: isDirty,
            rev: RED.nodes.version()
        }

        RED.history.push(historyEvent);

        // var originalFlow = RED.nodes.originalFlow();
        // // originalFlow is what the editor thinks it loaded
        // //  - add any newly added nodes from remote diff as they are now part of the record
        // for (var id in diff.remoteDiff.added) {
        //     if (diff.remoteDiff.added.hasOwnProperty(id)) {
        //         if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) {
        //             originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id])));
        //         }
        //     }
        // }

        RED.nodes.clear();
        var imported = RED.nodes.import(newConfig);

        // // Restore the original flow so subsequent merge resolutions can properly
        // // identify new-vs-old
        // RED.nodes.originalFlow(originalFlow);

        // Clear all change flags from the import
        RED.nodes.dirty(false);
        
        const flowsToLock = new Set()
        function ensureUnlocked(id) {
            const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null);
            const isLocked = flow ? flow.locked : false;
            if (flow && isLocked) {
                flow.locked = false;
                flowsToLock.add(flow)
            }
        }
        imported.nodes.forEach(function(n) {
            if (nodeChangedStates[n.id]) {
                ensureUnlocked(n.z)
                n.changed = true;
            }
            if (nodeMovedStates[n.id]) {
                ensureUnlocked(n.z)
                n.moved = true;
            }
        })
        flowsToLock.forEach(flow => {
            flow.locked = true
        })

        RED.nodes.version(diff.remoteDiff.rev);

        if (isDirty) {
            RED.nodes.dirty(true);
        }

        RED.view.redraw(true);
        RED.palette.refresh();
        RED.workspaces.refresh();
        RED.workspaces.show(selectedTab, true);
        RED.sidebar.config.refresh();
    }

    function showTestFlowDiff(index) {
        if (index === 1) {
            var localFlow = RED.nodes.createCompleteNodeSet();
            var originalFlow = RED.nodes.originalFlow();
            showTextDiff(JSON.stringify(localFlow,null,4),JSON.stringify(originalFlow,null,4))
        } else if (index === 2) {
            var local = "1\n2\n3\n4\n5\nA\n6\n7\n8\n9\n";
            var remote = "1\nA\n2\n3\nD\nE\n6\n7\n8\n9\n";
            showTextDiff(local,remote);
        } else if (index === 3) {
            var local =  "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22";
            var remote = "1\nTWO\nTHREE\nEXTRA\n4\n5\n6\n7\n8\n9\n10\n11\n12\nTHIRTEEN\n14\n15\n16\n17\n18\n19\n20\n21\n22";
            showTextDiff(local,remote);
        }
    }

    function showTextDiff(textA,textB) {
        var trayOptions = {
            title: RED._("diff.compareChanges"),
            width: Infinity,
            overlay: true,
            buttons: [
                {
                    text: RED._("common.label.close"),
                    click: function() {
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                // trayWidth = dimensions.width;
            },
            open: function(tray) {
                var trayBody = tray.find('.red-ui-tray-body');
                var diffPanel = $('<div class="red-ui-diff-text"></div>').appendTo(trayBody);

                var codeTable = $("<table>",{class:"red-ui-diff-text-content"}).appendTo(diffPanel);
                $('<colgroup><col width="50"><col width="50%"><col width="50"><col width="50%"></colgroup>').appendTo(codeTable);
                var codeBody = $('<tbody>').appendTo(codeTable);
                var diffSummary = diffText(textA||"",textB||"");
                var aIndex = 0;
                var bIndex = 0;
                var diffLength = Math.max(diffSummary.a.length, diffSummary.b.length);

                var diffLines = [];
                var diffBlocks = [];
                var currentBlock;
                var blockLength = 0;
                var blockType = 0;

                for (var i=0;i<diffLength;i++) {
                    var diffLine = diffSummary[i];
                    var Adiff = (aIndex < diffSummary.a.length)?diffSummary.a[aIndex]:{type:2,line:""};
                    var Bdiff = (bIndex < diffSummary.b.length)?diffSummary.b[bIndex]:{type:2,line:""};
                    if (Adiff.type === 0 && Bdiff.type !== 0) {
                        Adiff = {type:2,line:""};
                        bIndex++;
                    } else if (Bdiff.type === 0 && Adiff.type !== 0) {
                        Bdiff = {type:2,line:""};
                        aIndex++;
                    } else {
                        aIndex++;
                        bIndex++;
                    }
                    diffLines.push({
                        a: Adiff,
                        b: Bdiff
                    });
                    if (currentBlock === undefined) {
                        currentBlock = {start:i,end:i};
                        blockLength = 0;
                        blockType = (Adiff.type === 0 && Bdiff.type === 0)?0:1;
                    } else {
                        if (Adiff.type === 0 && Bdiff.type === 0) {
                            // Unchanged line
                            if (blockType === 0) {
                                // still unchanged - extend the block
                                currentBlock.end = i;
                                blockLength++;
                            } else if (blockType === 1) {
                                // end of a change
                                currentBlock.end = i;
                                blockType = 2;
                                blockLength = 0;
                            } else if (blockType === 2) {
                                // post-change unchanged
                                currentBlock.end = i;
                                blockLength++;
                                if (blockLength === 8) {
                                    currentBlock.end -= 5; // rollback the end
                                    diffBlocks.push(currentBlock);
                                    currentBlock = {start:i-5,end:i-5};
                                    blockType = 0;
                                    blockLength = 0;
                                }
                            }
                        } else {
                            // in a change
                            currentBlock.end = i;
                            blockLength++;
                            if (blockType === 0) {
                                if (currentBlock.end > 3) {
                                    currentBlock.end -= 3;
                                    currentBlock.empty = true;
                                    diffBlocks.push(currentBlock);
                                    currentBlock = {start:i-3,end:i-3};
                                }
                                blockType = 1;
                            } else if (blockType === 2) {
                                // we were in unchanged, but hit a change again
                                blockType = 1;
                            }
                        }
                    }
                }
                if (blockType === 0) {
                    currentBlock.empty = true;
                }
                currentBlock.end = diffLength;
                diffBlocks.push(currentBlock);
                console.table(diffBlocks);
                var diffRow;
                for (var b = 0; b<diffBlocks.length; b++) {
                    currentBlock = diffBlocks[b];
                    if (currentBlock.empty) {
                        diffRow = createExpandLine(currentBlock.start,currentBlock.end,diffLines).appendTo(codeBody);
                    } else {
                        for (var i=currentBlock.start;i<currentBlock.end;i++) {
                            var row = createDiffLine(diffLines[i]).appendTo(codeBody);
                            if (i === currentBlock.start) {
                                row.addClass("start-block");
                            } else if (i === currentBlock.end-1) {
                                row.addClass("end-block");
                            }
                        }
                    }
                }

            },
            close: function() {
                diffVisible = false;

            },
            show: function() {

            }
        }
        RED.tray.show(trayOptions);
    }

    function createExpandLine(start,end,diffLines) {
        diffRow = $('<tr class="red-ui-diff-text-header red-ui-diff-text-expand">');
        var content = $('<td colspan="4"> <i class="fa fa-arrows-v"></i> </td>').appendTo(diffRow);
        var label = $('<span></span>').appendTo(content);
        if (end < diffLines.length-1) {
            label.text("@@ -"+(diffLines[end-1].a.i+1)+" +"+(diffLines[end-1].b.i+1));
        }
        diffRow.on("click", function(evt) {
            // console.log(start,end,diffLines.length);
            if (end - start > 20) {
                var startPos = $(this).offset();
                // console.log(startPos);
                if (start > 0) {
                    for (var i=start;i<start+10;i++) {
                        createDiffLine(diffLines[i]).addClass("unchanged").insertBefore($(this));
                    }
                    start += 10;
                }
                if (end < diffLines.length-1) {
                    for (var i=end-1;i>end-11;i--) {
                        createDiffLine(diffLines[i]).addClass("unchanged").insertAfter($(this));
                    }
                    end -= 10;
                }
                if (end < diffLines.length-1) {
                    label.text("@@ -"+(diffLines[end-1].a.i+1)+" +"+(diffLines[end-1].b.i+1));
                }
                var endPos = $(this).offset();
                var delta = endPos.top - startPos.top;
                $(".red-ui-diff-text").scrollTop($(".red-ui-diff-text").scrollTop() + delta);
            } else {
                for (var i=start;i<end;i++) {
                    createDiffLine(diffLines[i]).addClass("unchanged").insertBefore($(this));
                }
                $(this).remove();
            }
        });
        return diffRow;
    }

    function createDiffLine(diffLine) {
        var diffRow = $('<tr>');
        var Adiff = diffLine.a;
        var Bdiff = diffLine.b;
        //console.log(diffLine);
        var cellNo = $('<td class="lineno">').text(Adiff.type === 2?"":Adiff.i).appendTo(diffRow);
        var cellLine = $('<td class="linetext">').text(Adiff.line).appendTo(diffRow);
        if (Adiff.type === 2) {
            cellNo.addClass('blank');
            cellLine.addClass('blank');
        } else if (Adiff.type === 4) {
            cellNo.addClass('added');
            cellLine.addClass('added');
        } else if (Adiff.type === 1) {
            cellNo.addClass('removed');
            cellLine.addClass('removed');
        }
        cellNo = $('<td class="lineno">').text(Bdiff.type === 2?"":Bdiff.i).appendTo(diffRow);
        cellLine = $('<td class="linetext">').text(Bdiff.line).appendTo(diffRow);
        if (Bdiff.type === 2) {
            cellNo.addClass('blank');
            cellLine.addClass('blank');
        } else if (Bdiff.type === 4) {
            cellNo.addClass('added');
            cellLine.addClass('added');
        } else if (Bdiff.type === 1) {
            cellNo.addClass('removed');
            cellLine.addClass('removed');
        }
        return diffRow;
    }

    function diffText(string1, string2,ignoreWhitespace) {
        var lines1 = string1.split(/\r?\n/);
        var lines2 = string2.split(/\r?\n/);
        var i = lines1.length;
        var j = lines2.length;
        var k;
        var m;
        var diffSummary = {a:[],b:[]};
        var diffMap = [];
        for (k = 0; k < i + 1; k++) {
            diffMap[k] = [];
            for (m = 0; m < j + 1; m++) {
                diffMap[k][m] = 0;
            }
        }
        var c = 0;
        for (k = i - 1; k >= 0; k--) {
            for (m = j - 1; m >=0; m--) {
                c++;
                if (compareLines(lines1[k],lines2[m],ignoreWhitespace) !== 1) {
                    diffMap[k][m] = diffMap[k+1][m+1]+1;
                } else {
                    diffMap[k][m] = Math.max(diffMap[(k + 1)][m], diffMap[k][(m + 1)]);
                }
            }
        }
        //console.log(c);
        k = 0;
        m = 0;

        while ((k < i) && (m < j)) {
            var n = compareLines(lines1[k],lines2[m],ignoreWhitespace);
            if (n !== 1) {
                var d = 0;
                if (n===0) {
                    d = 0;
                } else if (n==2) {
                    d = 3;
                }
                diffSummary.a.push({i:k+1,j:m+1,line:lines1[k],type:d});
                diffSummary.b.push({i:m+1,j:k+1,line:lines2[m],type:d});
                k++;
                m++;
            } else if (diffMap[(k + 1)][m] >= diffMap[k][(m + 1)]) {
                diffSummary.a.push({i:k+1,line:lines1[k],type:1});
                k++;
            } else {
                diffSummary.b.push({i:m+1,line:lines2[m],type:4});
                m++;
            }
        }
        while ((k < i) || (m < j)) {
            if (k == i) {
                diffSummary.b.push({i:m+1,line:lines2[m],type:4});
                m++;
            } else if (m == j) {
                diffSummary.a.push({i:k+1,line:lines1[k],type:1});
                k++;
            }
        }
        return diffSummary;
    }

    function compareLines(string1, string2, ignoreWhitespace) {
        if (ignoreWhitespace) {
            if (string1 === string2) {
                return 0;
            }
            return string1.trim() === string2.trime() ? 2 : 1;
        }
        return string1 === string2 ? 0 : 1;
    }

    function createUnifiedDiffTable(files,commitOptions) {
        var diffPanel = $('<div></div>');
        files.forEach(function(file) {
            var hunks = file.hunks;
            var isBinary = file.binary;
            var codeTable = $("<table>",{class:"red-ui-diff-text-content"}).appendTo(diffPanel);
            $('<colgroup><col width="50"><col width="50"><col width="100%"></colgroup>').appendTo(codeTable);
            var codeBody = $('<tbody>').appendTo(codeTable);

            var diffFileRow = $('<tr class="red-ui-diff-text-file-header">').appendTo(codeBody);
            var content = $('<td colspan="3"></td>').appendTo(diffFileRow);

            var chevron = $('<i class="red-ui-diff-list-chevron fa fa-angle-down"></i>').appendTo(content);
            diffFileRow.on("click", function(e) {
                diffFileRow.toggleClass("collapsed");
                var isCollapsed = diffFileRow.hasClass("collapsed");
                diffFileRow.nextUntil(".red-ui-diff-text-file-header").toggle(!isCollapsed);
            })
            var label = $('<span class="filename"></span>').text(file.file).appendTo(content);

            var conflictHeader;
            var unresolvedConflicts = 0;
            var resolvedConflicts = 0;
            var conflictResolutions = {};
            if (commitOptions.project.files && commitOptions.project.files.flow === file.file) {
                if (commitOptions.unmerged) {
                    $('<span style="float: right;"><span id="red-ui-diff-dialog-toolbar-resolved-conflicts"></span></span>').appendTo(content);
                }
                var diffRow = $('<tr class="red-ui-diff-text-header">').appendTo(codeBody);
                var flowDiffContent = $('<td class="red-ui-diff-flow-diff" colspan="3"></td>').appendTo(diffRow);

                var projectName = commitOptions.project.name;
                var filename = commitOptions.project.files.flow;
                var commonVersionUrl = "projects/"+projectName+"/files/"+commitOptions.commonRev+"/"+filename;
                var oldVersionUrl = "projects/"+projectName+"/files/"+commitOptions.oldRev+"/"+filename;
                var newVersionUrl = "projects/"+projectName+"/files/"+commitOptions.newRev+"/"+filename;
                var promises = [$.Deferred(),$.Deferred(),$.Deferred()];
                if (commitOptions.commonRev) {
                    var commonVersionUrl = "projects/"+projectName+"/files/"+commitOptions.commonRev+"/"+filename;
                    $.ajax({dataType: "json",url: commonVersionUrl}).then(function(data) { promises[0].resolve(data); }).fail(function() { promises[0].resolve(null);})
                } else {
                    promises[0].resolve(null);
                }

                $.ajax({dataType: "json",url: oldVersionUrl}).then(function(data) { promises[1].resolve(data); }).fail(function() { promises[1].resolve({content:"[]"});})
                $.ajax({dataType: "json",url: newVersionUrl}).then(function(data) { promises[2].resolve(data); }).fail(function() { promises[2].resolve({content:"[]"});})
                $.when.apply($,promises).always(function(commonVersion, oldVersion,newVersion) {
                    var commonFlow;
                    var oldFlow;
                    var newFlow;
                    if (commonVersion) {
                        try {
                            commonFlow = JSON.parse(commonVersion.content||"[]");
                        } catch(err) {
                            console.log(RED._("diff.commonVersionError"),commonVersionUrl);
                            console.log(err);
                            return;
                        }
                    }
                    try {
                        oldFlow = JSON.parse(oldVersion.content||"[]");
                    } catch(err) {
                        console.log(RED._("diff.oldVersionError"),oldVersionUrl);
                        console.log(err);
                        return;
                    }
                    if (!commonFlow) {
                        commonFlow = oldFlow;
                    }
                    try {
                        newFlow = JSON.parse(newVersion.content||"[]");
                    } catch(err) {
                        console.log(RED._("diff.newVersionError"),newFlow);
                        console.log(err);
                        return;
                    }
                    var localDiff = generateDiff(commonFlow,oldFlow);
                    var remoteDiff = generateDiff(commonFlow,newFlow);
                    commitOptions.currentDiff = resolveDiffs(localDiff,remoteDiff);
                    var diffTable = buildDiffPanel(flowDiffContent,commitOptions.currentDiff,{
                        title: filename,
                        mode: commitOptions.commonRev?'merge':'view',
                        oldRevTitle: commitOptions.oldRevTitle,
                        newRevTitle: commitOptions.newRevTitle
                    });
                    diffTable.list.hide();
                    refreshConflictHeader(commitOptions.currentDiff);
                    setTimeout(function() {
                        diffTable.finish();
                        diffTable.list.show();
                    },300);
                    // var flowDiffRow = $("<tr>").insertAfter(diffRow);
                    // var content = $('<td colspan="3"></td>').appendTo(flowDiffRow);
                    // currentDiff = diff;
                    // var diffTable = buildDiffPanel(content,diff,{mode:"view"}).finish();
                });



            } else

            if (isBinary) {
                var diffBinaryRow = $('<tr class="red-ui-diff-text-header">').appendTo(codeBody);
                var binaryContent = $('<td colspan="3"></td>').appendTo(diffBinaryRow);
                $('<span></span>').text(RED._("diff.noBinaryFileShowed")).appendTo(binaryContent);

            } else {
                if (commitOptions.unmerged) {
                    conflictHeader = $('<span style="float: right;">'+RED._("diff.conflictHeader",{resolved:resolvedConflicts, unresolved:unresolvedConflicts})+'</span>').appendTo(content);
                }
                hunks.forEach(function(hunk) {
                    var diffRow = $('<tr class="red-ui-diff-text-header">').appendTo(codeBody);
                    var content = $('<td colspan="3"></td>').appendTo(diffRow);
                    var label = $('<span></span>').text(hunk.header).appendTo(content);
                    var isConflict = hunk.conflict;
                    var localLine = hunk.localStartLine;
                    var remoteLine = hunk.remoteStartLine;
                    if (isConflict) {
                        unresolvedConflicts++;
                    }

                    hunk.lines.forEach(function(lineText,lineNumber) {
                        // if (lineText[0] === '\\' || lineText === "") {
                        //     // Comment line - bail out of this hunk
                        //     break;
                        // }

                        var actualLineNumber = hunk.diffStart + lineNumber;
                        var isMergeHeader = isConflict && /^\+\+(<<<<<<<|=======$|>>>>>>>)/.test(lineText);
                        var diffRow = $('<tr>').appendTo(codeBody);
                        var localLineNo = $('<td class="lineno">').appendTo(diffRow);
                        var remoteLineNo;
                        if (!isMergeHeader) {
                            remoteLineNo = $('<td class="lineno">').appendTo(diffRow);
                        } else {
                            localLineNo.attr('colspan',2);
                        }
                        var line = $('<td class="linetext">').appendTo(diffRow);
                        var prefixStart = 0;
                        var prefixEnd = 1;
                        if (isConflict) {
                            prefixEnd = 2;
                        }
                        if (!isMergeHeader) {
                            var changeMarker = lineText[0];
                            if (isConflict && !commitOptions.unmerged && changeMarker === ' ') {
                                changeMarker = lineText[1];
                            }
                            $('<span class="prefix">').text(changeMarker).appendTo(line);
                            var handledlLine = false;
                            if (isConflict && commitOptions.unmerged) {
                                $('<span class="prefix">').text(lineText[1]).appendTo(line);
                                if (lineText[0] === '+') {
                                    localLineNo.text(localLine++);
                                    handledlLine = true;
                                }
                                if (lineText[1] === '+') {
                                    remoteLineNo.text(remoteLine++);
                                    handledlLine = true;
                                }
                            } else {
                                if (lineText[0] === '+' || (isConflict && lineText[1] === '+')) {
                                    localLineNo.addClass("added");
                                    remoteLineNo.addClass("added");
                                    line.addClass("added");
                                    remoteLineNo.text(remoteLine++);
                                    handledlLine = true;
                                } else if (lineText[0] === '-' || (isConflict && lineText[1] === '-')) {
                                    localLineNo.addClass("removed");
                                    remoteLineNo.addClass("removed");
                                    line.addClass("removed");
                                    localLineNo.text(localLine++);
                                    handledlLine = true;
                                }
                            }
                            if (!handledlLine) {
                                line.addClass("unchanged");
                                if (localLine > 0 && lineText[0] !== '\\' && lineText !== "") {
                                    localLineNo.text(localLine++);
                                }
                                if (remoteLine > 0 && lineText[0] !== '\\' && lineText !== "") {
                                    remoteLineNo.text(remoteLine++);
                                }
                            }
                            $('<span>').text(lineText.substring(prefixEnd)).appendTo(line);
                        } else {
                            diffRow.addClass("mergeHeader");
                            var isSeparator = /^\+\+=======$/.test(lineText);
                            if (!isSeparator) {
                                var isOurs = /^..<<<<<<</.test(lineText);
                                if (isOurs) {
                                    $('<span>').text("<<<<<<< " + RED._("diff.localChanges")).appendTo(line);
                                    hunk.localChangeStart = actualLineNumber;
                                } else {
                                    hunk.remoteChangeEnd = actualLineNumber;
                                    $('<span>').text(">>>>>>> " + RED._("diff.remoteChanges")).appendTo(line);
                                }
                                diffRow.addClass("mergeHeader-"+(isOurs?"ours":"theirs"));
                                $('<button class="red-ui-button red-ui-button-small" style="float: right; margin-right: 20px;"><i class="fa fa-angle-double-'+(isOurs?"down":"up")+'"></i> '+RED._(isOurs?"diff.useLocalChanges":"diff.useRemoteChanges")+'</button>')
                                    .appendTo(line)
                                    .on("click", function(evt) {
                                        evt.preventDefault();
                                        resolvedConflicts++;
                                        var addedRows;
                                        var midRow;
                                        if (isOurs) {
                                            addedRows = diffRow.nextUntil(".mergeHeader-separator");
                                            midRow = addedRows.last().next();
                                            midRow.nextUntil(".mergeHeader").remove();
                                            midRow.next().remove();
                                        } else {
                                            addedRows = diffRow.prevUntil(".mergeHeader-separator");
                                            midRow = addedRows.last().prev();
                                            midRow.prevUntil(".mergeHeader").remove();
                                            midRow.prev().remove();
                                        }
                                        midRow.remove();
                                        diffRow.remove();
                                        addedRows.find(".linetext").addClass('added');
                                        conflictHeader.empty();
                                        $('<span>'+RED._("diff.conflictHeader",{resolved:resolvedConflicts, unresolved:unresolvedConflicts})+'</span>').appendTo(conflictHeader);

                                        conflictResolutions[file.file] = conflictResolutions[file.file] || {};
                                        conflictResolutions[file.file][hunk.localChangeStart] = {
                                            changeStart: hunk.localChangeStart,
                                            separator: hunk.changeSeparator,
                                            changeEnd: hunk.remoteChangeEnd,
                                            selection: isOurs?"A":"B"
                                        }
                                        if (commitOptions.resolveConflict) {
                                            commitOptions.resolveConflict({
                                                conflicts: unresolvedConflicts,
                                                resolved: resolvedConflicts,
                                                resolutions: conflictResolutions
                                            });
                                        }
                                    })
                            } else {
                                hunk.changeSeparator = actualLineNumber;
                                diffRow.addClass("mergeHeader-separator");
                            }
                        }
                    });
                });
            }
        });
        return diffPanel;
    }

    function showCommitDiff(options) {
        var commit = parseCommitDiff(options.commit);
        var trayOptions = {
            title: RED._("diff.viewCommitDiff"),
            width: Infinity,
            overlay: true,
            buttons: [
                {
                    text: RED._("common.label.close"),
                    click: function() {
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                // trayWidth = dimensions.width;
            },
            open: function(tray) {
                var trayBody = tray.find('.red-ui-tray-body');
                var diffPanel = $('<div class="red-ui-diff-text"></div>').appendTo(trayBody);

                var codeTable = $("<table>",{class:"red-ui-diff-text-content"}).appendTo(diffPanel);
                $('<colgroup><col width="50"><col width="50"><col width="100%"></colgroup>').appendTo(codeTable);
                var codeBody = $('<tbody>').appendTo(codeTable);

                var diffRow = $('<tr class="red-ui-diff-text-commit-header">').appendTo(codeBody);
                var content = $('<td colspan="3"></td>').appendTo(diffRow);

                $("<h3>").text(commit.title).appendTo(content);
                $('<div class="commit-body"></div>').text(commit.comment).appendTo(content);
                var summary = $('<div class="commit-summary"></div>').appendTo(content);
                $('<div style="float: right">').text(RED._('diff.commit')+" "+commit.sha).appendTo(summary);
                $('<div>').text((commit.authorName||commit.author)+" - "+options.date).appendTo(summary);

                if (commit.files) {
                    createUnifiedDiffTable(commit.files,options).appendTo(diffPanel);
                }


            },
            close: function() {
                diffVisible = false;
            },
            show: function() {

            }
        }
        RED.tray.show(trayOptions);
    }
    function showUnifiedDiff(options) {
        var diff = options.diff;
        var title = options.title;
        var files = parseUnifiedDiff(diff);

        var currentResolution;
        if (options.unmerged) {
            options.resolveConflict = function(results) {
                currentResolution = results;
                if (results.conflicts === results.resolved) {
                    $("#red-ui-diff-view-resolve-diff").removeClass('disabled');
                }
            }
        }

        var trayOptions = {
            title: title|| RED._("diff.compareChanges"),
            width: Infinity,
            overlay: true,
            buttons: [
                {
                    text: RED._((options.unmerged)?"common.label.cancel":"common.label.close"),
                    click: function() {
                        if (options.oncancel) {
                            options.oncancel();
                        }
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                // trayWidth = dimensions.width;
            },
            open: function(tray) {
                var trayBody = tray.find('.red-ui-tray-body');
                var diffPanel = $('<div class="red-ui-diff-text"></div>').appendTo(trayBody);
                createUnifiedDiffTable(files,options).appendTo(diffPanel);
            },
            close: function() {
                diffVisible = false;
            },
            show: function() {

            }
        }
        if (options.unmerged) {
            trayOptions.buttons.push(
                {
                    id: "red-ui-diff-view-resolve-diff",
                    text: RED._("diff.saveConflict"),
                    class: "primary disabled",
                    click: function() {
                        if (!$("#red-ui-diff-view-resolve-diff").hasClass('disabled')) {
                            if (options.currentDiff) {
                                // This is a flow file. Need to apply the diff
                                // and generate the new flow.
                                var result = applyDiff(options.currentDiff);
                                currentResolution = {
                                    resolutions:{}
                                };
                                currentResolution.resolutions[options.project.files.flow] = JSON.stringify(result.config,"",4);
                            }
                            if (options.onresolve) {
                                options.onresolve(currentResolution);
                            }
                            RED.tray.close();
                        }
                    }
                }
            );
        }
        RED.tray.show(trayOptions);
    }

    function parseCommitDiff(diff) {
        var result = {};
        var lines = diff.split("\n");
        var comment = [];
        for (var i=0;i<lines.length;i++) {
            if (/^commit /.test(lines[i])) {
                result.sha = lines[i].substring(7);
            } else if (/^Author: /.test(lines[i])) {
                result.author = lines[i].substring(8);
                var m = /^(.*) <(.*)>$/.exec(result.author);
                if (m) {
                    result.authorName = m[1];
                    result.authorEmail = m[2];
                }
            } else if (/^Date: /.test(lines[i])) {
                result.date = lines[i].substring(8);
            } else if (/^    /.test(lines[i])) {
                if (!result.title) {
                    result.title = lines[i].substring(4);
                } else {
                    if (lines[i].length !== 4 || comment.length > 0) {
                        comment.push(lines[i].substring(4));
                    }
                }
            } else if (/^diff /.test(lines[i])) {
                result.files = parseUnifiedDiff(lines.slice(i));
                break;
            }
         }
         result.comment = comment.join("\n");
         return result;
    }
    function parseUnifiedDiff(diff) {
        var lines;
        if (Array.isArray(diff)) {
            lines = diff;
        } else {
            lines = diff.split("\n");
        }
        var diffHeader = /^diff (?:(?:--git a\/(.*) b\/(.*))|(?:--cc (.*)))$/;
        var fileHeader = /^\+\+\+ b\/(.*)\t?/;
        var binaryFile = /^Binary files /;
        var hunkHeader = /^@@ -((\d+)(,(\d+))?) \+((\d+)(,(\d+))?) @@ ?(.*)$/;
        var conflictHunkHeader = /^@+ -((\d+)(,(\d+))?) -((\d+)(,(\d+))?) \+((\d+)(,(\d+))?) @+/;
        var files = [];
        var currentFile;
        var hunks = [];
        var currentHunk;
        for (var i=0;i<lines.length;i++) {
            var line = lines[i];
            var diffLine = diffHeader.exec(line);
            if (diffLine) {
                if (currentHunk) {
                    currentFile.hunks.push(currentHunk);
                    files.push(currentFile);
                }
                currentHunk = null;
                currentFile = {
                    file: diffLine[1]||diffLine[3],
                    hunks: []
                }
            } else if (binaryFile.test(line)) {
                if (currentFile) {
                    currentFile.binary = true;
                }
            } else {
                var fileLine = fileHeader.exec(line);
                if (fileLine) {
                    currentFile.file = fileLine[1];
                } else {
                    var hunkLine = hunkHeader.exec(line);
                    if (hunkLine) {
                        if (currentHunk) {
                            currentFile.hunks.push(currentHunk);
                        }
                        currentHunk = {
                            header: line,
                            localStartLine: hunkLine[2],
                            localLength: hunkLine[4]||1,
                            remoteStartLine: hunkLine[6],
                            remoteLength: hunkLine[8]||1,
                            lines: [],
                            conflict: false
                        }
                        continue;
                    }
                    hunkLine = conflictHunkHeader.exec(line);
                    if (hunkLine) {
                        if (currentHunk) {
                            currentFile.hunks.push(currentHunk);
                        }
                        currentHunk = {
                            header: line,
                            localStartLine: hunkLine[2],
                            localLength: hunkLine[4]||1,
                            remoteStartLine: hunkLine[6],
                            remoteLength: hunkLine[8]||1,
                            diffStart: parseInt(hunkLine[10]),
                            lines: [],
                            conflict: true
                        }
                        continue;
                    }
                    if (currentHunk) {
                        currentHunk.lines.push(line);
                    }
                }
            }
        }
        if (currentHunk) {
            currentFile.hunks.push(currentHunk);
        }
        files.push(currentFile);
        return files;
    }

    return {
        init: init,
        getRemoteDiff: getRemoteDiff,
        showRemoteDiff: showRemoteDiff,
        showUnifiedDiff: showUnifiedDiff,
        showCommitDiff: showCommitDiff,
        mergeDiff: mergeDiff
    }
})();
;/**
 * 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.
 **/
RED.keyboard = (function() {

    var isMac = /Mac/i.test(window.navigator.platform);

    var handlersActive = true;

    var handlers = {};

    var knownShortcuts;

    var partialState;

    var keyMap = {
        "left":37,
        "up":38,
        "right":39,
        "down":40,
        "escape":27,
        "enter": 13,
        "backspace": 8,
        "delete": 46,
        "space": 32,
        ";":186,
        "=":187,
        "+":187, // <- QWERTY specific
        ",":188,
        "-":189,
        ".":190,
        "/":191,
        "\\":220,
        "'":222,
        "?":191, // <- QWERTY specific
        "[": 219,
        "]": 221,
        "{": 219,// <- QWERTY specific
        "}": 221 // <- QWERTY specific
    };
    var metaKeyCodes = {
        16: true,
        17: true,
        18: true,
        91: true,
        93: true
    };
    var actionToKeyMap = {};
    var defaultKeyMap = {};

    // FF generates some different keycodes because reasons.
    var firefoxKeyCodeMap = {
        59:186,
        61:187,
        173:189
    };

    function migrateOldKeymap() {
        // pre-0.18
        if ('localStorage' in window && window['localStorage'] !== null) {
            var oldKeyMap = localStorage.getItem("keymap");
            if (oldKeyMap !== null) {
                localStorage.removeItem("keymap");
                RED.settings.set('editor.keymap',JSON.parse(oldKeyMap));
            }
        }

    }

    function getUserKey(action) {
        return RED.settings.get('editor.keymap',{})[action];
    }

    function mergeKeymaps(defaultKeymap, themeKeymap) {
        // defaultKeymap has format: { scope: { key: action , key: action }}
        // themeKeymap has format: {action: {scope,key}, action: {scope:key}}


        var mergedKeymap = {};
        for (var scope in defaultKeymap) {
            if (defaultKeymap.hasOwnProperty(scope)) {
                var keys = defaultKeymap[scope];
                for (var key in keys) {
                    if (keys.hasOwnProperty(key)) {
                        if (!mergedKeymap[keys[key]]) {
                            mergedKeymap[keys[key]] = [{
                                scope:scope,
                                key:key,
                                user:false
                            }];
                        } else {
                            mergedKeymap[keys[key]].push({
                                scope:scope,
                                key:key,
                                user:false
                            });
                        }
                    }
                }
            }
        }
        for (var action in themeKeymap) {
            if (themeKeymap.hasOwnProperty(action)) {
                if (!themeKeymap[action].key) {
                    // No key for this action - default is no keybinding
                    delete mergedKeymap[action];
                } else {
                    mergedKeymap[action] = [{
                        scope: themeKeymap[action].scope || "*",
                        key: themeKeymap[action].key,
                        user: false
                    }];
                    if (mergedKeymap[action][0].scope === "workspace") {
                        mergedKeymap[action][0].scope = "red-ui-workspace";
                    }
                }
            }
        }
        return mergedKeymap;
    }

    function init(done) {
        // Migrate from pre-0.18
        migrateOldKeymap();

        var userKeymap = RED.settings.get('editor.keymap', {});
        $.getJSON("red/keymap.json",function(defaultKeymap) {
            var keymap = mergeKeymaps(defaultKeymap, RED.settings.theme('keymap',{}));
            // keymap has the format:  {action: [{scope,key},{scope,key}], action: [{scope:key}]}

            var action;
            for (action in keymap) {
                if (keymap.hasOwnProperty(action)) {
                    if (!userKeymap.hasOwnProperty(action)) {
                        keymap[action].forEach(function(km) {
                            addHandler(km.scope,km.key,action,false);
                        });
                    }
                    defaultKeyMap[action] = keymap[action][0];
                }
            }

            for (var action in userKeymap) {
                if (userKeymap.hasOwnProperty(action) && userKeymap[action]) {
                    var obj = userKeymap[action];
                    if (obj.hasOwnProperty('key')) {
                        var scope = obj.scope;
                        if (scope === "workspace") {
                            scope = "red-ui-workspace";
                        }
                        addHandler(scope, obj.key, action, true);
                    }
                }
            }
            done();
        });

        RED.userSettings.add({
            id:'keyboard',
            title: RED._("keyboard.keyboard"),
            get: getSettingsPane,
            focus: function() {
                setTimeout(function() {
                    $("#red-ui-settings-tab-keyboard-filter").trigger("focus");
                },200);
            },
            close: function() {
                RED.menu.refreshShortcuts();
            }
        });
    }

    function revertToDefault(action) {
        var currentAction = actionToKeyMap[action];
        if (currentAction) {
            removeHandler(currentAction.key);
        }
        if (defaultKeyMap.hasOwnProperty(action)) {
            var obj = defaultKeyMap[action];
            addHandler(obj.scope, obj.key, action, false);
        }
    }
    function parseKeySpecifier(key) {
        var parts = key.toLowerCase().split("-");
        var modifiers = {};
        var keycode;
        var blank = 0;
        for (var i=0;i<parts.length;i++) {
            switch(parts[i]) {
                case "ctrl":
                case "cmd":
                    modifiers.ctrl = true;
                    modifiers.meta = true;
                    break;
                case "alt":
                    modifiers.alt = true;
                    break;
                case "shift":
                    modifiers.shift = true;
                    break;
                case "":
                    blank++;
                    keycode = keyMap["-"];
                    break;
                default:
                    if (keyMap.hasOwnProperty(parts[i])) {
                        keycode = keyMap[parts[i]];
                    } else if (parts[i].length > 1) {
                        return null;
                    } else {
                        keycode = parts[i].toUpperCase().charCodeAt(0);
                    }
                    break;
            }
        }
        return [keycode,modifiers];
    }

    function matchHandlerToEvent(evt,handler) {
        var target = evt.target;
        var depth = 0;
        while (target.nodeName !== 'BODY' && target.id !== handler.scope) {
            target = target.parentElement;
            depth++;
        }
        if (target.nodeName === 'BODY' && handler.scope !== "*") {
            depth = -1;
        }
        return depth;
    }

    function resolveKeyEvent(evt) {
        var slot = partialState||handlers;
        // We cheat with MacOS CMD key and consider it the same as Ctrl.
        // That means we don't have to have separate keymaps for different OS.
        // It mostly works.
        // One exception is shortcuts that include both Cmd and Ctrl. We don't
        // support them - but we need to make sure we don't block browser-specific
        // shortcuts (such as Cmd-Ctrl-F for fullscreen).
        if (evt.ctrlKey && evt.metaKey) {
            return null; // dont handle both cmd+ctrl - let browser handle this
        }
        if (evt.ctrlKey || evt.metaKey) {
            slot = slot.ctrl;
        }
        if (slot && evt.shiftKey) {
            slot = slot.shift;
        }
        if (slot && evt.altKey) {
            slot = slot.alt;
        }
        var keyCode = firefoxKeyCodeMap[evt.keyCode] || evt.keyCode;
        if (slot && slot[keyCode]) {
            var handler = slot[keyCode];
            if (!handler.handlers) {
                if (partialState) {
                    partialState = null;
                    return resolveKeyEvent(evt);
                }
                if (Object.keys(handler).length > 0) {
                    // check if there's a potential combined handler initiated by this keyCode 
                    for (let h in handler) {
                        if (matchHandlerToEvent(evt,handler[h]) > -1) {
                            partialState = handler;
                            evt.preventDefault();
                            break;
                        }
                    }
                }
                return null;
            } else {
                var depth = Infinity;
                var matchedHandler;
                var i = 0;
                var l = handler.handlers.length;
                for (i=0;i<l;i++) {
                    var d = matchHandlerToEvent(evt,handler.handlers[i]);
                    if (d > -1 && d < depth) {
                        depth = d;
                        matchedHandler = handler.handlers[i];
                    }
                }
                handler = matchedHandler;
            }
            partialState = null;
            return handler;
        } else if (partialState) {
            partialState = null;
            return resolveKeyEvent(evt);
        }
    }
    d3.select(window).on("keydown",function() {
        if (!handlersActive) {
            return;
        }
        if (metaKeyCodes[d3.event.keyCode]) {
            return;
        }
        var handler = resolveKeyEvent(d3.event);
        if (handler && handler.ondown) {
            if (typeof handler.ondown === "string") {
                RED.actions.invoke(handler.ondown);
            } else {
                handler.ondown();
            }
            d3.event.preventDefault();
        }
    });

    function addHandler(scope,key,modifiers,ondown) {
        var mod = modifiers;
        var cbdown = ondown;
        if (typeof modifiers == "function" || typeof modifiers === "string") {
            mod = {};
            cbdown = modifiers;
        }
        var keys = [];
        var i=0;
        if (typeof key === 'string') {
            if (typeof cbdown === 'string') {
                if (!ondown && !defaultKeyMap.hasOwnProperty(cbdown)) {
                    defaultKeyMap[cbdown] = {
                        scope:scope,
                        key:key,
                        user:false
                    };
                }
                if (!ondown) {
                    var userAction = getUserKey(cbdown);
                    if (userAction) {
                        return;
                    }
                }
                actionToKeyMap[cbdown] = {scope:scope,key:key};
                if (typeof ondown === 'boolean') {
                    actionToKeyMap[cbdown].user = ondown;
                }
            }
            var parts = key.split(" ");
            for (i=0;i<parts.length;i++) {
                var parsedKey = parseKeySpecifier(parts[i]);
                if (parsedKey) {
                    keys.push(parsedKey);
                } else {
                    return;
                }
            }
        } else {
            keys.push([key,mod]);
        }
        var slot = handlers;
        for (i=0;i<keys.length;i++) {
            key = keys[i][0];
            mod = keys[i][1];
            if (mod.ctrl) {
                slot.ctrl = slot.ctrl||{};
                slot = slot.ctrl;
            }
            if (mod.shift) {
                slot.shift = slot.shift||{};
                slot = slot.shift;
            }
            if (mod.alt) {
                slot.alt = slot.alt||{};
                slot = slot.alt;
            }
            slot[key] = slot[key] || {};
            slot = slot[key];
            //slot[key] = {scope: scope, ondown:cbdown};
        }
        slot.handlers = slot.handlers || [];
        slot.handlers.push({scope:scope,ondown:cbdown});
        slot.scope = scope;
        slot.ondown = cbdown;
    }

    function removeHandler(key,modifiers) {
        var mod = modifiers || {};
        var keys = [];
        var i=0;
        if (typeof key === 'string') {

            var parts = key.split(" ");
            for (i=0;i<parts.length;i++) {
                var parsedKey = parseKeySpecifier(parts[i]);
                if (parsedKey) {
                    keys.push(parsedKey);
                } else {
                    console.log("Unrecognised key specifier:",key);
                    return;
                }
            }
        } else {
            keys.push([key,mod]);
        }
        var slot = handlers;
        for (i=0;i<keys.length;i++) {
            key = keys[i][0];
            mod = keys[i][1];
            if (mod.ctrl) {
                slot = slot.ctrl;
            }
            if (slot && mod.shift) {
                slot = slot.shift;
            }
            if (slot && mod.alt) {
                slot = slot.alt;
            }
            if (!slot[key]) {
                return;
            }
            slot = slot[key];
        }
        if (typeof slot.ondown === "string") {
            if (typeof modifiers === 'boolean' && modifiers) {
                actionToKeyMap[slot.ondown] = {user: modifiers};
            } else {
                delete actionToKeyMap[slot.ondown];
            }
        }
        delete slot.scope;
        delete slot.ondown;
        // TODO: this wipes everything! Need to have something to identify handler
        delete slot.handlers;
    }

    var cmdCtrlKey = '<span class="help-key">'+(isMac?'&#8984;':'Ctrl')+'</span>';

    function formatKey(key,plain) {
        var formattedKey = isMac?key.replace(/ctrl-?/,"&#8984;"):key;
        formattedKey = isMac?formattedKey.replace(/alt-?/,"&#8997;"):key;
        formattedKey = formattedKey.replace(/shift-?/,"&#8679;");
        formattedKey = formattedKey.replace(/left/,"&#x2190;");
        formattedKey = formattedKey.replace(/up/,"&#x2191;");
        formattedKey = formattedKey.replace(/right/,"&#x2192;");
        formattedKey = formattedKey.replace(/down/,"&#x2193;");
        if (plain) {
            return formattedKey;
        }
        return '<span class="help-key-block"><span class="help-key">'+formattedKey.split(" ").join('</span> <span class="help-key">')+'</span></span>';
    }

    function validateKey(key) {
        key = key.trim();
        var parts = key.split(" ");
        for (i=0;i<parts.length;i++) {
            var parsedKey = parseKeySpecifier(parts[i]);
            if (!parsedKey) {
                return false;
            }
        }
        return true;
    }

    function editShortcut(e) {
        e.preventDefault();
        var container = $(this);
        var object = container.data('data');

        if (!container.hasClass('keyboard-shortcut-entry-expanded')) {
            endEditShortcut();

            var key = container.find(".keyboard-shortcut-entry-key");
            var scope = container.find(".keyboard-shortcut-entry-scope");
            container.addClass('keyboard-shortcut-entry-expanded');

            var keyInput = $('<input type="text">').attr('placeholder',RED._('keyboard.unassigned')).val(object.key||"").appendTo(key);
            keyInput.on("change paste keyup",function(e) {
                if (e.keyCode === 13 && !$(this).hasClass("input-error")) {
                    return endEditShortcut();
                }
                if (e.keyCode === 27) {
                    return endEditShortcut(true);
                }
                var currentVal = $(this).val();
                currentVal = currentVal.trim();
                var valid = (currentVal === "" || RED.keyboard.validateKey(currentVal));
                if (valid && currentVal !== "") {
                    valid = !knownShortcuts.has(scopeSelect.val()+":"+currentVal.toLowerCase());
                }
                $(this).toggleClass("input-error",!valid);
                okButton.attr("disabled",!valid);
            });

            var scopeSelect = $('<select>'+
                '<option value="*" data-i18n="keyboard.global"></option>'+
                '<option value="red-ui-workspace" data-i18n="keyboard.workspace"></option>'+
                '<option value="red-ui-editor-stack" data-i18n="keyboard.editor"></option>'+
                '</select>').appendTo(scope);
            scopeSelect.i18n();
            if (object.scope === "workspace") {
                object.scope = "red-ui-workspace";
            }
            scopeSelect.val(object.scope||'*');
            scopeSelect.on("change", function() {
                keyInput.trigger("change");
            });

            var div = $('<div class="keyboard-shortcut-edit button-group-vertical"></div>').appendTo(scope);
            var okButton = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-check"></i></button>').appendTo(div);
            var revertButton = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-reply"></i></button>').appendTo(div);

            okButton.on("click", function(e) {
                e.stopPropagation();
                endEditShortcut();
            });
            revertButton.on("click", function(e) {
                e.stopPropagation();
                container.empty();
                container.removeClass('keyboard-shortcut-entry-expanded');

                var userKeymap = RED.settings.get('editor.keymap', {});
                userKeymap[object.id] = null;
                RED.settings.set('editor.keymap',userKeymap);

                RED.keyboard.revertToDefault(object.id);

                var shortcut = RED.keyboard.getShortcut(object.id);
                var obj = {
                    id:object.id,
                    scope:shortcut?shortcut.scope:undefined,
                    key:shortcut?shortcut.key:undefined,
                    user:shortcut?shortcut.user:undefined,

                    label: object.label,
                    options: object.options,
                };
                buildShortcutRow(container,obj);
            });

            keyInput.trigger("focus");
        }
    }

    function endEditShortcut(cancel) {
        var container = $('.keyboard-shortcut-entry-expanded');
        if (container.length === 1) {
            var object = container.data('data');
            var keyInput = container.find(".keyboard-shortcut-entry-key input");
            var scopeSelect = container.find(".keyboard-shortcut-entry-scope select");
            if (!cancel) {
                var key = keyInput.val().trim();
                var scope = scopeSelect.val();
                var valid = (key === "" || RED.keyboard.validateKey(key));
                if (valid) {
                    var current = RED.keyboard.getShortcut(object.id);
                    if ((!current && key) || (current && (current.scope !== scope || current.key !== key))) {
                        var keyDiv = container.find(".keyboard-shortcut-entry-key");
                        var scopeDiv = container.find(".keyboard-shortcut-entry-scope");
                        keyDiv.empty();
                        scopeDiv.empty();
                        if (object.key) {
                            knownShortcuts.delete(object.scope+":"+object.key);
                            RED.keyboard.remove(object.key,true);
                        }
                        container.find(".keyboard-shortcut-entry-text i").css("opacity",1);
                        if (key === "") {
                            keyDiv.parent().addClass("keyboard-shortcut-entry-unassigned");
                            keyDiv.append($('<span>').text(RED._('keyboard.unassigned'))  );
                            delete object.key;
                            delete object.scope;
                        } else {
                            keyDiv.parent().removeClass("keyboard-shortcut-entry-unassigned");
                            keyDiv.append(RED.keyboard.formatKey(key));
                            $("<span>").text(scope).appendTo(scopeDiv);
                            object.key = key;
                            object.scope = scope;
                            knownShortcuts.add(object.scope+":"+object.key);
                            RED.keyboard.add(object.scope,object.key,object.id,true);
                        }

                        var userKeymap = RED.settings.get('editor.keymap', {});
                        var shortcut = RED.keyboard.getShortcut(object.id);
                        userKeymap[object.id] = {
                            scope:shortcut.scope,
                            key:shortcut.key
                        };
                        RED.settings.set('editor.keymap',userKeymap);
                    }
                }
            }
            keyInput.remove();
            scopeSelect.remove();
            $('.keyboard-shortcut-edit').remove();
            container.removeClass('keyboard-shortcut-entry-expanded');
        }
    }

    function buildShortcutRow(container,object) {
        var item = $('<div class="keyboard-shortcut-entry">').appendTo(container);
        container.data('data',object);

        var text = object.label;
        var label = $('<div>').addClass("keyboard-shortcut-entry-text").text(text).appendTo(item);

        var user = $('<i class="fa fa-user"></i>').prependTo(label);

        if (!object.user) {
            user.css("opacity",0);
        }

        var key = $('<div class="keyboard-shortcut-entry-key">').appendTo(item);
        if (object.key) {
            key.append(RED.keyboard.formatKey(object.key));
        } else {
            item.addClass("keyboard-shortcut-entry-unassigned");
            key.append($('<span>').text(RED._('keyboard.unassigned'))  );
        }

        var scope = $('<div class="keyboard-shortcut-entry-scope">').appendTo(item);

        $("<span>").text(object.scope === '*'?'global':object.scope||"").appendTo(scope);
        container.on("click", editShortcut);
    }

    function getSettingsPane() {
        var pane = $('<div id="red-ui-settings-tab-keyboard"></div>');

        $('<div class="keyboard-shortcut-entry keyboard-shortcut-list-header">'+
        '<div class="keyboard-shortcut-entry-key keyboard-shortcut-entry-text"><input autocomplete="off" name="keyboard-filter" id="red-ui-settings-tab-keyboard-filter" type="text" data-i18n="[placeholder]keyboard.filterActions"></div>'+
        '<div class="keyboard-shortcut-entry-key" data-i18n="keyboard.shortcut"></div>'+
        '<div class="keyboard-shortcut-entry-scope" data-i18n="keyboard.scope"></div>'+
        '</div>').appendTo(pane);

        pane.find("#red-ui-settings-tab-keyboard-filter").searchBox({
            delay: 100,
            change: function() {
                var filterValue = $(this).val().trim().toLowerCase();
                if (filterValue === "") {
                    shortcutList.editableList('filter', null);
                } else {
                    filterValue = filterValue.replace(/\s/g,"");
                    shortcutList.editableList('filter', function(data) {
                        var label = data.label.toLowerCase();
                        return label.indexOf(filterValue) > -1;
                    });
                }
            }
        });

        var shortcutList = $('<ol class="keyboard-shortcut-list"></ol>').css({
            position: "absolute",
            top: "32px",
            bottom: "0",
            left: "0",
            right: "0"
        }).appendTo(pane).editableList({
            addButton: false,
            scrollOnAdd: false,
            addItem: function(container,i,object) {
                buildShortcutRow(container,object);
            },

        });
        var shortcuts = RED.actions.list();
        shortcuts.sort(function(A,B) {
            var Akey = A.label;
            var Bkey = B.label;
            return Akey.localeCompare(Bkey);
        });
        knownShortcuts = new Set();
        shortcuts.forEach(function(s) {
            if (s.key) {
                knownShortcuts.add(s.scope+":"+s.key);
            }
            shortcutList.editableList('addItem',s);
        });
        return pane;
    }

    function enable() {
        handlersActive = true;
    }
    function disable() {
        handlersActive = false;
    }

    return {
        init: init,
        add: addHandler,
        remove: removeHandler,
        getShortcut: function(actionName) {
            return actionToKeyMap[actionName];
        },
        getUserShortcut: getUserKey,
        revertToDefault: revertToDefault,
        formatKey: formatKey,
        validateKey: validateKey,
        disable: disable,
        enable: enable
    }

})();
;RED.envVar = (function() {
    function saveEnvList(list) {
        const items = list.editableList("items")
        const new_env = [];
        items.each(function (i,el) {
            var data = el.data('data');
            var item;
            if (data.nameField && data.valueField) {
                item = {
                    name: data.nameField.val(),
                    value: data.valueField.typedInput("value"),
                    type: data.valueField.typedInput("type")
                };
                new_env.push(item);
            }
        });
        return new_env;
    }

    function getGlobalConf(create) {
        var gconf = null;
        RED.nodes.eachConfig(function (conf) {
            if (conf.type === "global-config") {
                gconf = conf;
            }
        });
        if ((gconf === null) && create) {
            var cred = {
                _ : {},
                map: {}
            };
            gconf = {
                id: RED.nodes.id(),
                type: "global-config",
                env: [],
                name: "global-config",
                label: "",
                hasUsers: false,
                users: [],
                credentials: cred,
                _def: RED.nodes.getType("global-config"),
            };
            RED.nodes.add(gconf);
        }
        return gconf;
    }

    function applyChanges(list) {
        var gconf = getGlobalConf(false);
        var new_env = [];
        var items = list.editableList('items');
        var credentials = gconf ? gconf.credentials : null;
        if (!gconf && list.editableList('length') === 0) {
            // No existing global-config node and nothing in the list,
            // so no need to do anything more
            return
        }
        if (!credentials) {
            credentials = {
                _ : {},
                map: {}
            };
        }
        items.each(function (i,el) {
            var data = el.data('data');
            if (data.nameField && data.valueField) {
                var item = {
                    name: data.nameField.val(),
                    value: data.valueField.typedInput("value"),
                    type: data.valueField.typedInput("type")
                };
                if (item.name.trim() !== "") {
                    new_env.push(item);
                    if (item.type === "cred") {
                        credentials.map[item.name] = item.value;
                        credentials.map["has_"+item.name] = (item.value !== "");
                        item.value = "__PWRD__";
                    }
                }
            }
        });
        if (gconf === null) {
            gconf = getGlobalConf(true);
        }
        if (!gconf.credentials) {
            gconf.credentials = {
                _ : {},
                map: {}
            };
        }
        if ((JSON.stringify(new_env) !== JSON.stringify(gconf.env)) ||
            (JSON.stringify(credentials) !== JSON.stringify(gconf.credentials))) {
            gconf.env = new_env;
            gconf.credentials = credentials;
            RED.nodes.dirty(true);
        }
    }

    function getSettingsPane() {
        var gconf = getGlobalConf(false);
        var env = gconf ? gconf.env : [];
        var cred = gconf ? gconf.credentials : null;
        if (!cred) {
            cred = {
                _ : {},
                map: {}
            };
        }

        var pane = $("<div/>", {
            id: "red-ui-settings-tab-envvar",
            class: "form-horizontal"
        });
        var content = $("<div/>", {
            class: "form-row node-input-env-container-row"
        }).css({
            "margin": "10px"
        }).appendTo(pane);

        var label = $("<label></label>").css({
            width: "100%"
        }).appendTo(content);
        $("<i/>", {
            class: "fa fa-list"
        }).appendTo(label);
        $("<span/>").text(" "+RED._("env-var.header")).appendTo(label);

        var list = $("<ol/>", {
            id: "node-input-env-container"
        }).appendTo(content);
        var node = {
            type: "",
            env: env,
            credentials: cred.map,
        };
        RED.editor.envVarList.create(list, node);

        var buttons = $("<div/>").css({
            "text-align": "right",
        }).appendTo(content);
        var revertButton = $("<button/>", {
            class: "red-ui-button"
        }).css({
        }).text(RED._("env-var.revert")).appendTo(buttons);

        var items = saveEnvList(list);
        revertButton.on("click", function (ev) {
            list.editableList("empty");
            list.editableList("addItems", items);
        });

        return pane;
    }

    function init(done) {
        RED.userSettings.add({
            id:'envvar',
            title: RED._("env-var.environment"),
            get: getSettingsPane,
            focus: function() {
                var height = $("#red-ui-settings-tab-envvar").parent().height();
                $("#node-input-env-container").editableList("height", (height -100));
            },
            close: function() {
                var list = $("#node-input-env-container");
                try {
                    applyChanges(list);
                }
                catch (e) {
                    console.log(e);
                    console.log(e.stack);
                }
            }
        });

        RED.actions.add("core:show-global-env", function() {
            RED.userSettings.show('envvar');
        });
    }

    return {
        init: init,
    };

})();
;/**
 * 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.
 **/


RED.workspaces = (function() {

    const documentTitle = document.title;

    var activeWorkspace = 0;
    var workspaceIndex = 0;

    var viewStack = [];
    var hideStack = [];
    var viewStackPos = 0;

    let flashingTab;
    let flashingTabTimer;

    function addToViewStack(id) {
        if (viewStackPos !== viewStack.length) {
            viewStack.splice(viewStackPos);
        }
        viewStack.push(id);
        viewStackPos = viewStack.length;
    }

    function removeFromHideStack(id) {
        hideStack = hideStack.filter(function(v) {
            if (v === id) {
                return false;
            } else if (Array.isArray(v)) {
                var i = v.indexOf(id);
                if (i > -1) {
                    v.splice(i,1);
                }
                if (v.length === 0) {
                    return false;
                }
                return true
            }
            return true;
        })
    }

    function addWorkspace(ws,skipHistoryEntry,targetIndex) {
        if (ws) {
            if (!ws.closeable) {
                ws.hideable = true;
            }
            if (!ws.hasOwnProperty('locked')) {
                ws.locked = false
            }
            workspace_tabs.addTab(ws,targetIndex);

            var hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}");
            if (hiddenTabs[ws.id]) {
                workspace_tabs.hideTab(ws.id);
            }
            workspace_tabs.resize();
        } else {
            var tabId = RED.nodes.id();
            do {
                workspaceIndex += 1;
            } while ($("#red-ui-workspace-tabs li[flowname='"+RED._('workspace.defaultName',{number:workspaceIndex})+"']").size() !== 0);

            ws = {
                type: "tab",
                id: tabId,
                disabled: false,
                locked: false,
                info: "",
                label: RED._('workspace.defaultName',{number:workspaceIndex}),
                env: [],
                hideable: true,
            };
            if (!skipHistoryEntry) {
                ws.added = true
            }
            RED.nodes.addWorkspace(ws,targetIndex);
            workspace_tabs.addTab(ws,targetIndex);

            workspace_tabs.activateTab(tabId);
            if (!skipHistoryEntry) {
                RED.history.push({t:'add',workspaces:[ws],dirty:RED.nodes.dirty()});
                RED.nodes.dirty(true);
            }
        }
        $("#red-ui-tab-"+(ws.id.replace(".","-"))).attr("flowname",ws.label).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added));
        RED.view.focus();
        return ws;
    }

    function deleteWorkspace(ws) {
        if (workspaceTabCount === 1) {
            return;
        }
        if (ws.locked) {
            return
        }
        var workspaceOrder = RED.nodes.getWorkspaceOrder();
        ws._index = workspaceOrder.indexOf(ws.id);
        removeWorkspace(ws);
        var historyEvent = RED.nodes.removeWorkspace(ws.id);
        historyEvent.t = 'delete';
        historyEvent.dirty = RED.nodes.dirty();
        historyEvent.workspaces = [ws];
        RED.history.push(historyEvent);
        RED.nodes.dirty(true);
        RED.sidebar.config.refresh();
    }

    function showEditWorkspaceDialog(id) {
        var workspace = RED.nodes.workspace(id);
        if (!workspace) {
            var subflow = RED.nodes.subflow(id);
            if (subflow) {
                RED.editor.editSubflow(subflow);
            }
        } else {
            if (!workspace.locked) {
                RED.editor.editFlow(workspace);
            }
        }
    }


    var workspace_tabs;
    var workspaceTabCount = 0;

    function getMenuItems(isMenuButton, tab) {
        let hiddenFlows = new Set()
        for (let i = 0; i < hideStack.length; i++) {
            let ids = hideStack[i]
            if (!Array.isArray(ids)) {
                ids = [ids]
            }
            ids.forEach(id => {
                if (RED.nodes.workspace(id)) {
                    hiddenFlows.add(id)
                }
            })
        }
        const hiddenflowCount = hiddenFlows.size;
        let activeWorkspace = tab || RED.nodes.workspace(RED.workspaces.active()) || RED.nodes.subflow(RED.workspaces.active())
        let isFlowDisabled = activeWorkspace ? activeWorkspace.disabled : false
        const currentTabs = workspace_tabs.listTabs();
        let flowCount = 0;
        currentTabs.forEach(tab => {
            if (RED.nodes.workspace(tab)) {
                flowCount++;
            }
        });

        let isCurrentLocked = RED.workspaces.isLocked()
        if (tab) {
            isCurrentLocked = tab.locked
        }

        var menuItems = []
        if (isMenuButton) {
            menuItems.push({
                id:"red-ui-tabs-menu-option-search-flows",
                label: RED._("workspace.listFlows"),
                onselect: "core:list-flows"
            },
            {
                id:"red-ui-tabs-menu-option-search-subflows",
                label: RED._("workspace.listSubflows"),
                onselect: "core:list-subflows"
            },
            null)
        }
        if (RED.settings.theme("menu.menu-item-workspace-add", true)) {
            menuItems.push(
                {
                    id:"red-ui-tabs-menu-option-add-flow",
                    label: RED._("workspace.addFlow"),
                    onselect: "core:add-flow"
                }
            )
        }
        if (isMenuButton || !!tab) {
            if (RED.settings.theme("menu.menu-item-workspace-add", true)) {
                menuItems.push(
                    {
                        id:"red-ui-tabs-menu-option-add-flow-right",
                        label: RED._("workspace.addFlowToRight"),
                        shortcut: RED.keyboard.getShortcut("core:add-flow-to-right"),
                        onselect: function() {
                            RED.actions.invoke("core:add-flow-to-right", tab)
                        }
                    },
                    null
                )
            }
            if (activeWorkspace && activeWorkspace.type === 'tab') {
                menuItems.push(
                    isFlowDisabled ? {
                        label: RED._("workspace.enableFlow"),
                        shortcut: RED.keyboard.getShortcut("core:enable-flow"),
                        onselect: function() {
                            RED.actions.invoke("core:enable-flow", tab?tab.id:undefined)
                        },
                        disabled: isCurrentLocked
                    } : {
                        label: RED._("workspace.disableFlow"),
                        shortcut: RED.keyboard.getShortcut("core:disable-flow"),
                        onselect: function() {
                            RED.actions.invoke("core:disable-flow", tab?tab.id:undefined)
                        },
                        disabled: isCurrentLocked
                    },
                    isCurrentLocked? {
                        label: RED._("workspace.unlockFlow"),
                        shortcut: RED.keyboard.getShortcut("core:unlock-flow"),
                        onselect: function() {
                            RED.actions.invoke('core:unlock-flow', tab?tab.id:undefined)
                        }
                    } : {
                        label: RED._("workspace.lockFlow"),
                        shortcut: RED.keyboard.getShortcut("core:lock-flow"),
                        onselect: function() {
                            RED.actions.invoke('core:lock-flow', tab?tab.id:undefined)
                        }
                    },
                    null
                )
            }
            const activeIndex = currentTabs.findIndex(id => (activeWorkspace && (id === activeWorkspace.id)));
            menuItems.push(
                {
                    label: RED._("workspace.moveToStart"),
                    shortcut: RED.keyboard.getShortcut("core:move-flow-to-start"),
                    onselect: function() {
                        RED.actions.invoke("core:move-flow-to-start", tab?tab.id:undefined)
                    },
                    disabled: activeIndex === 0
                },
                {
                    label: RED._("workspace.moveToEnd"),
                    shortcut: RED.keyboard.getShortcut("core:move-flow-to-end"),
                    onselect: function() {
                        RED.actions.invoke("core:move-flow-to-end", tab?tab.id:undefined)
                    },
                    disabled: activeIndex === currentTabs.length - 1
                }
            )
        }
        if (menuItems.length > 0) {
            menuItems.push(null)
        }
        if (isMenuButton || !!tab) {
            menuItems.push(
                {
                    id:"red-ui-tabs-menu-option-add-hide-flows",
                    label: RED._("workspace.hideFlow"),
                    shortcut: RED.keyboard.getShortcut("core:hide-flow"),
                    onselect: function() {
                        RED.actions.invoke("core:hide-flow", tab)
                    }
                },
                {
                    id:"red-ui-tabs-menu-option-add-hide-other-flows",
                    label: RED._("workspace.hideOtherFlows"),
                    shortcut: RED.keyboard.getShortcut("core:hide-other-flows"),
                    onselect: function() {
                        RED.actions.invoke("core:hide-other-flows", tab)
                    }
                }
            )

        }
        
        menuItems.push(
            {
                id:"red-ui-tabs-menu-option-add-hide-all-flows",
                label: RED._("workspace.hideAllFlows"),
                onselect: "core:hide-all-flows",
                disabled: (hiddenflowCount === flowCount)
            },
            {
                id:"red-ui-tabs-menu-option-add-show-all-flows",
                disabled: hiddenflowCount === 0,
                label: RED._("workspace.showAllFlows", { count: hiddenflowCount }),
                onselect: "core:show-all-flows"
            },
            {
                id:"red-ui-tabs-menu-option-add-show-last-flow",
                disabled: hideStack.length === 0,
                label: RED._("workspace.showLastHiddenFlow"),
                onselect: "core:show-last-hidden-flow"
            }
        )
        if (tab) {
            menuItems.push(null)

            if (RED.settings.theme("menu.menu-item-workspace-delete", true)) {
                menuItems.push(
                    {
                        label: RED._("common.label.delete"),
                        onselect: function() {
                            if (tab.type === 'tab') {
                                RED.workspaces.delete(tab)
                            } else if (tab.type === 'subflow') {
                                RED.subflow.delete(tab.id)
                            }
                        },
                        disabled: isCurrentLocked || (workspaceTabCount === 1)
                    }
                )
            }
            menuItems.push(
                {
                    label: RED._("menu.label.export"),
                    shortcut: RED.keyboard.getShortcut("core:show-export-dialog"),
                    onselect: function() {
                        RED.workspaces.show(tab.id)
                        RED.actions.invoke('core:show-export-dialog', null, 'flow')
                    }
                }
            )
        }
        // if (isMenuButton && hiddenflowCount > 0) {
        //     menuItems.unshift({
        //         label: RED._("workspace.hiddenFlows",{count: hiddenflowCount}),
        //         onselect: "core:list-hidden-flows"
        //     })
        // }
        return menuItems;
    }
    function createWorkspaceTabs() {
        workspace_tabs = RED.tabs.create({
            id: "red-ui-workspace-tabs",
            onchange: function(tab) {
                var event = {
                    old: activeWorkspace
                }
                if (tab) {
                    $("#red-ui-workspace-chart").show();
                    activeWorkspace = tab.id;
                    window.location.hash = 'flow/'+tab.id;
                    if (tab.label) {
                        document.title = `${documentTitle} : ${tab.label}`
                    } else {
                        document.title = documentTitle
                    }
                    $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled", !!tab.disabled);
                    $("#red-ui-workspace").toggleClass("red-ui-workspace-locked", !!tab.locked);
                } else {
                    $("#red-ui-workspace-chart").hide();
                    activeWorkspace = 0;
                    window.location.hash = '';
                    document.title = documentTitle
                }
                event.workspace = activeWorkspace;
                RED.events.emit("workspace:change",event);
                RED.sidebar.config.refresh();
                RED.view.focus();
            },
            onclick: function(tab, evt) {
                if(evt.which === 2) {
                    evt.preventDefault();
                    evt.stopPropagation();
                    RED.actions.invoke("core:hide-flow", tab)
                } else {
                    if (tab.id !== activeWorkspace) {
                        addToViewStack(activeWorkspace);
                    }
                    RED.view.focus();
                }
            },
            ondblclick: function(tab) {
                if (tab.type != "subflow") {
                    showEditWorkspaceDialog(tab.id);
                } else {
                    RED.editor.editSubflow(RED.nodes.subflow(tab.id));
                }
            },
            onadd: function(tab) {
                if (tab.type === "tab") {
                    workspaceTabCount++;
                }
                $('<span class="red-ui-workspace-disabled-icon"><i class="fa fa-ban"></i> </span>').prependTo("#red-ui-tab-"+(tab.id.replace(".","-"))+" .red-ui-tab-label");
                if (tab.disabled) {
                    $("#red-ui-tab-"+(tab.id.replace(".","-"))).addClass('red-ui-workspace-disabled');
                }
                $('<span class="red-ui-workspace-locked-icon"><i class="fa fa-lock"></i> </span>').prependTo("#red-ui-tab-"+(tab.id.replace(".","-"))+" .red-ui-tab-label");
                if (tab.locked) {
                    $("#red-ui-tab-"+(tab.id.replace(".","-"))).addClass('red-ui-workspace-locked');
                }

                const changeBadgeContainer = $('<svg class="red-ui-flow-tab-changed red-ui-flow-node-changed" width="10" height="10" viewBox="-1 -1 12 12"></svg>').appendTo("#red-ui-tab-"+(tab.id.replace(".","-")))
                const changeBadge = document.createElementNS("http://www.w3.org/2000/svg","circle");
                changeBadge.setAttribute("cx",5);
                changeBadge.setAttribute("cy",5);
                changeBadge.setAttribute("r",5);
                changeBadgeContainer.append(changeBadge)

                RED.menu.setDisabled("menu-item-workspace-delete",activeWorkspace === 0 || workspaceTabCount <= 1);
                if (workspaceTabCount === 1) {
                    showWorkspace();
                }
            },
            onremove: function(tab) {
                if (tab.type === "tab") {
                    workspaceTabCount--;
                } else {
                    RED.events.emit("workspace:close",{workspace: tab.id})
                    hideStack.push(tab.id);
                }
                RED.menu.setDisabled("menu-item-workspace-delete",activeWorkspace === 0 || workspaceTabCount <= 1);
                if (workspaceTabCount === 0) {
                    hideWorkspace();
                }
            },
            onreorder: function(oldOrder, newOrder) {
                RED.history.push({
                    t:'reorder',
                    workspaces: {
                        from: oldOrder,
                        to: newOrder
                    },
                    dirty:RED.nodes.dirty()
                });
                // Only mark flows dirty if flow-order has changed (excluding subflows)
                const filteredOldOrder = oldOrder.filter(id => !!RED.nodes.workspace(id))
                const filteredNewOrder = newOrder.filter(id => !!RED.nodes.workspace(id))

                if (JSON.stringify(filteredOldOrder) !== JSON.stringify(filteredNewOrder)) {
                    RED.nodes.dirty(true);
                    setWorkspaceOrder(newOrder);
                }
            },
            onselect: function(selectedTabs) {
                RED.view.select(false)
                if (selectedTabs.length === 0) {
                    $("#red-ui-workspace-chart svg").css({"pointer-events":"auto",filter:"none"})
                    $("#red-ui-workspace-toolbar").css({"pointer-events":"auto",filter:"none"})
                    $("#red-ui-palette-container").css({"pointer-events":"auto",filter:"none"})
                    $(".red-ui-sidebar-shade").hide();
                } else {
                    RED.view.select(false)
                    $("#red-ui-workspace-chart svg").css({"pointer-events":"none",filter:"opacity(60%)"})
                    $("#red-ui-workspace-toolbar").css({"pointer-events":"none",filter:"opacity(60%)"})
                    $("#red-ui-palette-container").css({"pointer-events":"none",filter:"opacity(60%)"})
                    $(".red-ui-sidebar-shade").show();
                }
            },
            onhide: function(tab) {
                hideStack.push(tab.id);
                if (tab.type === "tab") {
                    var hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}");
                    hiddenTabs[tab.id] = true;
                    RED.settings.setLocal("hiddenTabs",JSON.stringify(hiddenTabs));
                    RED.events.emit("workspace:hide",{workspace: tab.id})
                }
            },
            onshow: function(tab) {
                removeFromHideStack(tab.id);

                var hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}");
                delete hiddenTabs[tab.id];
                RED.settings.setLocal("hiddenTabs",JSON.stringify(hiddenTabs));

                RED.events.emit("workspace:show",{workspace: tab.id})
            },
            minimumActiveTabWidth: 150,
            scrollable: true,
            addButton: RED.settings.theme("menu.menu-item-workspace-add", true) ? "core:add-flow" : undefined,
            addButtonCaption: RED._("workspace.addFlow"),
            menu: function() { return getMenuItems(true) },
            contextmenu: function(tab) { return getMenuItems(false, tab) }
        });
        workspaceTabCount = 0;
    }
    function showWorkspace() {
        $("#red-ui-workspace .red-ui-tabs").show()
        $("#red-ui-workspace-chart").show()
        $("#red-ui-workspace-footer").children().show()
    }
    function hideWorkspace() {
        $("#red-ui-workspace .red-ui-tabs").hide()
        $("#red-ui-workspace-chart").hide()
        $("#red-ui-workspace-footer").children().hide()
    }

    function init() {
        $('<ul id="red-ui-workspace-tabs"></ul>').appendTo("#red-ui-workspace");
        $('<div id="red-ui-workspace-tabs-shade" class="hide"></div>').appendTo("#red-ui-workspace");
        $('<div id="red-ui-workspace-chart" tabindex="1"></div>').appendTo("#red-ui-workspace");
        $('<div id="red-ui-workspace-toolbar"></div>').appendTo("#red-ui-workspace");
        $('<div id="red-ui-workspace-footer" class="red-ui-component-footer"></div>').appendTo("#red-ui-workspace");
        $('<div id="red-ui-editor-shade" class="hide"></div>').appendTo("#red-ui-workspace");


        createWorkspaceTabs();
        RED.events.on("sidebar:resize",workspace_tabs.resize);

        RED.events.on("workspace:clear", () => {
            // Reset the index used to generate new flow names
            workspaceIndex = 0
        })

        RED.actions.add("core:show-next-tab",function() {
            var oldActive = activeWorkspace;
            workspace_tabs.nextTab();
            if (oldActive !== activeWorkspace) {
                addToViewStack(oldActive)
            }
        });
        RED.actions.add("core:show-previous-tab",function() {
            var oldActive = activeWorkspace;
            workspace_tabs.previousTab();
            if (oldActive !== activeWorkspace) {
                addToViewStack(oldActive)
            }
        });

        RED.menu.setAction('menu-item-workspace-delete',function() {
            deleteWorkspace(RED.nodes.workspace(activeWorkspace));
        });

        $(window).on("resize", function() {
            workspace_tabs.resize();
        });
        if (RED.settings.theme("menu.menu-item-workspace-add", true)) {
            RED.actions.add("core:add-flow",function(opts) { addWorkspace(undefined,undefined,opts?opts.index:undefined)});
            RED.actions.add("core:add-flow-to-right",function(workspace) {
                let index
                if (workspace) {
                    index = workspace_tabs.getTabIndex(workspace.id)+1
                } else {
                    index = workspace_tabs.activeIndex()+1
                }
                addWorkspace(undefined,undefined,index)
            });
        }
        if (RED.settings.theme("menu.menu-item-workspace-edit", true)) {
            RED.actions.add("core:edit-flow",editWorkspace);
        }
        if (RED.settings.theme("menu.menu-item-workspace-delete", true)) {
            RED.actions.add("core:remove-flow",removeWorkspace);
        }
        RED.actions.add("core:enable-flow",enableWorkspace);
        RED.actions.add("core:disable-flow",disableWorkspace);
        RED.actions.add("core:lock-flow",lockWorkspace);
        RED.actions.add("core:unlock-flow",unlockWorkspace);
        RED.actions.add("core:move-flow-to-start", function(id) { moveWorkspace(id, 'start') });
        RED.actions.add("core:move-flow-to-end", function(id) { moveWorkspace(id, 'end') });

        RED.actions.add("core:hide-flow", function(workspace) {
            let selection
            if (workspace) {
                selection = [workspace]
            } else {
                selection = workspace_tabs.selection();
                if (selection.length === 0) {
                    selection = [{id:activeWorkspace}]
                }
            }
            var hiddenTabs = [];
            selection.forEach(function(ws) {
                RED.workspaces.hide(ws.id);
                hideStack.pop();
                hiddenTabs.push(ws.id);
            })
            if (hiddenTabs.length > 0) {
                hideStack.push(hiddenTabs);
            }
            workspace_tabs.clearSelection();
        })

        RED.actions.add("core:hide-other-flows", function(workspace) {
            let selection
            if (workspace) {
                selection = [workspace]
            } else {
                selection = workspace_tabs.selection();
                if (selection.length === 0) {
                    selection = [{id:activeWorkspace}]
                }
            }
            var selected = new Set(selection.map(function(ws) { return ws.id }))

            var currentTabs = workspace_tabs.listTabs();
            var hiddenTabs = [];
            currentTabs.forEach(function(id) {
                if (!selected.has(id)) {
                    RED.workspaces.hide(id);
                    hideStack.pop();
                    hiddenTabs.push(id);
                }
            })
            if (hiddenTabs.length > 0) {
                hideStack.push(hiddenTabs);
            }
        })

        RED.actions.add("core:hide-all-flows", function() {
            var currentTabs = workspace_tabs.listTabs();
            currentTabs.forEach(function(id) {
                RED.workspaces.hide(id);
                hideStack.pop();
            })
            if (currentTabs.length > 0) {
                hideStack.push(currentTabs);
            }
            workspace_tabs.clearSelection();
        })
        RED.actions.add("core:show-all-flows", function() {
            var currentTabs = workspace_tabs.listTabs();
            currentTabs.forEach(function(id) {
                RED.workspaces.show(id, null, true)
            })
        })
        // RED.actions.add("core:toggle-flows", function() {
        //     var currentTabs = workspace_tabs.listTabs();
        //     var visibleCount = workspace_tabs.count();
        //     currentTabs.forEach(function(id) {
        //         if (visibleCount === 0) {
        //             RED.workspaces.show(id)
        //         } else {
        //             RED.workspaces.hide(id)
        //         }
        //     })
        // })
        RED.actions.add("core:show-last-hidden-flow", function() {
            var id = hideStack.pop();
            if (id) {
                if (typeof id === 'string') {
                    RED.workspaces.show(id);
                } else {
                    var last = id.pop();
                    id.forEach(function(i) {
                        RED.workspaces.show(i, null, true);
                    })
                    setTimeout(function() {
                        RED.workspaces.show(last);
                    },150)

                }
            }
        })
        RED.actions.add("core:list-modified-nodes",function() {
            RED.actions.invoke("core:search","is:modified ");
        })
        RED.actions.add("core:list-hidden-flows",function() {
            RED.actions.invoke("core:search","is:hidden ");
        })
        RED.actions.add("core:list-flows",function() {
            RED.actions.invoke("core:search","type:tab ");
        })
        RED.actions.add("core:list-subflows",function() {
            RED.actions.invoke("core:search","type:subflow ");
        })
        RED.actions.add("core:go-to-previous-location", function() {
            if (viewStackPos > 0) {
                if (viewStackPos === viewStack.length) {
                    // We're at the end of the stack. Remember the activeWorkspace
                    // so we can come back to it.
                    viewStack.push(activeWorkspace);
                }
                RED.workspaces.show(viewStack[--viewStackPos],true);
            }
        })
        RED.actions.add("core:go-to-next-location", function() {
            if (viewStackPos < viewStack.length - 1) {
                RED.workspaces.show(viewStack[++viewStackPos],true);
            }
        })

        RED.events.on("flows:change", (ws) => {
            $("#red-ui-tab-"+(ws.id.replace(".","-"))).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added));
        })
        RED.events.on("subflows:change", (ws) => {
            $("#red-ui-tab-"+(ws.id.replace(".","-"))).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added));
        })

        hideWorkspace();
    }

    function editWorkspace(id) {
        showEditWorkspaceDialog(id||activeWorkspace);
    }

    function enableWorkspace(id) {
        setWorkspaceState(id,false);
    }
    function disableWorkspace(id) {
        setWorkspaceState(id,true);
    }
    function setWorkspaceState(id,disabled) {
        var workspace = RED.nodes.workspace(id||activeWorkspace);
        if (!workspace || workspace.locked) {
            return;
        }
        if (workspace.disabled !== disabled) {
            var changes = { disabled: workspace.disabled };
            workspace.disabled = disabled;
            $("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!workspace.disabled);
            if (!id || (id === activeWorkspace)) {
                $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!workspace.disabled);
            }
            var historyEvent = {
                t: "edit",
                changes:changes,
                node: workspace,
                dirty: RED.nodes.dirty()
            }
            workspace.changed = true;
            RED.history.push(historyEvent);
            RED.events.emit("flows:change",workspace);
            RED.nodes.dirty(true);
            RED.sidebar.config.refresh();
            var selection = RED.view.selection();
            if (!selection.nodes && !selection.links && workspace.id === activeWorkspace) {
                RED.sidebar.info.refresh(workspace);
            }
            if (changes.hasOwnProperty('disabled')) {
                RED.nodes.eachNode(function(n) {
                    if (n.z === workspace.id) {
                        n.dirty = true;
                    }
                });
                RED.view.redraw();
            }
        }
    }
    function lockWorkspace(id) {
        setWorkspaceLockState(id,true);
    }
    function unlockWorkspace(id) {
        setWorkspaceLockState(id,false);
    }
    function setWorkspaceLockState(id,locked) {
        var workspace = RED.nodes.workspace(id||activeWorkspace);
        if (!workspace) {
            return;
        }
        if (workspace.locked !== locked) {
            var changes = { locked: workspace.locked };
            workspace.locked = locked;
            $("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-locked',!!workspace.locked);
            if (!id || (id === activeWorkspace)) {
                $("#red-ui-workspace").toggleClass("red-ui-workspace-locked",!!workspace.locked);
            }
            var historyEvent = {
                t: "edit",
                changes:changes,
                node: workspace,
                dirty: RED.nodes.dirty()
            }
            workspace.changed = true;
            RED.history.push(historyEvent);
            RED.events.emit("flows:change",workspace);
            RED.nodes.dirty(true);
            RED.nodes.filterNodes({z:workspace.id}).forEach(n => n.dirty = true)
            RED.view.redraw(true);
        }
    }

    function removeWorkspace(ws) {
        if (!ws) {
            ws = RED.nodes.workspace(activeWorkspace)
            if (ws && !ws.locked) {
                deleteWorkspace(RED.nodes.workspace(activeWorkspace));
            }
        } else {
            if (ws.locked) { return }
            if (workspace_tabs.contains(ws.id)) {
                workspace_tabs.removeTab(ws.id);
            }
            if (ws.id === activeWorkspace) {
                activeWorkspace = 0;
            }
        }
    }

    function moveWorkspace(id, direction) {
        const workspace = RED.nodes.workspace(id||activeWorkspace) || RED.nodes.subflow(id||activeWorkspace);
        if (!workspace) {
            return;
        }
        const currentOrder = workspace_tabs.listTabs()
        const oldOrder = [...currentOrder]
        const currentIndex = currentOrder.findIndex(id => id === workspace.id)
        currentOrder.splice(currentIndex, 1)
        if (direction === 'start') {
            currentOrder.unshift(workspace.id)
        } else if (direction === 'end') {
            currentOrder.push(workspace.id)
        }
        const newOrder = setWorkspaceOrder(currentOrder)
        if (JSON.stringify(newOrder) !== JSON.stringify(oldOrder)) {
            RED.history.push({
                t:'reorder',
                workspaces: {
                    from:oldOrder,
                    to:newOrder
                },
                dirty:RED.nodes.dirty()
            });
            const filteredOldOrder = oldOrder.filter(id => !!RED.nodes.workspace(id))
            const filteredNewOrder = newOrder.filter(id => !!RED.nodes.workspace(id))
            if (JSON.stringify(filteredOldOrder) !== JSON.stringify(filteredNewOrder)) {
                RED.nodes.dirty(true);
            }
        }
    }
    function setWorkspaceOrder(order) {
        var newOrder = order.filter(id => !!RED.nodes.workspace(id))
        var currentOrder = RED.nodes.getWorkspaceOrder();
        if (JSON.stringify(newOrder) !== JSON.stringify(currentOrder)) {
            RED.nodes.setWorkspaceOrder(newOrder);
            RED.events.emit("flows:reorder",newOrder);
        }
        workspace_tabs.order(order);
        return newOrder
    }

    function flashTab(tabId) {
        if(flashingTab && flashingTab.length) {
            //cancel current flashing node before flashing new node
            clearInterval(flashingTabTimer);
            flashingTabTimer = null;
            flashingTab.removeClass('highlighted');
            flashingTab = null;
        }
        let tab = $("#red-ui-tab-" + tabId);
        if(!tab || !tab.length) { return; }

        flashingTabTimer = setInterval(function(flashEndTime) {
            if (flashEndTime >= Date.now()) {
                const highlighted = tab.hasClass("highlighted");
                tab.toggleClass('highlighted', !highlighted)
            } else {
                clearInterval(flashingTabTimer);
                flashingTabTimer = null;
                flashingTab = null;
                tab.removeClass('highlighted');
            }
        }, 100, Date.now() + 2200);
        flashingTab = tab;
        tab.addClass('highlighted');
    }
    return {
        init: init,
        add: addWorkspace,
        // remove: remove workspace without editor history etc
        remove: removeWorkspace,
        // delete: remove workspace and update editor history
        delete: deleteWorkspace,
        order: setWorkspaceOrder,
        edit: editWorkspace,
        contains: function(id) {
            return workspace_tabs.contains(id);
        },
        count: function() {
            return workspaceTabCount;
        },
        active: function() {
            return activeWorkspace
        },
        isLocked: function(id) {
            id = id || activeWorkspace
            var ws = RED.nodes.workspace(id) || RED.nodes.subflow(id)
            return ws && ws.locked
        },
        selection: function() {
            return workspace_tabs.selection();
        },
        hide: function(id) {
            if (!id) {
                id = activeWorkspace;
            }
            if (workspace_tabs.contains(id)) {
                workspace_tabs.hideTab(id);
            }
        },
        isHidden: function(id) {
            return hideStack.includes(id)
        },
        show: function(id,skipStack,unhideOnly,flash) {
            if (!workspace_tabs.contains(id)) {
                var sf = RED.nodes.subflow(id);
                if (sf) {
                    addWorkspace(
                        {type:"subflow",id:id,icon:"red/images/subflow_tab.svg",label:sf.name, closeable: true},
                        null,
                        workspace_tabs.activeIndex()+1
                    );
                    removeFromHideStack(id);
                } else {
                    return;
                }
            }
            if (unhideOnly) {
                workspace_tabs.showTab(id);
            } else {
                if (!skipStack && activeWorkspace !== id) {
                    addToViewStack(activeWorkspace)
                }
                workspace_tabs.activateTab(id);
            }
            if(flash) {
                flashTab(id.replace(".","-"))
            }
        },
        refresh: function() {
            var workspace = RED.nodes.workspace(RED.workspaces.active());
            if (workspace) {
                document.title = `${documentTitle} : ${workspace.label}`;
            } else {
                var subflow = RED.nodes.subflow(RED.workspaces.active());
                if (subflow) {
                    document.title = `${documentTitle} : ${subflow.name}`;
                } else {
                    document.title = documentTitle
                }
            }
            RED.nodes.eachWorkspace(function(ws) {
                workspace_tabs.renameTab(ws.id,ws.label);
                $("#red-ui-tab-"+(ws.id.replace(".","-"))).attr("flowname",ws.label)
            })
            RED.nodes.eachSubflow(function(sf) {
                if (workspace_tabs.contains(sf.id)) {
                    workspace_tabs.renameTab(sf.id,sf.name);
                }
            });
            RED.sidebar.config.refresh();
        },
        resize: function() {
            workspace_tabs.resize();
        },
        enable: enableWorkspace,
        disable: disableWorkspace,
        lock: lockWorkspace,
        unlock: unlockWorkspace
    }
})();
;/**
 * 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.
 **/

 /*!
 RED.statusBar.add({
     id: "widget-identifier",
     align: "left|right",
     element: widgetElement
 })
*/

RED.statusBar = (function() {

    var widgets = {};
    var leftBucket;
    var rightBucket;

    function addWidget(options) {
        widgets[options.id] = options;
        var el = $('<span class="red-ui-statusbar-widget"></span>');
        el.prop('id', options.id);
        options.element.appendTo(el);
        if (options.align === 'left') {
            leftBucket.append(el);
        } else if (options.align === 'right') {
            rightBucket.prepend(el);
        }
    }

    return {
        init: function() {
            leftBucket = $('<span class="red-ui-statusbar-bucket red-ui-statusbar-bucket-left">').appendTo("#red-ui-workspace-footer");
            rightBucket = $('<span class="red-ui-statusbar-bucket red-ui-statusbar-bucket-right">').appendTo("#red-ui-workspace-footer");
        },
        add: addWidget
    }

})();
;/**
 * 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.
 **/


 /* <div>#red-ui-workspace-chart
  *   \-  <svg> "outer"
  *       \- <g>
  *           \- <g>.red-ui-workspace-chart-event-layer "eventLayer"
  *                |- <rect>.red-ui-workspace-chart-background
  *                |- <g>.red-ui-workspace-chart-grid "gridLayer"
  *                |- <g> "groupLayer"
  *                |- <g> "groupSelectLayer"
  *                |- <g> "linkLayer"
  *                |- <g> "junctionLayer"
  *                |- <g> "dragGroupLayer"
  *                |- <g> "nodeLayer"
  */

RED.view = (function() {
    var space_width = 8000,
        space_height = 8000,
        lineCurveScale = 0.75,
        scaleFactor = 1,
        node_width = 100,
        node_height = 30,
        dblClickInterval = 650;

    var touchLongPressTimeout = 1000,
        startTouchDistance = 0,
        startTouchCenter = [],
        moveTouchCenter = [],
        touchStartTime = 0;

    var workspaceScrollPositions = {};

    var gridSize = 20;
    var snapGrid = false;

    var activeSpliceLink;
    var spliceActive = false;
    var spliceTimer;
    var groupHoverTimer;

    var activeFlowLocked = false;
    var activeSubflow = null;
    var activeNodes = [];
    var activeLinks = [];
    var activeJunctions = [];
    var activeFlowLinks = [];
    var activeLinkNodes = {};
    var activeHoverGroup = null;
    var groupAddActive = false;
    var groupAddParentGroup = null;
    var activeGroups = [];
    var dirtyGroups = {};

    var mousedown_link = null;
    var mousedown_node = null;
    var mousedown_group = null;
    var mousedown_port_type = null;
    var mousedown_port_index = 0;
    var mouseup_node = null;
    var mouse_offset = [0,0];
    var mouse_position = null;
    var mouse_mode = 0;
    var mousedown_group_handle = null;
    var lasso = null;
    var slicePath = null;
    var slicePathLast = null;
    var ghostNode = null;
    var showStatus = false;
    var lastClickNode = null;
    var dblClickPrimed = null;
    var clickTime = 0;
    var clickElapsed = 0;
    var scroll_position = [];
    var quickAddActive = false;
    var quickAddLink = null;
    var showAllLinkPorts = -1;
    var groupNodeSelectPrimed = false;
    var lastClickPosition = [];
    var selectNodesOptions;

    let flashingNodeId;

    var clipboard = "";
    let clipboardSource

    // Note: these are the permitted status colour aliases. The actual RGB values
    //       are set in the CSS - flow.scss/colors.scss
    const status_colours = {
        "red":    "#c00",
        "green":  "#5a8",
        "yellow": "#F9DF31",
        "blue":   "#53A3F3",
        "grey":   "#d3d3d3",
        "gray":   "#d3d3d3"
    }

    const PORT_TYPE_INPUT = 1;
    const PORT_TYPE_OUTPUT = 0;

    /**
     * The jQuery object for the workspace chart `#red-ui-workspace-chart` div element
     * @type {JQuery<HTMLElement>} #red-ui-workspace-chart HTML Element 
     */ 
    let chart;
    /**
     * The d3 object `#red-ui-workspace-chart` svg element
     * @type {d3.Selection<HTMLElement, Any, Any, Any>}
     */ 
    let outer;
    /** 
     * The d3 object `#red-ui-workspace-chart` svg element (specifically for events)
     * @type {d3.Selection<d3.BaseType, any, any, any>}
     */
    var eventLayer;

    /** @type {SVGGElement} */ let gridLayer;
    /** @type {SVGGElement} */ let linkLayer;
    /** @type {SVGGElement} */ let junctionLayer;
    /** @type {SVGGElement} */ let dragGroupLayer;
    /** @type {SVGGElement} */ let groupSelectLayer;
    /** @type {SVGGElement} */ let nodeLayer;
    /** @type {SVGGElement} */ let groupLayer;
    var drag_lines;

    const movingSet = (function() {
        var setIds = new Set();
        var set = [];
        const api = {
            add: function(node) {
                if (Array.isArray(node)) {
                    for (var i=0;i<node.length;i++) {
                        api.add(node[i]);
                    }
                } else {
                    if (!setIds.has(node.id)) {
                        set.push({n:node});
                        setIds.add(node.id);
                        var links = RED.nodes.getNodeLinks(node.id,PORT_TYPE_INPUT).concat(RED.nodes.getNodeLinks(node.id,PORT_TYPE_OUTPUT))
                        for (var i=0,l=links.length;i<l;i++) {
                            var link = links[i]
                            if (link.source === node && setIds.has(link.target.id) ||
                                link.target === node && setIds.has(link.source.id)) {
                                selectedLinks.add(link)
                            }
                        }
                    }
                }
            },
            remove: function(node, index) {
                if (setIds.has(node.id)) {
                    setIds.delete(node.id);
                    if (index !== undefined && set[index].n === node) {
                        set.splice(index,1);
                    } else {
                        for (var i=0;i<set.length;i++) {
                            if (set[i].n === node) {
                                set.splice(i,1)
                                break;
                            }
                        }
                    }
                    var links = RED.nodes.getNodeLinks(node.id,PORT_TYPE_INPUT).concat(RED.nodes.getNodeLinks(node.id,PORT_TYPE_OUTPUT))
                    for (var i=0,l=links.length;i<l;i++) {
                        selectedLinks.remove(links[i]);
                    }
                }
            },
            clear: function() {
                setIds.clear();
                set = [];
            },
            length: function() { return set.length},
            get: function(i) { return set[i] },
            forEach: function(func) { set.forEach(func) },
            nodes: function() { return set.map(function(n) { return n.n })},
            has: function(node) { return setIds.has(node.id) },
            /**
             * Make the specified node the first node of the moving set, if
             * it is already in the set.
             * @param {Node} node 
             */
            makePrimary: function (node) {
                const index = set.findIndex(n => n.n === node)
                if (index > -1) {
                    const removed = set.splice(index, 1)
                    set.unshift(...removed)
                }
            },
            find: function(func) { return set.find(func) },
            dump: function () {
                console.log('MovingSet Contents')
                api.forEach((n, i) => {
                    console.log(`${i+1}\t${n.n.id}\t${n.n.type}`)
                })
            }
        }
        return api;
    })();

    const selectedLinks = (function() {
        var links = new Set();
        const api = {
            add: function(link) {
                links.add(link);
                link.selected = true;
            },
            remove: function(link) {
                links.delete(link);
                link.selected = false;
            },
            clear: function() {
                links.forEach(function(link) { link.selected = false })
                links.clear();
            },
            length: function() {
                return links.size;
            },
            forEach: function(func) { links.forEach(func) },
            has: function(link) { return links.has(link) },
            toArray: function() { return Array.from(links) },
            clearUnselected: function () {
                api.forEach(l => {
                    if (!l.source.selected || !l.target.selected) {
                        api.remove(l)
                    }
                })
            }
        }
        return api
    })();

    const selectedGroups = (function() {
        let groups = new Set()
        const api = {
            add: function(g, includeNodes, addToMovingSet) {
                groups.add(g)
                if (!g.selected) {
                    g.selected = true;
                    g.dirty = true;
                }
                if (addToMovingSet !== false) {
                    movingSet.add(g);
                }
                if (includeNodes) {
                    var currentSet = new Set(movingSet.nodes());
                    var allNodes = RED.group.getNodes(g,true);
                    allNodes.forEach(function(n) {
                        if (!currentSet.has(n)) {
                            movingSet.add(n)
                        }
                        n.dirty = true;
                    })
                }
                selectedLinks.clearUnselected()
            },
            remove: function(g) {
                groups.delete(g)
                if (g.selected) {
                    g.selected = false;
                    g.dirty = true;
                }
                const allNodes = RED.group.getNodes(g,true);
                const nodeSet = new Set(allNodes);
                nodeSet.add(g);
                for (let i = movingSet.length()-1; i >= 0; i -= 1) {
                    const msn = movingSet.get(i);
                    if (nodeSet.has(msn.n) || msn.n === g) {
                        msn.n.selected = false;
                        msn.n.dirty = true;
                        movingSet.remove(msn.n,i)
                    }
                }
                selectedLinks.clearUnselected()
            },
            length: () => groups.size,
            forEach: (func) => { groups.forEach(func) },
            toArray: () => [...groups],
            clear: function () {
                groups.forEach(g => {
                    g.selected = false
                    g.dirty = true
                })
                groups.clear()
            }
        }
        return api
    })()

    const isMac = RED.utils.getBrowserInfo().os === 'mac'
    // 'Control' is the main modifier key for mouse actions. On Windows,
    // that is the standard Ctrl key. On Mac that is the Cmd key.
    function isControlPressed (event) {
        return (isMac && event.metaKey) || (!isMac && event.ctrlKey)
    }

    function init() {

        chart = $("#red-ui-workspace-chart");
        chart.on('contextmenu', function(evt) {
            if (RED.view.DEBUG) {
                console.warn("contextmenu", { mouse_mode, event: d3.event });
            }
            mouse_mode = RED.state.DEFAULT
            evt.preventDefault()
            evt.stopPropagation()
            RED.contextMenu.show({
                type: 'workspace',
                x: evt.clientX,
                y: evt.clientY
            })
            return false
        })
        outer = d3.select("#red-ui-workspace-chart")
            .append("svg:svg")
            .attr("width", space_width)
            .attr("height", space_height)
            .attr("pointer-events", "all")
            .style("cursor","crosshair")
            .style("touch-action","none")
            .on("mousedown", function() {
                focusView();
            })
            .on("contextmenu", function(){
                d3.event.preventDefault();
            });

        eventLayer = outer
            .append("svg:g")
            .on("dblclick.zoom", null)
            .append("svg:g")
            .attr('class','red-ui-workspace-chart-event-layer')
            .on("mousemove", canvasMouseMove)
            .on("mousedown", canvasMouseDown)
            .on("mouseup", canvasMouseUp)
            .on("mouseenter", function() {
                d3.select(document).on('mouseup.red-ui-workspace-tracker', null)
                if (lasso) {
                    if (d3.event.buttons !== 1) {
                        outer.classed('red-ui-workspace-lasso-active', false)
                        lasso.remove();
                        lasso = null;
                    }
                } else if (mouse_mode === RED.state.PANNING && d3.event.buttons !== 4) {
                    resetMouseVars();
                } else if (slicePath) {
                    if (d3.event.buttons !== 2) {
                        slicePath.remove();
                        slicePath = null;
                        resetMouseVars()
                    }
                }
            })
            .on("mouseleave", canvasMouseLeave)
            .on("touchend", function() {
                d3.event.preventDefault();
                clearTimeout(touchStartTime);
                touchStartTime = null;
                if  (RED.touch.radialMenu.active()) {
                    return;
                }
                canvasMouseUp.call(this);
            })
            .on("touchcancel", function() {
                if (RED.view.DEBUG) { console.warn("eventLayer.touchcancel", mouse_mode); }
                d3.event.preventDefault();
                canvasMouseUp.call(this);
            })
            .on("touchstart", function() {
                if (RED.view.DEBUG) { console.warn("eventLayer.touchstart", mouse_mode); }
                var touch0;
                if (d3.event.touches.length>1) {
                    clearTimeout(touchStartTime);
                    touchStartTime = null;
                    d3.event.preventDefault();
                    touch0 = d3.event.touches.item(0);
                    var touch1 = d3.event.touches.item(1);
                    var a = touch0["pageY"]-touch1["pageY"];
                    var b = touch0["pageX"]-touch1["pageX"];

                    var offset = chart.offset();
                    var scrollPos = [chart.scrollLeft(),chart.scrollTop()];
                    startTouchCenter = [
                        (touch1["pageX"]+(b/2)-offset.left+scrollPos[0])/scaleFactor,
                        (touch1["pageY"]+(a/2)-offset.top+scrollPos[1])/scaleFactor
                    ];
                    moveTouchCenter = [
                        touch1["pageX"]+(b/2),
                        touch1["pageY"]+(a/2)
                    ]
                    startTouchDistance = Math.sqrt((a*a)+(b*b));
                } else {
                    var obj = d3.select(document.body);
                    touch0 = d3.event.touches.item(0);
                    var pos = [touch0.pageX,touch0.pageY];
                    startTouchCenter = [touch0.pageX,touch0.pageY];
                    startTouchDistance = 0;
                    var point = d3.touches(this)[0];
                    touchStartTime = setTimeout(function() {
                        touchStartTime = null;
                        showTouchMenu(obj,pos);
                    },touchLongPressTimeout);
                }
                d3.event.preventDefault();
            })
            .on("touchmove", function(){
                    if  (RED.touch.radialMenu.active()) {
                        d3.event.preventDefault();
                        return;
                    }
                    if (RED.view.DEBUG) { console.warn("eventLayer.touchmove", mouse_mode, mousedown_node); }
                    var touch0;
                    if (d3.event.touches.length<2) {
                        if (touchStartTime) {
                            touch0 = d3.event.touches.item(0);
                            var dx = (touch0.pageX-startTouchCenter[0]);
                            var dy = (touch0.pageY-startTouchCenter[1]);
                            var d = Math.abs(dx*dx+dy*dy);
                            if (d > 64) {
                                clearTimeout(touchStartTime);
                                touchStartTime = null;
                                if (!mousedown_node && !mousedown_group) {
                                    mouse_mode = RED.state.PANNING;
                                    mouse_position = [touch0.pageX,touch0.pageY]
                                    scroll_position = [chart.scrollLeft(),chart.scrollTop()];
                                }

                            }
                        } else if (lasso) {
                            d3.event.preventDefault();
                        }
                        canvasMouseMove.call(this);
                    } else {
                        touch0 = d3.event.touches.item(0);
                        var touch1 = d3.event.touches.item(1);
                        var a = touch0["pageY"]-touch1["pageY"];
                        var b = touch0["pageX"]-touch1["pageX"];
                        var offset = chart.offset();
                        var scrollPos = [chart.scrollLeft(),chart.scrollTop()];
                        var moveTouchDistance = Math.sqrt((a*a)+(b*b));
                        var touchCenter = [
                            touch1["pageX"]+(b/2),
                            touch1["pageY"]+(a/2)
                        ];

                        if (!isNaN(moveTouchDistance)) {
                            oldScaleFactor = scaleFactor;
                            scaleFactor = Math.min(2,Math.max(0.3, scaleFactor + (Math.floor(((moveTouchDistance*100)-(startTouchDistance*100)))/10000)));

                            var deltaTouchCenter = [                             // Try to pan whilst zooming - not 100%
                                startTouchCenter[0]*(scaleFactor-oldScaleFactor),//-(touchCenter[0]-moveTouchCenter[0]),
                                startTouchCenter[1]*(scaleFactor-oldScaleFactor) //-(touchCenter[1]-moveTouchCenter[1])
                            ];

                            startTouchDistance = moveTouchDistance;
                            moveTouchCenter = touchCenter;

                            chart.scrollLeft(scrollPos[0]+deltaTouchCenter[0]);
                            chart.scrollTop(scrollPos[1]+deltaTouchCenter[1]);
                            redraw();
                        }
                    }
                    d3.event.preventDefault();
            });
            

        const handleAltToggle = (event) => {
            if (mouse_mode === RED.state.MOVING_ACTIVE && event.key === 'Alt' && groupAddParentGroup) {
                RED.nodes.group(groupAddParentGroup).dirty = true
                for (let n = 0; n<movingSet.length(); n++) {
                    const node = movingSet.get(n);
                    node.n._detachFromGroup = event.altKey
                }
                if (!event.altKey) {
                    if (groupHoverTimer) {
                        clearTimeout(groupHoverTimer)
                        groupHoverTimer = null
                    }
                    if (activeHoverGroup) {
                        activeHoverGroup.hovered = false
                        activeHoverGroup.dirty = true
                        activeHoverGroup = null
                    }
                }
                RED.view.redraw()
            }
        }
        document.addEventListener("keyup", handleAltToggle)
        document.addEventListener("keydown", handleAltToggle)

        // Workspace Background
        eventLayer.append("svg:rect")
            .attr("class","red-ui-workspace-chart-background")
            .attr("width", space_width)
            .attr("height", space_height);

        gridLayer = eventLayer.append("g").attr("class","red-ui-workspace-chart-grid");
        updateGrid();

        groupLayer = eventLayer.append("g");
        groupSelectLayer = eventLayer.append("g");
        linkLayer = eventLayer.append("g");
        dragGroupLayer = eventLayer.append("g");
        junctionLayer = eventLayer.append("g");
        nodeLayer = eventLayer.append("g");

        drag_lines = [];

        RED.events.on("workspace:change",function(event) {
            // Just in case the mouse left the workspace whilst doing an action,
            // put us back into default mode so the refresh works
            mouse_mode = 0
            if (event.old !== 0) {
                workspaceScrollPositions[event.old] = {
                    left:chart.scrollLeft(),
                    top:chart.scrollTop()
                };
            }
            var scrollStartLeft = chart.scrollLeft();
            var scrollStartTop = chart.scrollTop();

            activeSubflow = RED.nodes.subflow(event.workspace);

            if (activeSubflow) {
                activeFlowLocked = activeSubflow.locked
            } else {
                var activeWorkspace = RED.nodes.workspace(event.workspace)
                if (activeWorkspace) {
                    activeFlowLocked = activeWorkspace.locked
                } else {
                    activeFlowLocked = true
                }
            }

            RED.menu.setDisabled("menu-item-workspace-edit", activeFlowLocked || activeSubflow || event.workspace === 0);
            RED.menu.setDisabled("menu-item-workspace-delete",activeFlowLocked || event.workspace === 0 || RED.workspaces.count() == 1 || activeSubflow);

            if (workspaceScrollPositions[event.workspace]) {
                chart.scrollLeft(workspaceScrollPositions[event.workspace].left);
                chart.scrollTop(workspaceScrollPositions[event.workspace].top);
            } else {
                chart.scrollLeft(0);
                chart.scrollTop(0);
            }
            var scrollDeltaLeft = chart.scrollLeft() - scrollStartLeft;
            var scrollDeltaTop = chart.scrollTop() - scrollStartTop;
            if (mouse_position != null) {
                mouse_position[0] += scrollDeltaLeft;
                mouse_position[1] += scrollDeltaTop;
            }
            if (RED.workspaces.selection().length === 0) {
                clearSelection();
            }
            RED.nodes.eachNode(function(n) {
                n.dirty = true;
                n.dirtyStatus = true;
            });
            updateSelection();
            updateActiveNodes();
            redraw();
        });

        RED.events.on("flows:change", function(workspace) {
            if (workspace.id === RED.workspaces.active()) {
                activeFlowLocked = !!workspace.locked
                $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!workspace.disabled);
                $("#red-ui-workspace").toggleClass("red-ui-workspace-locked",!!workspace.locked);

            }
        })

        RED.statusBar.add({
            id: "view-zoom-controls",
            align: "right",
            element: $('<span class="button-group">'+
            '<button class="red-ui-footer-button" id="red-ui-view-zoom-out"><i class="fa fa-minus"></i></button>'+
            '<button class="red-ui-footer-button" id="red-ui-view-zoom-zero"><i class="fa fa-circle-o"></i></button>'+
            '<button class="red-ui-footer-button" id="red-ui-view-zoom-in"><i class="fa fa-plus"></i></button>'+
            '</span>')
        })

        $("#red-ui-view-zoom-out").on("click", zoomOut);
        RED.popover.tooltip($("#red-ui-view-zoom-out"),RED._('actions.zoom-out'),'core:zoom-out');
        $("#red-ui-view-zoom-zero").on("click", zoomZero);
        RED.popover.tooltip($("#red-ui-view-zoom-zero"),RED._('actions.zoom-reset'),'core:zoom-reset');
        $("#red-ui-view-zoom-in").on("click", zoomIn);
        RED.popover.tooltip($("#red-ui-view-zoom-in"),RED._('actions.zoom-in'),'core:zoom-in');
        chart.on("DOMMouseScroll mousewheel", function (evt) {
            if ( evt.altKey ) {
                evt.preventDefault();
                evt.stopPropagation();
                var move = -(evt.originalEvent.detail) || evt.originalEvent.wheelDelta;
                if (move <= 0) { zoomOut(); }
                else { zoomIn(); }
            }
        });

        //add search to status-toolbar
        RED.statusBar.add({
            id: "view-search-tools",
            align: "left",
            hidden: false,
            element: $('<span class="button-group">'+
                    '<button class="red-ui-footer-button" id="red-ui-view-searchtools-search"><i class="fa fa-search"></i></button>' +
                    '</span>' +
                    '<span class="button-group search-counter">' +
                    '<span class="red-ui-footer-button" id="red-ui-view-searchtools-counter">? of ?</span>' +
                    '</span>' +
                    '<span class="button-group">' +
                    '<button class="red-ui-footer-button" id="red-ui-view-searchtools-prev"><i class="fa fa-chevron-left"></i></button>' +
                    '<button class="red-ui-footer-button" id="red-ui-view-searchtools-next"><i class="fa fa-chevron-right"></i></button>' +
                    '</span>' +
                    '<span class="button-group">' +
                    '<button class="red-ui-footer-button" id="red-ui-view-searchtools-close"><i class="fa fa-close"></i></button>' +
                    '</span>')
        })
        $("#red-ui-view-searchtools-search").on("click", searchFlows);
        RED.popover.tooltip($("#red-ui-view-searchtools-search"),RED._('actions.search-flows'),'core:search');
        $("#red-ui-view-searchtools-prev").on("click", searchPrev);
        RED.popover.tooltip($("#red-ui-view-searchtools-prev"),RED._('actions.search-prev'),'core:search-previous');
        $("#red-ui-view-searchtools-next").on("click", searchNext);
        RED.popover.tooltip($("#red-ui-view-searchtools-next"),RED._('actions.search-next'),'core:search-next');
        RED.popover.tooltip($("#red-ui-view-searchtools-close"),RED._('common.label.close'));

        // Handle nodes dragged from the palette
        chart.droppable({
            accept:".red-ui-palette-node",
            drop: function( event, ui ) {
                if (activeFlowLocked) {
                    return
                }
                d3.event = event;
                var selected_tool = $(ui.draggable[0]).attr("data-palette-type");
                try {
                    var result = createNode(selected_tool);
                    if (!result) {
                        return;
                    }
                    var historyEvent = result.historyEvent;
                    var nn = RED.nodes.add(result.node);

                    var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label");
                    if (showLabel !== undefined &&  (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) {
                        nn.l = showLabel;
                    }

                    var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0));
                    var helperWidth = ui.helper.width();
                    var helperHeight = ui.helper.height();
                    var mousePos = d3.touches(this)[0]||d3.mouse(this);

                    try {
                        var isLink = (nn.type === "link in" || nn.type === "link out")
                        var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink;

                        var label = RED.utils.getNodeLabel(nn, nn.type);
                        var labelParts = getLabelParts(label, "red-ui-flow-node-label");
                        if (hideLabel) {
                            nn.w = node_height;
                            nn.h = Math.max(node_height,(nn.outputs || 0) * 15);
                        } else {
                            nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) );
                            nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30);
                        }
                    } catch(err) {
                    }

                    mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]);
                    mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]);
                    mousePos[1] /= scaleFactor;
                    mousePos[0] /= scaleFactor;

                    nn.x = mousePos[0];
                    nn.y = mousePos[1];

                    var minX = nn.w/2 -5;
                    if (nn.x < minX) {
                        nn.x = minX;
                    }
                    var minY = nn.h/2 -5;
                    if (nn.y < minY) {
                        nn.y = minY;
                    }
                    var maxX = space_width -nn.w/2 +5;
                    if (nn.x > maxX) {
                        nn.x = maxX;
                    }
                    var maxY = space_height -nn.h +5;
                    if (nn.y > maxY) {
                        nn.y = maxY;
                    }

                    if (snapGrid) {
                        var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn);
                        nn.x -= gridOffset.x;
                        nn.y -= gridOffset.y;
                    }

                    var linkToSplice = $(ui.helper).data("splice");
                    if (linkToSplice) {
                        spliceLink(linkToSplice, nn, historyEvent)
                    }

                    var group = $(ui.helper).data("group");
                    if (group) {
                        var oldX = group.x; 
                        var oldY = group.y; 
                        RED.group.addToGroup(group, nn);
                        var moveEvent = null;
                        if ((group.x !== oldX) ||
                            (group.y !== oldY)) {
                            moveEvent = {
                                t: "move",
                                nodes: [{n: group,
                                        ox: oldX, oy: oldY,
                                        dx: group.x -oldX,
                                        dy: group.y -oldY}],
                                dirty: true
                            };
                        }
                        historyEvent = {
                            t: 'multi',
                            events: [historyEvent],

                        };
                        if (moveEvent) {
                            historyEvent.events.push(moveEvent)
                        }
                        historyEvent.events.push({
                            t: "addToGroup",
                            group: group,
                            nodes: nn
                        })
                    }

                    RED.history.push(historyEvent);
                    RED.editor.validateNode(nn);
                    RED.nodes.dirty(true);
                    // auto select dropped node - so info shows (if visible)
                    clearSelection();
                    nn.selected = true;
                    movingSet.add(nn);
                    updateActiveNodes();
                    updateSelection();
                    redraw();

                    if (nn._def.autoedit) {
                        RED.editor.edit(nn);
                    }
                } catch (error) {
                    if (error.code != "NODE_RED") {
                        RED.notify(RED._("notification.error",{message:error.toString()}),"error");
                    } else {
                        RED.notify(RED._("notification.error",{message:error.message}),"error");
                    }
                }
            }
        });
        chart.on("focus", function() {
            $("#red-ui-workspace-tabs").addClass("red-ui-workspace-focussed");
        });
        chart.on("blur", function() {
            $("#red-ui-workspace-tabs").removeClass("red-ui-workspace-focussed");
        });

        RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection);
        RED.actions.add("core:cut-selection-to-internal-clipboard",function(){copySelection(true);deleteSelection();});
        RED.actions.add("core:paste-from-internal-clipboard",function(){
            if (RED.workspaces.isLocked()) {
                return
            }
            importNodes(clipboard,{generateIds: clipboardSource === 'copy', generateDefaultNames: clipboardSource === 'copy'});
        });

        RED.actions.add("core:detach-selected-nodes", function() { detachSelectedNodes() })

        RED.events.on("view:selection-changed", function(selection) {
            var hasSelection = (selection.nodes && selection.nodes.length > 0);
            var hasMultipleSelection = hasSelection && selection.nodes.length > 1;
            var hasLinkSelected = selection.links && selection.links.length > 0;
            var canEdit = !activeFlowLocked && hasSelection
            var canEditMultiple = !activeFlowLocked && hasMultipleSelection
            RED.menu.setDisabled("menu-item-edit-cut", !canEdit);
            RED.menu.setDisabled("menu-item-edit-copy", !hasSelection);
            RED.menu.setDisabled("menu-item-edit-select-connected", !hasSelection);
            RED.menu.setDisabled("menu-item-view-tools-move-to-back", !canEdit);
            RED.menu.setDisabled("menu-item-view-tools-move-to-front", !canEdit);
            RED.menu.setDisabled("menu-item-view-tools-move-backwards", !canEdit);
            RED.menu.setDisabled("menu-item-view-tools-move-forwards", !canEdit);

            RED.menu.setDisabled("menu-item-view-tools-align-left", !canEditMultiple);
            RED.menu.setDisabled("menu-item-view-tools-align-center", !canEditMultiple);
            RED.menu.setDisabled("menu-item-view-tools-align-right", !canEditMultiple);
            RED.menu.setDisabled("menu-item-view-tools-align-top", !canEditMultiple);
            RED.menu.setDisabled("menu-item-view-tools-align-middle", !canEditMultiple);
            RED.menu.setDisabled("menu-item-view-tools-align-bottom", !canEditMultiple);
            RED.menu.setDisabled("menu-item-view-tools-distribute-horizontally", !canEditMultiple);
            RED.menu.setDisabled("menu-item-view-tools-distribute-veritcally", !canEditMultiple);

            RED.menu.setDisabled("menu-item-edit-split-wire-with-links", activeFlowLocked || !hasLinkSelected);
        })

        RED.actions.add("core:delete-selection",deleteSelection);
        RED.actions.add("core:delete-selection-and-reconnect",function() { deleteSelection(true) });
        RED.actions.add("core:edit-selected-node",editSelection);
        RED.actions.add("core:go-to-selection",function() {
            if (movingSet.length() > 0) {
                var node = movingSet.get(0).n;
                if (/^subflow:/.test(node.type)) {
                    RED.workspaces.show(node.type.substring(8))
                } else if (node.type === 'group') {
                    // enterActiveGroup(node);
                    redraw();
                }
            }
        });
        RED.actions.add("core:undo",RED.history.pop);
        RED.actions.add("core:redo",RED.history.redo);
        RED.actions.add("core:select-all-nodes",selectAll);
        RED.actions.add("core:select-none", selectNone);
        RED.actions.add("core:zoom-in",zoomIn);
        RED.actions.add("core:zoom-out",zoomOut);
        RED.actions.add("core:zoom-reset",zoomZero);
        RED.actions.add("core:enable-selected-nodes", function() { setSelectedNodeState(false)});
        RED.actions.add("core:disable-selected-nodes", function() { setSelectedNodeState(true)});

        RED.actions.add("core:toggle-show-grid",function(state) {
            if (state === undefined) {
                RED.userSettings.toggle("view-show-grid");
            } else {
                toggleShowGrid(state);
            }
        });
        RED.actions.add("core:toggle-snap-grid",function(state) {
            if (state === undefined) {
                RED.userSettings.toggle("view-snap-grid");
            } else {
                toggleSnapGrid(state);
            }
        });
        RED.actions.add("core:toggle-status",function(state) {
            if (state === undefined) {
                RED.userSettings.toggle("view-node-status");
            } else {
                toggleStatus(state);
            }
        });

        RED.view.annotations.init();
        RED.view.navigator.init();
        RED.view.tools.init();


        RED.view.annotations.register("red-ui-flow-node-changed",{
            type: "badge",
            class: "red-ui-flow-node-changed",
            element: function() {
                var changeBadge = document.createElementNS("http://www.w3.org/2000/svg","circle");
                changeBadge.setAttribute("cx",5);
                changeBadge.setAttribute("cy",5);
                changeBadge.setAttribute("r",5);
                return changeBadge;
            },
            show: function(n) { return n.changed||n.moved }
        })

        RED.view.annotations.register("red-ui-flow-node-error",{
            type: "badge",
            class: "red-ui-flow-node-error",
            element: function(d) {
                var errorBadge = document.createElementNS("http://www.w3.org/2000/svg","path");
                errorBadge.setAttribute("d","M 0,9 l 10,0 -5,-8 z");
                return errorBadge
            },
            tooltip: function(d) {
                if (d.validationErrors && d.validationErrors.length > 0) {
                    return RED._("editor.errors.invalidProperties")+"\n  - "+d.validationErrors.join("\n  - ")
                }
            },
            show: function(n) { return !n.valid }
        })

        if (RED.settings.get("editor.view.view-store-zoom")) {
            var  userZoomLevel = parseFloat(RED.settings.getLocal('zoom-level'))
            if (!isNaN(userZoomLevel)) {
                scaleFactor = userZoomLevel
            }
        }

        var onScrollTimer = null;
        function storeScrollPosition() {
            workspaceScrollPositions[RED.workspaces.active()] = {
                left:chart.scrollLeft(),
                top:chart.scrollTop()
            };
            RED.settings.setLocal('scroll-positions', JSON.stringify(workspaceScrollPositions) )
        }
        chart.on("scroll", function() {
            if (RED.settings.get("editor.view.view-store-position")) {
                if (onScrollTimer) {
                    clearTimeout(onScrollTimer)
                }
                onScrollTimer = setTimeout(storeScrollPosition, 200);
            }
        })

        if (RED.settings.get("editor.view.view-store-position")) {
            var scrollPositions = RED.settings.getLocal('scroll-positions')
            if (scrollPositions) {
                try {
                    workspaceScrollPositions = JSON.parse(scrollPositions)
                } catch(err) {
                }
            }
        }
    }



    function updateGrid() {
        var gridTicks = [];
        for (var i=0;i<space_width;i+=+gridSize) {
            gridTicks.push(i);
        }
        gridLayer.selectAll("line.red-ui-workspace-chart-grid-h").remove();
        gridLayer.selectAll("line.red-ui-workspace-chart-grid-h").data(gridTicks).enter()
            .append("line")
            .attr(
                {
                    "class":"red-ui-workspace-chart-grid-h",
                    "x1" : 0,
                    "x2" : space_width,
                    "y1" : function(d){ return d;},
                    "y2" : function(d){ return d;}
                });
        gridLayer.selectAll("line.red-ui-workspace-chart-grid-v").remove();
        gridLayer.selectAll("line.red-ui-workspace-chart-grid-v").data(gridTicks).enter()
            .append("line")
            .attr(
                {
                    "class":"red-ui-workspace-chart-grid-v",
                    "y1" : 0,
                    "y2" : space_width,
                    "x1" : function(d){ return d;},
                    "x2" : function(d){ return d;}
                });
    }

    function showDragLines(nodes) {
        showAllLinkPorts = -1;
        for (var i=0;i<nodes.length;i++) {
            var node = nodes[i];
            node.el = dragGroupLayer.append("svg:path").attr("class", "red-ui-flow-drag-line");
            if ((node.node.type === "link out" && node.portType === PORT_TYPE_OUTPUT) ||
                (node.node.type === "link in" && node.portType === PORT_TYPE_INPUT)) {
                node.el.attr("class","red-ui-flow-link-link red-ui-flow-drag-line");
                node.virtualLink = true;
                showAllLinkPorts = (node.portType === PORT_TYPE_OUTPUT)?PORT_TYPE_INPUT:PORT_TYPE_OUTPUT;
            }
            drag_lines.push(node);
        }
        if (showAllLinkPorts !== -1) {
            activeNodes.forEach(function(n) {
                if (n.type === "link in" || n.type === "link out") {
                    n.dirty = true;
                }
            })
        }
    }
    function hideDragLines() {
        if (showAllLinkPorts !== -1) {
            activeNodes.forEach(function(n) {
                if (n.type === "link in" || n.type === "link out") {
                    n.dirty = true;
                }
            })
        }
        showAllLinkPorts = -1;
        while(drag_lines.length) {
            var line = drag_lines.pop();
            if (line.el) {
                line.el.remove();
            }
        }
    }

    function updateActiveNodes() {
        var activeWorkspace = RED.workspaces.active();
        if (activeWorkspace !== 0) {
            activeNodes = RED.nodes.filterNodes({z:activeWorkspace});
            activeNodes.forEach(function(n,i) {
                n._index = i;
            })
            activeLinks = RED.nodes.filterLinks({
                source:{z:activeWorkspace},
                target:{z:activeWorkspace}
            });
            activeJunctions = RED.nodes.junctions(activeWorkspace) || [];
            activeGroups = RED.nodes.groups(activeWorkspace)||[];
            if (activeGroups.length) {
                const groupTree = {}
                const rootGroups = []
                activeGroups.forEach(function(g, i) {
                    groupTree[g.id] = g
                    g._index = i;
                    g._childGroups = []
                    if (!g.g) {
                        rootGroups.push(g)
                    }
                });
                activeGroups.forEach(function(g) {
                    if (g.g) {
                        groupTree[g.g]._childGroups.push(g)
                        g._parentGroup = groupTree[g.g]
                    }
                })
                let ii = 0
                // Depth-first walk of the groups
                const processGroup = g => {
                    g._order = ii++
                    g._childGroups.forEach(processGroup)
                }
                rootGroups.forEach(processGroup)
            }
        } else {
            activeNodes = [];
            activeLinks = [];
            activeJunctions = [];
            activeGroups = [];
        }

        activeGroups.sort(function(a,b) {
            return a._order - b._order
        });

        var group = groupLayer.selectAll(".red-ui-flow-group").data(activeGroups,function(d) { return d.id });
        group.sort(function(a,b) {
            return a._order - b._order
        })
    }

    function generateLinkPath(origX,origY, destX, destY, sc, hasStatus = false) {
        var dy = destY-origY;
        var dx = destX-origX;
        var delta = Math.sqrt(dy*dy+dx*dx);
        var scale = lineCurveScale;
        var scaleY = 0;
        if (dx*sc > 0) {
            if (delta < node_width) {
                scale = 0.75-0.75*((node_width-delta)/node_width);
                // scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width));
                // if (Math.abs(dy) < 3*node_height) {
                //     scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ;
                // }
            }
        } else {
            scale = 0.4-0.2*(Math.max(0,(node_width-Math.min(Math.abs(dx),Math.abs(dy)))/node_width));
        }
        function genCP(cp) {
            return ` M ${cp[0]-5} ${cp[1]} h 10 M ${cp[0]} ${cp[1]-5} v 10 `
        }
        if (dx*sc > 0) {
            let cp = [
                [(origX+sc*(node_width*scale)), (origY+scaleY*node_height)],
                [(destX-sc*(scale)*node_width), (destY-scaleY*node_height)]
            ]
            return `M ${origX} ${origY} C ${cp[0][0]} ${cp[0][1]} ${cp[1][0]} ${cp[1][1]} ${destX} ${destY}`
                //    + ` ${genCP(cp[0])} ${genCP(cp[1])}`
        } else {
            let topX, topY, bottomX, bottomY
            let cp
            let midX = Math.floor(destX-dx/2);
            let midY = Math.floor(destY-dy/2);          
            if (Math.abs(dy) < 10) {
                bottomY = Math.max(origY, destY) + (hasStatus?35:25)
                let startCurveHeight = bottomY - origY
                let endCurveHeight = bottomY - destY
                cp = [
                    [ origX + sc*15 , origY ],
                    [ origX + sc*25 , origY + 5 ],
                    [ origX + sc*25 , origY + startCurveHeight/2 ],

                    [ origX + sc*25 , origY + startCurveHeight - 5 ],
                    [ origX + sc*15 , origY + startCurveHeight ],
                    [ origX , origY + startCurveHeight ],

                    [ destX - sc*15, origY + startCurveHeight ],
                    [ destX - sc*25, origY + startCurveHeight - 5 ],
                    [ destX - sc*25, destY + endCurveHeight/2 ],

                    [ destX - sc*25, destY + 5 ],
                    [ destX - sc*15, destY ],
                    [ destX, destY ],
                ]

                return "M "+origX+" "+origY+
                    " C "+
                    cp[0][0]+" "+cp[0][1]+" "+
                    cp[1][0]+" "+cp[1][1]+" "+
                    cp[2][0]+" "+cp[2][1]+" "+
                    " C " +
                    cp[3][0]+" "+cp[3][1]+" "+
                    cp[4][0]+" "+cp[4][1]+" "+
                    cp[5][0]+" "+cp[5][1]+" "+
                    " h "+dx+
                    " C "+
                    cp[6][0]+" "+cp[6][1]+" "+
                    cp[7][0]+" "+cp[7][1]+" "+
                    cp[8][0]+" "+cp[8][1]+" "+
                    " C " +
                    cp[9][0]+" "+cp[9][1]+" "+
                    cp[10][0]+" "+cp[10][1]+" "+
                    cp[11][0]+" "+cp[11][1]+" "
                    // +genCP(cp[0])+genCP(cp[1])+genCP(cp[2])+genCP(cp[3])+genCP(cp[4])
                    // +genCP(cp[5])+genCP(cp[6])+genCP(cp[7])+genCP(cp[8])+genCP(cp[9])+genCP(cp[10])
            } else {
                var cp_height = node_height/2;
                var y1 = (destY + midY)/2
                topX = origX + sc*node_width*scale;
                topY = dy>0?Math.min(y1 - dy/2 , origY+cp_height):Math.max(y1 - dy/2 , origY-cp_height);
                bottomX = destX - sc*node_width*scale;
                bottomY = dy>0?Math.max(y1, destY-cp_height):Math.min(y1, destY+cp_height);
                var x1 = (origX+topX)/2;
                var scy = dy>0?1:-1;
                cp = [
                    // Orig -> Top
                    [x1,origY],
                    [topX,dy>0?Math.max(origY, topY-cp_height):Math.min(origY, topY+cp_height)],
                    // Top -> Mid
                    // [Mirror previous cp]
                    [x1,dy>0?Math.min(midY, topY+cp_height):Math.max(midY, topY-cp_height)],
                    // Mid -> Bottom
                    // [Mirror previous cp]
                    [bottomX,dy>0?Math.max(midY, bottomY-cp_height):Math.min(midY, bottomY+cp_height)],
                    // Bottom -> Dest
                    // [Mirror previous cp]
                    [(destX+bottomX)/2,destY]
                ];
                if (cp[2][1] === topY+scy*cp_height) {
                    if (Math.abs(dy) < cp_height*10) {
                        cp[1][1] = topY-scy*cp_height/2;
                        cp[3][1] = bottomY-scy*cp_height/2;
                    }
                    cp[2][0] = topX;
                }
                return "M "+origX+" "+origY+
                    " C "+
                    cp[0][0]+" "+cp[0][1]+" "+
                    cp[1][0]+" "+cp[1][1]+" "+
                    topX+" "+topY+
                    " S "+
                    cp[2][0]+" "+cp[2][1]+" "+
                    midX+" "+midY+
                " S "+
                    cp[3][0]+" "+cp[3][1]+" "+
                    bottomX+" "+bottomY+
                    " S "+
                        cp[4][0]+" "+cp[4][1]+" "+
                        destX+" "+destY

                // +genCP(cp[0])+genCP(cp[1])+genCP(cp[2])+genCP(cp[3])+genCP(cp[4])
            }
        }
    }

    function canvasMouseDown() {
        if (RED.view.DEBUG) {
            console.warn("canvasMouseDown", { mouse_mode, point: d3.mouse(this), event: d3.event });
        }
        RED.contextMenu.hide();
        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }

        if (d3.event.button === 1) {
            // Middle Click pan
            d3.event.preventDefault();
            mouse_mode = RED.state.PANNING;
            mouse_position = [d3.event.pageX,d3.event.pageY]
            scroll_position = [chart.scrollLeft(),chart.scrollTop()];
            return;
        }
        if (d3.event.button === 2) {
            return
        }
        if (!mousedown_node && !mousedown_link && !mousedown_group && !d3.event.shiftKey) {
            selectedLinks.clear();
            updateSelection();
        }
        if (mouse_mode === 0 && lasso) {
            outer.classed('red-ui-workspace-lasso-active', false)
            lasso.remove();
            lasso = null;
        }
        if (d3.event.touches || d3.event.button === 0) {
            if (
                (mouse_mode === 0 && isControlPressed(d3.event) && !(d3.event.altKey || d3.event.shiftKey)) ||
                mouse_mode === RED.state.QUICK_JOINING
            ) {
                // Trigger quick add dialog
                d3.event.stopPropagation();
                clearSelection();
                const point = d3.mouse(this);
                var clickedGroup = getGroupAt(point[0], point[1]);
                if (drag_lines.length > 0) {
                    clickedGroup = clickedGroup || RED.nodes.group(drag_lines[0].node.g)
                }
                showQuickAddDialog({ position: point, group: clickedGroup });
            } else if (mouse_mode === 0 && !isControlPressed(d3.event)) {
                // CTRL not being held
                if (!d3.event.altKey) {
                    // ALT not held (shift is allowed) Trigger lasso
                    if (!touchStartTime) {
                        const point = d3.mouse(this);
                        lasso = eventLayer.append("rect")
                            .attr("ox", point[0])
                            .attr("oy", point[1])
                            .attr("rx", 1)
                            .attr("ry", 1)
                            .attr("x", point[0])
                            .attr("y", point[1])
                            .attr("width", 0)
                            .attr("height", 0)
                            .attr("class", "nr-ui-view-lasso");
                        d3.event.preventDefault();
                        outer.classed('red-ui-workspace-lasso-active', true)
                    }
                } else if (d3.event.altKey && !activeFlowLocked) {
                    //Alt [+shift] held - Begin slicing
                    clearSelection();
                    mouse_mode = (d3.event.shiftKey) ? RED.state.SLICING_JUNCTION : RED.state.SLICING;
                    const point = d3.mouse(this);
                    slicePath = eventLayer.append("path").attr("class", "nr-ui-view-slice").attr("d", `M${point[0]} ${point[1]}`)
                    slicePathLast = point;
                    RED.view.redraw();
                }
            }
        }
    }

    function showQuickAddDialog(options) {
        if (activeFlowLocked) {
            return
        }
        options = options || {};
        var point = options.position || lastClickPosition;
        var linkToSplice = options.splice;
        var spliceMultipleLinks = options.spliceMultiple
        var targetGroup = options.group;
        var touchTrigger = options.touchTrigger;

        // `point` is the place in the workspace the mouse has clicked.
        //  This takes into account scrolling and scaling of the workspace.
        var ox = point[0];
        var oy = point[1];

        // Need to map that to browser location to position the pop-up
        const offset = $("#red-ui-workspace-chart").offset()
        var clientX = (ox * scaleFactor) + offset.left - $("#red-ui-workspace-chart").scrollLeft()
        var clientY = (oy * scaleFactor) + offset.top - $("#red-ui-workspace-chart").scrollTop()

        if (RED.settings.get("editor").view['view-snap-grid']) {
            // eventLayer.append("circle").attr("cx",point[0]).attr("cy",point[1]).attr("r","2").attr('fill','red')
            point[0] = Math.round(point[0] / gridSize) * gridSize;
            point[1] = Math.round(point[1] / gridSize) * gridSize;
            // eventLayer.append("circle").attr("cx",point[0]).attr("cy",point[1]).attr("r","2").attr('fill','blue')
        }

        var mainPos = $("#red-ui-main-container").position();
        if (mouse_mode !== RED.state.QUICK_JOINING) {
            mouse_mode = RED.state.QUICK_JOINING;
            $(window).on('keyup',disableQuickJoinEventHandler);
        }
        quickAddActive = true;

        if (ghostNode) {
            ghostNode.remove();
        }
        ghostNode = eventLayer.append("g").attr('transform','translate('+(point[0] - node_width/2)+','+(point[1] - node_height/2)+')');
        ghostNode.append("rect")
            .attr("class","red-ui-flow-node-placeholder")
            .attr("rx", 5)
            .attr("ry", 5)
            .attr("width",node_width)
            .attr("height",node_height)
            .attr("fill","none")
        // var ghostLink = ghostNode.append("svg:path")
        //     .attr("class","red-ui-flow-link-link")
        //     .attr("d","M 0 "+(node_height/2)+" H "+(gridSize * -2))
        //     .attr("opacity",0);

        var filter;
        if (drag_lines.length > 0) {
            if (drag_lines[0].virtualLink) {
                filter = {type:drag_lines[0].node.type === 'link in'?'link out':'link in'}
            } else if (drag_lines[0].portType === PORT_TYPE_OUTPUT) {
                filter = {input:true}
            } else {
                filter = {output:true}
            }

            quickAddLink = {
                node: drag_lines[0].node,
                port: drag_lines[0].port,
                portType: drag_lines[0].portType,
            }
            if (drag_lines[0].virtualLink) {
                quickAddLink.virtualLink = true;
            }
            hideDragLines();
        }
        if (linkToSplice || spliceMultipleLinks) {
            filter = {
                input:true,
                output:true,
                spliceMultiple: spliceMultipleLinks
            }
        }

        var rebuildQuickAddLink = function() {
            if (!quickAddLink) {
                return;
            }
            if (!quickAddLink.el) {
                quickAddLink.el = dragGroupLayer.append("svg:path").attr("class", "red-ui-flow-drag-line");
            }
            var numOutputs = (quickAddLink.portType === PORT_TYPE_OUTPUT)?(quickAddLink.node.outputs || 1):1;
            var sourcePort = quickAddLink.port;
            var portY = -((numOutputs-1)/2)*13 +13*sourcePort;
            var sc = (quickAddLink.portType === PORT_TYPE_OUTPUT)?1:-1;
            quickAddLink.el.attr("d",generateLinkPath(quickAddLink.node.x+sc*quickAddLink.node.w/2,quickAddLink.node.y+portY,point[0]-sc*node_width/2,point[1],sc));
        }
        if (quickAddLink) {
            rebuildQuickAddLink();
        }


        var lastAddedX;
        var lastAddedWidth;

        RED.typeSearch.show({
            x:clientX-mainPos.left-node_width/2 - (ox-point[0]),
            y:clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]),
            disableFocus: touchTrigger,
            filter: filter,
            move: function(dx,dy) {
                if (ghostNode) {
                    var pos = d3.transform(ghostNode.attr("transform")).translate;
                    ghostNode.attr("transform","translate("+(pos[0]+dx)+","+(pos[1]+dy)+")")
                    point[0] += dx;
                    point[1] += dy;
                    rebuildQuickAddLink();
                }
            },
            cancel: function() {
                if (quickAddLink) {
                    if (quickAddLink.el) {
                        quickAddLink.el.remove();
                    }
                    quickAddLink = null;
                }
                quickAddActive = false;
                if (ghostNode) {
                    ghostNode.remove();
                }
                resetMouseVars();
                updateSelection();
                hideDragLines();
                redraw();
            },
            add: function(type, keepAdding) {
                if (touchTrigger) {
                    keepAdding = false;
                    resetMouseVars();
                }

                var nn;
                var historyEvent;
                if (/^_action_:/.test(type)) {
                    const actionName = type.substring(9)
                    quickAddActive = false;
                    ghostNode.remove();
                    RED.actions.invoke(actionName)
                    return
                } else if (type === 'junction') {
                    nn = {
                        _def: {defaults:{}},
                        type: 'junction',
                        z: RED.workspaces.active(),
                        id: RED.nodes.id(),
                        x: 0,
                        y: 0,
                        w: 0, h: 0,
                        outputs: 1,
                        inputs: 1,
                        dirty: true,
                        moved: true
                    }
                    historyEvent = {
                        t:'add',
                        dirty: RED.nodes.dirty(),
                        junctions:[nn]
                    }
                } else {
                    var result = createNode(type);
                    if (!result) {
                        return;
                    }
                    nn = result.node;
                    historyEvent = result.historyEvent;
                }
                if (keepAdding) {
                    mouse_mode = RED.state.QUICK_JOINING;
                }

                nn.x = point[0];
                nn.y = point[1];
                var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label");
                if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) {
                    nn.l = showLabel;
                }
                if (nn.type === 'junction') {
                    nn = RED.nodes.addJunction(nn);
                } else {
                    nn = RED.nodes.add(nn);
                }
                if (quickAddLink) {
                    var drag_line = quickAddLink;
                    var src = null,dst,src_port;
                    if (drag_line.portType === PORT_TYPE_OUTPUT && (nn.inputs > 0 || drag_line.virtualLink) ) {
                        src = drag_line.node;
                        src_port = drag_line.port;
                        dst = nn;
                    } else if (drag_line.portType === PORT_TYPE_INPUT && (nn.outputs > 0 || drag_line.virtualLink)) {
                        src = nn;
                        dst = drag_line.node;
                        src_port = 0;
                    }

                    if (src !== null) {
                        // Joining link nodes via virual wires. Need to update
                        // the src and dst links property
                        if (drag_line.virtualLink) {
                            historyEvent = {
                                t:'multi',
                                events: [historyEvent]
                            }
                            var oldSrcLinks = $.extend(true,{},{v:src.links}).v
                            var oldDstLinks = $.extend(true,{},{v:dst.links}).v
                            src.links.push(dst.id);
                            dst.links.push(src.id);
                            src.dirty = true;
                            dst.dirty = true;

                            historyEvent.events.push({
                                t:'edit',
                                node: src,
                                dirty: RED.nodes.dirty(),
                                changed: src.changed,
                                changes: {
                                    links:oldSrcLinks
                                }
                            });
                            historyEvent.events.push({
                                t:'edit',
                                node: dst,
                                dirty: RED.nodes.dirty(),
                                changed: dst.changed,
                                changes: {
                                    links:oldDstLinks
                                }
                            });
                            src.changed = true;
                            dst.changed = true;
                        } else {
                            var link = {source: src, sourcePort:src_port, target: dst};
                            RED.nodes.addLink(link);
                            historyEvent.links = [link];
                        }
                        if (!keepAdding) {
                            quickAddLink.el.remove();
                            quickAddLink = null;
                            if (mouse_mode === RED.state.QUICK_JOINING) {
                                if (drag_line.portType === PORT_TYPE_OUTPUT && nn.outputs > 0) {
                                    showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]);
                                } else if (!quickAddLink && drag_line.portType === PORT_TYPE_INPUT && nn.inputs > 0) {
                                    showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]);
                                } else {
                                    resetMouseVars();
                                }
                            }
                        } else {
                            quickAddLink.node = nn;
                            quickAddLink.port = 0;
                        }
                    } else {
                        hideDragLines();
                        resetMouseVars();
                    }
                } else {
                    if (!keepAdding) {
                        if (mouse_mode === RED.state.QUICK_JOINING) {
                            if (nn.outputs > 0) {
                                showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]);
                            } else if (nn.inputs > 0) {
                                showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]);
                            } else {
                                resetMouseVars();
                            }
                        }
                    } else {
                        if (nn.outputs > 0) {
                            quickAddLink = {
                                node: nn,
                                port: 0,
                                portType: PORT_TYPE_OUTPUT
                            }
                        } else if (nn.inputs > 0) {
                            quickAddLink = {
                                node: nn,
                                port: 0,
                                portType: PORT_TYPE_INPUT
                            }
                        } else {
                            resetMouseVars();
                        }
                    }
                }

                RED.editor.validateNode(nn);

                if (targetGroup) {
                    var oldX = targetGroup.x; 
                    var oldY = targetGroup.y; 
                    RED.group.addToGroup(targetGroup, nn);
                    var moveEvent = null;
                    if ((targetGroup.x !== oldX) ||
                        (targetGroup.y !== oldY)) {
                        moveEvent = {
                            t: "move",
                            nodes: [{n: targetGroup,
                                     ox: oldX, oy: oldY,
                                     dx: targetGroup.x -oldX,
                                     dy: targetGroup.y -oldY}],
                            dirty: true
                        };
                    }
                    if (historyEvent.t !== "multi") {
                        historyEvent = {
                            t:'multi',
                            events: [historyEvent]
                        };
                    }
                    historyEvent.events.push({
                        t: "addToGroup",
                        group: targetGroup,
                        nodes: nn
                    });
                    if (moveEvent) {
                        historyEvent.events.push(moveEvent);
                    }
                }

                if (linkToSplice) {
                    resetMouseVars();
                    spliceLink(linkToSplice, nn, historyEvent)
                }
                RED.history.push(historyEvent);
                RED.nodes.dirty(true);
                // auto select dropped node - so info shows (if visible)
                clearSelection();
                nn.selected = true;
                movingSet.add(nn);
                updateActiveNodes();
                updateSelection();
                redraw();
                // At this point the newly added node will have a real width,
                // so check if the position needs nudging
                if (lastAddedX !== undefined) {
                    var lastNodeRHEdge = lastAddedX + lastAddedWidth/2;
                    var thisNodeLHEdge = nn.x - nn.w/2;
                    var gap = thisNodeLHEdge - lastNodeRHEdge;
                    if (gap != gridSize *2) {
                        nn.x = nn.x + gridSize * 2 - gap;
                        nn.dirty = true;
                        nn.x = Math.ceil(nn.x / gridSize) * gridSize;
                        redraw();
                    }
                }
                if (keepAdding) {
                    if (lastAddedX === undefined) {
                        // ghostLink.attr("opacity",1);
                        setTimeout(function() {
                            RED.typeSearch.refresh({filter:{input:true}});
                        },100);
                    }

                    lastAddedX = nn.x;
                    lastAddedWidth = nn.w;

                    point[0] = nn.x + nn.w/2 + node_width/2 + gridSize * 2;
                    ghostNode.attr('transform','translate('+(point[0] - node_width/2)+','+(point[1] - node_height/2)+')');
                    rebuildQuickAddLink();
                } else {
                    quickAddActive = false;
                    ghostNode.remove();
                }
            }
        });

        updateActiveNodes();
        updateSelection();
        redraw();
    }

    function canvasMouseMove() {
        var i;
        var node;
        // Prevent touch scrolling...
        //if (d3.touches(this)[0]) {
        //    d3.event.preventDefault();
        //}

        // TODO: auto scroll the container
        //var point = d3.mouse(this);
        //if (point[0]-container.scrollLeft < 30 && container.scrollLeft > 0) { container.scrollLeft -= 15; }
        //console.log(d3.mouse(this),container.offsetWidth,container.offsetHeight,container.scrollLeft,container.scrollTop);

        if (mouse_mode === RED.state.PANNING) {
            var pos = [d3.event.pageX,d3.event.pageY];
            if (d3.event.touches) {
                var touch0 = d3.event.touches.item(0);
                pos = [touch0.pageX, touch0.pageY];
            }
            var deltaPos = [
                mouse_position[0]-pos[0],
                mouse_position[1]-pos[1]
            ];

            chart.scrollLeft(scroll_position[0]+deltaPos[0])
            chart.scrollTop(scroll_position[1]+deltaPos[1])
            return
        }

        mouse_position = d3.touches(this)[0]||d3.mouse(this);

        if (lasso) {
            var ox = parseInt(lasso.attr("ox"));
            var oy = parseInt(lasso.attr("oy"));
            var x = parseInt(lasso.attr("x"));
            var y = parseInt(lasso.attr("y"));
            var w;
            var h;
            if (mouse_position[0] < ox) {
                x = mouse_position[0];
                w = ox-x;
            } else {
                w = mouse_position[0]-x;
            }
            if (mouse_position[1] < oy) {
                y = mouse_position[1];
                h = oy-y;
            } else {
                h = mouse_position[1]-y;
            }
            lasso
                .attr("x",x)
                .attr("y",y)
                .attr("width",w)
                .attr("height",h)
            ;
            return;
        } else if (mouse_mode === RED.state.SLICING || mouse_mode === RED.state.SLICING_JUNCTION) {
            if (slicePath) {
                var delta = Math.max(1,Math.abs(slicePathLast[0]-mouse_position[0]))*Math.max(1,Math.abs(slicePathLast[1]-mouse_position[1]))
                if (delta > 20) {
                    var currentPath = slicePath.attr("d")
                    currentPath += " L"+mouse_position[0]+" "+mouse_position[1]
                    slicePath.attr("d",currentPath);
                    slicePathLast = mouse_position
                }
            }
            return
        }

        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }

        if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && mouse_mode != RED.state.DETACHED_DRAGGING && !mousedown_node && !mousedown_group && selectedLinks.length() === 0) {
            return;
        }

        var mousePos;
        // if (mouse_mode === RED.state.GROUP_RESIZE) {
        //     mousePos = mouse_position;
        //     var nx = mousePos[0] + mousedown_group.dx;
        //     var ny = mousePos[1] + mousedown_group.dy;
        //     switch(mousedown_group.activeHandle) {
        //         case 0: mousedown_group.pos.x0 = nx; mousedown_group.pos.y0 = ny; break;
        //         case 1: mousedown_group.pos.x1 = nx; mousedown_group.pos.y0 = ny; break;
        //         case 2: mousedown_group.pos.x1 = nx; mousedown_group.pos.y1 = ny; break;
        //         case 3: mousedown_group.pos.x0 = nx; mousedown_group.pos.y1 = ny; break;
        //     }
        //     mousedown_group.dirty = true;
        // }
        if (mouse_mode == RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) {
            // update drag line
            if (drag_lines.length === 0 && mousedown_port_type !== null) {
                if (d3.event.shiftKey) {
                    // Get all the wires we need to detach.
                    var links = [];
                    var existingLinks = [];
                    if (selectedLinks.length() > 0) {
                        selectedLinks.forEach(function(link) {
                            if (((mousedown_port_type === PORT_TYPE_OUTPUT &&
                                link.source === mousedown_node &&
                                link.sourcePort === mousedown_port_index
                            ) ||
                            (mousedown_port_type === PORT_TYPE_INPUT &&
                                link.target === mousedown_node
                            ))) {
                                existingLinks.push(link);
                            }
                        })
                    } else {
                        var filter;
                        if (mousedown_port_type === PORT_TYPE_OUTPUT) {
                            filter = {
                                source:mousedown_node,
                                sourcePort: mousedown_port_index
                            }
                        } else {
                            filter = {
                                target: mousedown_node
                            }
                        }
                        existingLinks = RED.nodes.filterLinks(filter);
                    }
                    for (i=0;i<existingLinks.length;i++) {
                        var link = existingLinks[i];
                        RED.nodes.removeLink(link);
                        links.push({
                            link:link,
                            node: (mousedown_port_type===PORT_TYPE_OUTPUT)?link.target:link.source,
                            port: (mousedown_port_type===PORT_TYPE_OUTPUT)?0:link.sourcePort,
                            portType: (mousedown_port_type===PORT_TYPE_OUTPUT)?PORT_TYPE_INPUT:PORT_TYPE_OUTPUT
                        })
                    }
                    if (links.length === 0) {
                        resetMouseVars();
                        redraw();
                    } else {
                        showDragLines(links);
                        mouse_mode = 0;
                        updateActiveNodes();
                        redraw();
                        mouse_mode = RED.state.JOINING;
                    }
                } else if (mousedown_node && !quickAddLink) {
                    showDragLines([{node:mousedown_node,port:mousedown_port_index,portType:mousedown_port_type}]);
                }
                selectedLinks.clear();
            }
            mousePos = mouse_position;
            for (i=0;i<drag_lines.length;i++) {
                var drag_line = drag_lines[i];
                var numOutputs = (drag_line.portType === PORT_TYPE_OUTPUT)?(drag_line.node.outputs || 1):1;
                var sourcePort = drag_line.port;
                var portY = -((numOutputs-1)/2)*13 +13*sourcePort;

                var sc = (drag_line.portType === PORT_TYPE_OUTPUT)?1:-1;
                drag_line.el.attr("d",generateLinkPath(drag_line.node.x+sc*drag_line.node.w/2,drag_line.node.y+portY,mousePos[0],mousePos[1],sc, !!drag_line.node.status));
            }
            d3.event.preventDefault();
        } else if (mouse_mode == RED.state.MOVING) {
            mousePos = d3.mouse(document.body);
            if (isNaN(mousePos[0])) {
                mousePos = d3.touches(document.body)[0];
            }
            var d = (mouse_offset[0]-mousePos[0])*(mouse_offset[0]-mousePos[0]) + (mouse_offset[1]-mousePos[1])*(mouse_offset[1]-mousePos[1]);
            if ((d > 3 && !dblClickPrimed) || (dblClickPrimed && d > 10)) {
                clickElapsed = 0;
                if (!activeFlowLocked) {
                    if (mousedown_node) {
                        movingSet.makePrimary(mousedown_node)
                    }
                    mouse_mode = RED.state.MOVING_ACTIVE;
                    startSelectionMove()
                }
            }
        } else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING || mouse_mode == RED.state.DETACHED_DRAGGING) {
            mousePos = mouse_position;
            var minX = 0;
            var minY = 0;
            var maxX = space_width;
            var maxY = space_height;
            for (var n = 0; n<movingSet.length(); n++) {
                node = movingSet.get(n);
                if (d3.event.shiftKey) {
                    node.n.ox = node.n.x;
                    node.n.oy = node.n.y;
                }
                node.n._detachFromGroup = d3.event.altKey
                node.n.x = mousePos[0]+node.dx;
                node.n.y = mousePos[1]+node.dy;
                node.n.dirty = true;
                if (node.n.type === "group") {
                    if (node.n.groupMoved !== false) {
                        node.n.groupMoved = true;
                    }
                    RED.group.markDirty(node.n);
                    minX = Math.min(node.n.x-5,minX);
                    minY = Math.min(node.n.y-5,minY);
                    maxX = Math.max(node.n.x+node.n.w+5,maxX);
                    maxY = Math.max(node.n.y+node.n.h+5,maxY);
                } else {
                    minX = Math.min(node.n.x-node.n.w/2-5,minX);
                    minY = Math.min(node.n.y-node.n.h/2-5,minY);
                    maxX = Math.max(node.n.x+node.n.w/2+5,maxX);
                    maxY = Math.max(node.n.y+node.n.h/2+5,maxY);
                }
            }
            if (minX !== 0 || minY !== 0) {
                for (i = 0; i<movingSet.length(); i++) {
                    node = movingSet.get(i);
                    node.n.x -= minX;
                    node.n.y -= minY;
                }
            }
            if (maxX !== space_width || maxY !== space_height) {
                for (i = 0; i<movingSet.length(); i++) {
                    node = movingSet.get(i);
                    node.n.x -= (maxX - space_width);
                    node.n.y -= (maxY - space_height);
                }
            }
            // if (mousedown_group) {
            //     mousedown_group.x = mousePos[0] + mousedown_group.dx;
            //     mousedown_group.y = mousePos[1] + mousedown_group.dy;
            //     mousedown_group.dirty = true;
            // }
            var gridOffset = [0,0];
            if (snapGrid != d3.event.shiftKey && movingSet.length() > 0) {
                var i = 0;

                // Prefer to snap nodes to the grid if there is one in the selection
                do {
                    node = movingSet.get(i++);
                } while(i<movingSet.length() && node.n.type === "group")

                if (node.n.type === "group") {
                    // TODO: Group snap to grid
                    gridOffset[0] = node.n.x-(gridSize*Math.floor(node.n.x/gridSize))-gridSize/2;
                    gridOffset[1] = node.n.y-(gridSize*Math.floor(node.n.y/gridSize))-gridSize/2;
                } else {
                    const snapOffsets = RED.view.tools.calculateGridSnapOffsets(node.n);
                    gridOffset[0] = snapOffsets.x;
                    gridOffset[1] = snapOffsets.y;
                }
                if (gridOffset[0] !== 0 || gridOffset[1] !== 0) {
                    for (i = 0; i<movingSet.length(); i++) {
                        node = movingSet.get(i);
                        node.n.x -= gridOffset[0];
                        node.n.y -= gridOffset[1];
                        if (node.n.x == node.n.ox && node.n.y == node.n.oy) {
                            node.dirty = false;
                        }
                    }
                }
            }

            // Check link splice
            if (movingSet.length() === 1 && movingSet.get(0).n.type !== "group") {
                node = movingSet.get(0);
                if (spliceActive) {
                    if (!spliceTimer) {
                        spliceTimer = setTimeout(function() {
                            var nodes = [];
                            var bestDistance = Infinity;
                            var bestLink = null;
                            var mouseX = node.n.x;
                            var mouseY = node.n.y;
                            if (outer[0][0].getIntersectionList) {
                                var svgRect = outer[0][0].createSVGRect();
                                svgRect.x = mouseX*scaleFactor;
                                svgRect.y = mouseY*scaleFactor;
                                svgRect.width = 1;
                                svgRect.height = 1;
                                nodes = outer[0][0].getIntersectionList(svgRect, outer[0][0]);
                            } else {
                                // Firefox doesn"t do getIntersectionList and that
                                // makes us sad
                                nodes = RED.view.getLinksAtPoint(mouseX*scaleFactor,mouseY*scaleFactor);
                            }
                            for (var i=0;i<nodes.length;i++) {
                                if (d3.select(nodes[i]).classed("red-ui-flow-link-background")) {
                                    var length = nodes[i].getTotalLength();
                                    for (var j=0;j<length;j+=10) {
                                        var p = nodes[i].getPointAtLength(j);
                                        var d2 = ((p.x-mouseX)*(p.x-mouseX))+((p.y-mouseY)*(p.y-mouseY));
                                        if (d2 < 200 && d2 < bestDistance) {
                                            bestDistance = d2;
                                            bestLink = nodes[i];
                                        }
                                    }
                                }
                            }
                            if (activeSpliceLink && activeSpliceLink !== bestLink) {
                                d3.select(activeSpliceLink.parentNode).classed("red-ui-flow-link-splice",false);
                            }
                            if (bestLink) {
                                d3.select(bestLink.parentNode).classed("red-ui-flow-link-splice",true)
                            } else {
                                d3.select(".red-ui-flow-link-splice").classed("red-ui-flow-link-splice",false);
                            }
                            activeSpliceLink = bestLink;
                            spliceTimer = null;
                        },100);
                    }
                }
            }
            // Check merge into group
            if (groupAddActive) {
                if (!groupHoverTimer) {
                    const isDetachFromGroup = d3.event.altKey
                    groupHoverTimer = setTimeout(function() {
                        node = movingSet.get(0);
                        const hoveredGroup = getGroupAt(mousePos[0],mousePos[1], true);
                        if (hoveredGroup !== activeHoverGroup) {
                            if (activeHoverGroup) {
                                activeHoverGroup.hovered = false
                                activeHoverGroup.dirty = true
                            }
                            activeHoverGroup = hoveredGroup
                        }
                        if (activeHoverGroup && groupAddParentGroup && !isDetachFromGroup) {
                            if (groupAddParentGroup === activeHoverGroup.id) {
                                activeHoverGroup = null
                            } else {
                                const nodeGroup = RED.nodes.group(groupAddParentGroup)
                                // This node is already in a group. It should only be draggable
                                // into a group that is a child of the group its in
                                if (!RED.group.contains(nodeGroup, activeHoverGroup)) {
                                    activeHoverGroup = null
                                }
                            }
                        }
                        if (activeHoverGroup) {
                            activeHoverGroup.hovered = true
                            activeHoverGroup.dirty = true
                        }
                        groupHoverTimer = null;
                    }, 50);
                }
            }


        }
        if (mouse_mode !== 0) {
            redraw();
        }
    }
    function canvasMouseLeave() {
        if (mouse_mode !== 0 && d3.event.buttons !== 0) {
            d3.select(document).on('mouseup.red-ui-workspace-tracker', function() {
                d3.select(document).on('mouseup.red-ui-workspace-tracker', null)
                canvasMouseUp.call(this)
            })
        }
    }
    function canvasMouseUp() {
        lastClickPosition = [d3.event.offsetX/scaleFactor,d3.event.offsetY/scaleFactor];
        if (RED.view.DEBUG) {
            console.warn("canvasMouseUp", { mouse_mode, point: d3.mouse(this), event: d3.event });
        }
        var i;
        var historyEvent;
        if (d3.event.button === 2) {
            return
        }
        if (mouse_mode === RED.state.PANNING) {
            resetMouseVars();
            return
        }
        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }
        if (mouse_mode === RED.state.QUICK_JOINING) {
            return;
        }
        if (mousedown_node && mouse_mode == RED.state.JOINING) {
            var removedLinks = [];
            for (i=0;i<drag_lines.length;i++) {
                if (drag_lines[i].link) {
                    removedLinks.push(drag_lines[i].link)
                }
            }
            if (removedLinks.length > 0) {
                historyEvent = {
                    t:"delete",
                    links: removedLinks,
                    dirty:RED.nodes.dirty()
                };
                RED.history.push(historyEvent);
                RED.nodes.dirty(true);
            } else {
                // Trigger quick add dialog
                d3.event.stopPropagation();
                clearSelection();
                const point = d3.mouse(this);
                var clickedGroup = getGroupAt(point[0], point[1]);
                if (drag_lines.length > 0) {
                    clickedGroup = clickedGroup || RED.nodes.group(drag_lines[0].node.g)
                }
                showQuickAddDialog({ position: point, group: clickedGroup });
            }
            hideDragLines();
        }
        if (lasso) {
            var x = parseInt(lasso.attr("x"));
            var y = parseInt(lasso.attr("y"));
            var x2 = x+parseInt(lasso.attr("width"));
            var y2 = y+parseInt(lasso.attr("height"));
            if (!d3.event.shiftKey) {
                clearSelection();
            }

            activeGroups.forEach(function(n) {
                if (!movingSet.has(n) && !n.selected) {
                    // group entirely within lasso
                    if (n.x > x && n.y > y && n.x + n.w < x2 && n.y + n.h < y2) {
                        selectedGroups.add(n, true)
                    }
                }
            })
            activeNodes.forEach(function(n) {
                if (!movingSet.has(n) && !n.selected) {
                    if (n.x > x && n.x < x2 && n.y > y && n.y < y2) {
                        n.selected = true;
                        n.dirty = true;
                        movingSet.add(n);
                    }
                }
            });
            activeJunctions.forEach(function(n) {
                if (!movingSet.has(n) && !n.selected) {
                    if (n.x > x && n.x < x2 && n.y > y && n.y < y2) {
                        n.selected = true;
                        n.dirty = true;
                        movingSet.add(n);
                    }
                }
            })
            activeLinks.forEach(function(link) {
                if (!link.selected) {
                    var sourceY = link.source.y
                    var targetY = link.target.y
                    var sourceX = link.source.x+(link.source.w/2) + 10
                    var targetX = link.target.x-(link.target.w/2) - 10
                    if (
                        sourceX > x && sourceX < x2 && sourceY > y && sourceY < y2 &&
                        targetX > x && targetX < x2 && targetY > y && targetY < y2
                    ) {
                        selectedLinks.add(link);
                    }
                }
            })

            if (activeSubflow) {
                activeSubflow.in.forEach(function(n) {
                    n.selected = (n.x > x && n.x < x2 && n.y > y && n.y < y2);
                    if (n.selected) {
                        n.dirty = true;
                        movingSet.add(n);
                    }
                });
                activeSubflow.out.forEach(function(n) {
                    n.selected = (n.x > x && n.x < x2 && n.y > y && n.y < y2);
                    if (n.selected) {
                        n.dirty = true;
                        movingSet.add(n);
                    }
                });
                if (activeSubflow.status) {
                    activeSubflow.status.selected = (activeSubflow.status.x > x && activeSubflow.status.x < x2 && activeSubflow.status.y > y && activeSubflow.status.y < y2);
                    if (activeSubflow.status.selected) {
                        activeSubflow.status.dirty = true;
                        movingSet.add(activeSubflow.status);
                    }
                }
            }
            updateSelection();
            outer.classed('red-ui-workspace-lasso-active', false)
            lasso.remove();
            lasso = null;
        } else if (mouse_mode == RED.state.DEFAULT && mousedown_link == null && !d3.event.ctrlKey && !d3.event.metaKey ) {
            clearSelection();
            updateSelection();
        } else if (mouse_mode == RED.state.SLICING) {
            deleteSelection();
            slicePath.remove();
            slicePath = null;
            RED.view.redraw(true);
        } else if (mouse_mode == RED.state.SLICING_JUNCTION) {
            RED.actions.invoke("core:split-wires-with-junctions")
            slicePath.remove();
            slicePath = null;
        }
        if (mouse_mode == RED.state.MOVING_ACTIVE) {
            if (movingSet.length() > 0) {
                historyEvent = { t: 'multi', events: [] }

                // Check to see if we're dropping into a group
                const {
                    addedToGroup,
                    removedFromGroup,
                    groupMoveEvent,
                    rehomedNodes
                } = addMovingSetToGroup()

                if (groupMoveEvent) {
                    historyEvent.events.push(groupMoveEvent)
                }

                // Create two lists of nodes:
                //  - nodes that have moved without changing group
                //  - nodes that have moved AND changed group
                const moveEvent = {
                    t: 'move',
                    nodes: [],
                    dirty: RED.nodes.dirty()
                }
                const moveAndChangedGroupEvent = {
                    t: 'move',
                    nodes: [],
                    dirty: RED.nodes.dirty(),
                    addToGroup: addedToGroup,
                    removeFromGroup: removedFromGroup
                }
                for (let j = 0; j < movingSet.length(); j++) {
                    const n = movingSet.get(j);
                    delete n.n._detachFromGroup
                    if (n.ox !== n.n.x || n.oy !== n.n.y || addedToGroup) {
                        // This node has moved or added to a group
                        if (rehomedNodes.has(n)) {
                            moveAndChangedGroupEvent.nodes.push({...n})
                        } else {
                            moveEvent.nodes.push({...n})
                        }
                        n.n.dirty = true;
                        n.n.moved = true;
                    }
                }
                // If a node has moved and ends up being spliced into a link, keep
                // track of which historyEvent to add the splice info to
                let targetSpliceEvent = null
                if (moveEvent.nodes.length > 0) {
                    historyEvent.events.push(moveEvent)
                    targetSpliceEvent = moveEvent
                }
                if (moveAndChangedGroupEvent.nodes.length > 0) {
                    historyEvent.events.push(moveAndChangedGroupEvent)
                    targetSpliceEvent = moveAndChangedGroupEvent
                }
                // activeSpliceLink will only be set if the movingSet has a single
                // node that is able to splice.
                if (targetSpliceEvent && activeSpliceLink) {
                    var linkToSplice = d3.select(activeSpliceLink).data()[0];
                    spliceLink(linkToSplice, movingSet.get(0).n, targetSpliceEvent)
                }

                // Only continue if something has moved
                if (historyEvent.events.length > 0) {
                    RED.nodes.dirty(true);
                    if (historyEvent.events.length === 1) {
                        // Keep history tidy - no need for multi-event
                        RED.history.push(historyEvent.events[0]);
                    } else {
                        // Multiple events - push the whole lot as one
                        RED.history.push(historyEvent);
                    }
                    updateActiveNodes();
                }
            }
        }
        if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.DETACHED_DRAGGING) {
            if (mouse_mode === RED.state.DETACHED_DRAGGING) {
                var ns = [];
                for (var j=0;j<movingSet.length();j++) {
                    var n = movingSet.get(j);
                    if (n.ox !== n.n.x || n.oy !== n.n.y) {
                        ns.push({n:n.n,ox:n.ox,oy:n.oy,moved:n.n.moved});
                        n.n.dirty = true;
                        n.n.moved = true;
                    }
                }
                var detachEvent = RED.history.peek();
                // The last event in the stack *should* be a multi-event from
                // where the links were added/removed
                var historyEvent = {t:"move",nodes:ns,dirty:RED.nodes.dirty()}
                if (detachEvent.t === "multi") {
                    detachEvent.events.push(historyEvent)
                } else {
                    RED.history.push(historyEvent)
                }
            }
            for (i=0;i<movingSet.length();i++) {
                var node = movingSet.get(i);
                delete node.ox;
                delete node.oy;
            }
        }
        if (mouse_mode == RED.state.IMPORT_DRAGGING) {
            if (clipboardSource === 'cut') {
                clipboardSource = 'copy'
            }
            updateActiveNodes();
            RED.nodes.dirty(true);
        }
        resetMouseVars();
        redraw();
    }


    function spliceLink(link, node, historyEvent) {
        RED.nodes.removeLink(link);
        const link1 = {
            source: link.source,
            sourcePort: link.sourcePort,
            target: node
        };
        const link2 = {
            source: node,
            sourcePort: 0,
            target: link.target
        };
        RED.nodes.addLink(link1);
        RED.nodes.addLink(link2);

        historyEvent.links = (historyEvent.links || []).concat([link1,link2]);
        historyEvent.removedLinks = [link];
    }

    function addMovingSetToGroup() {

        const isDetachFromGroup = groupAddParentGroup && d3.event.altKey

        let addedToGroup = null;
        let removedFromGroup = null;
        let groupMoveEvent = null;
        let rehomedNodes = new Set()

        if (activeHoverGroup) {
            // Nodes are being dropped into a group. We have to assume at
            // this point that everything in the movingSet is valid for adding
            // to this group. But it could be a mix of nodes and existing groups.
            // In which case, we don't want to rehome all of the nodes inside
            // existing groups - we just want to rehome the top level objects.
            var oldX = activeHoverGroup.x; 
            var oldY = activeHoverGroup.y; 
            if (groupAddParentGroup) {
                removedFromGroup = RED.nodes.group(groupAddParentGroup)
            }
            // Second pass - now we know what to move, we can move it
            for (let j=0;j<movingSet.length();j++) {
                const n = movingSet.get(j)
                if (!n.n.g || (removedFromGroup && n.n.g === removedFromGroup.id)) {
                    rehomedNodes.add(n)  
                    RED.group.addToGroup(activeHoverGroup, n.n);
                }
            }
            if ((activeHoverGroup.x !== oldX) ||
                (activeHoverGroup.y !== oldY)) {
                groupMoveEvent = {
                    t: "move",
                    nodes: [{n: activeHoverGroup,
                             ox: oldX, oy: oldY,
                             dx: activeHoverGroup.x -oldX,
                             dy: activeHoverGroup.y -oldY}],
                    dirty: true
                };
            }
            addedToGroup = activeHoverGroup;
            activeHoverGroup.hovered = false;
            activeHoverGroup = null;
        } else if (isDetachFromGroup) {
            // The nodes are being removed from their group
            removedFromGroup = RED.nodes.group(groupAddParentGroup)
            for (let j=0;j<movingSet.length();j++) {
                const n = movingSet.get(j)
                if (n.n.g && n.n.g === removedFromGroup.id) {
                    rehomedNodes.add(n)  
                    RED.group.removeFromGroup(removedFromGroup, n.n);
                }
            }
        }
        activeGroups.forEach(g => {
            if (g.hovered) {
                g.hovered = false
                g.dirty = true
            }
        })

        return {
            addedToGroup,
            removedFromGroup,
            groupMoveEvent,
            rehomedNodes
        }

    }

    function zoomIn() {
        if (scaleFactor < 2) {
            zoomView(scaleFactor+0.1);
        }
    }
    function zoomOut() {
        if (scaleFactor > 0.3) {
            zoomView(scaleFactor-0.1);
        }
    }
    function zoomZero() { zoomView(1); }
    function searchFlows() { RED.actions.invoke("core:search", $(this).data("term")); }
    function searchPrev() { RED.actions.invoke("core:search-previous"); }
    function searchNext() { RED.actions.invoke("core:search-next"); }


    function zoomView(factor) {
        var screenSize = [chart.width(),chart.height()];
        var scrollPos = [chart.scrollLeft(),chart.scrollTop()];
        var center = [(scrollPos[0] + screenSize[0]/2)/scaleFactor,(scrollPos[1] + screenSize[1]/2)/scaleFactor];
        scaleFactor = factor;
        var newCenter = [(scrollPos[0] + screenSize[0]/2)/scaleFactor,(scrollPos[1] + screenSize[1]/2)/scaleFactor];
        var delta = [(newCenter[0]-center[0])*scaleFactor,(newCenter[1]-center[1])*scaleFactor]
        chart.scrollLeft(scrollPos[0]-delta[0]);
        chart.scrollTop(scrollPos[1]-delta[1]);

        RED.view.navigator.resize();
        redraw();
        if (RED.settings.get("editor.view.view-store-zoom")) {
            RED.settings.setLocal('zoom-level', factor.toFixed(1))
        }
    }

    function selectNone() {
        if (mouse_mode === RED.state.MOVING || mouse_mode === RED.state.MOVING_ACTIVE) {
            return;
        }
        if (mouse_mode === RED.state.DETACHED_DRAGGING) {
            for (var j=0;j<movingSet.length();j++) {
                var n = movingSet.get(j);
                n.n.x = n.ox;
                n.n.y = n.oy;
            }
            clearSelection();
            RED.history.pop();
            mouse_mode = 0;
        } else if (mouse_mode === RED.state.IMPORT_DRAGGING) {
            clearSelection();
            RED.history.pop();
            mouse_mode = 0;
        } else if (mouse_mode === RED.state.SLICING || mouse_mode === RED.state.SLICING_JUNCTION) {
            if (slicePath) {
                slicePath.remove();
                slicePath = null;
                resetMouseVars()
            }
            clearSelection();
        } else if (lasso) {
            outer.classed('red-ui-workspace-lasso-active', false)
            lasso.remove();
            lasso = null;
        } else {
            clearSelection();
        }
        redraw();
    }
    function selectAll() {
        if (mouse_mode === RED.state.SELECTING_NODE && selectNodesOptions.single) {
            return;
        }
        selectedLinks.clear();
        clearSelection();
        activeGroups.forEach(function(g) {
            if (!g.g) {
                selectedGroups.add(g, true);
                if (!g.selected) {
                    g.selected = true;
                    g.dirty = true;
                }
            } else {
                g.selected = false;
                g.dirty = true;
            }
        })

        activeNodes.forEach(function(n) {
            if (mouse_mode === RED.state.SELECTING_NODE) {
                if (selectNodesOptions.filter && !selectNodesOptions.filter(n)) {
                    return;
                }
            }
            if (!n.g && !n.selected) {
                n.selected = true;
                n.dirty = true;
                movingSet.add(n);
            }
        });

        activeJunctions.forEach(function(n) {
            if (!n.selected) {
                n.selected = true;
                n.dirty = true;
                movingSet.add(n);
            }
        })

        if (mouse_mode !== RED.state.SELECTING_NODE && activeSubflow) {
            activeSubflow.in.forEach(function(n) {
                if (!n.selected) {
                    n.selected = true;
                    n.dirty = true;
                    movingSet.add(n);
                }
            });
            activeSubflow.out.forEach(function(n) {
                if (!n.selected) {
                    n.selected = true;
                    n.dirty = true;
                    movingSet.add(n);
                }
            });
            if (activeSubflow.status) {
                if (!activeSubflow.status.selected) {
                    activeSubflow.status.selected = true;
                    activeSubflow.status.dirty = true;
                    movingSet.add(activeSubflow.status);
                }
            }
        }
        if (mouse_mode !== RED.state.SELECTING_NODE) {
            updateSelection();
        }
        redraw();
    }

    function clearSelection() {
        if (RED.view.DEBUG) { console.warn("clearSelection", mouse_mode,"movingSet.length():",movingSet.length()); }
        for (var i=0;i<movingSet.length();i++) {
            var n = movingSet.get(i);
            n.n.dirty = true;
            n.n.selected = false;
        }
        movingSet.clear();
        selectedLinks.clear();
        selectedGroups.clear();
    }

    var lastSelection = null;
    function updateSelection() {
        var selection = {};
        var activeWorkspace = RED.workspaces.active();
        var workspaceSelection = RED.workspaces.selection();
        if (activeWorkspace !== 0) {
            if (workspaceSelection.length === 0) {
                selection = getSelection();
                activeLinks = RED.nodes.filterLinks({
                    source:{z:activeWorkspace},
                    target:{z:activeWorkspace}
                });
                var tabOrder = RED.nodes.getWorkspaceOrder();
                var currentLinks = activeLinks;
                var addedLinkLinks = {};
                activeFlowLinks = [];
                var activeLinkNodeIds = Object.keys(activeLinkNodes);
                activeLinkNodeIds.forEach(function(n) {
                    activeLinkNodes[n].dirty = true;
                })
                activeLinkNodes = {};
                for (var i=0;i<movingSet.length();i++) {
                    var msn = movingSet.get(i);
                    if (((msn.n.type === "link out" && msn.n.mode !== 'return') || msn.n.type === "link in") &&
                        (msn.n.z === activeWorkspace)) {
                        var linkNode = msn.n;
                        activeLinkNodes[linkNode.id] = linkNode;
                        var offFlowLinks = {};
                        linkNode.links.forEach(function(id) {
                            var target = RED.nodes.node(id);
                            if (target) {
                                if (linkNode.type === "link out") {
                                    if (target.z === linkNode.z) {
                                        if (!addedLinkLinks[linkNode.id+":"+target.id]) {
                                            activeLinks.push({
                                                source:linkNode,
                                                sourcePort:0,
                                                target: target,
                                                link: true
                                            });
                                            addedLinkLinks[linkNode.id+":"+target.id] = true;
                                            activeLinkNodes[target.id] = target;
                                            target.dirty = true;

                                        }
                                    } else {
                                        offFlowLinks[target.z] = offFlowLinks[target.z]||[];
                                        offFlowLinks[target.z].push(target);
                                    }
                                } else {
                                    if (target.z === linkNode.z) {
                                        if (!addedLinkLinks[target.id+":"+linkNode.id]) {
                                            activeLinks.push({
                                                source:target,
                                                sourcePort:0,
                                                target: linkNode,
                                                link: true
                                            });
                                            addedLinkLinks[target.id+":"+linkNode.id] = true;
                                            activeLinkNodes[target.id] = target;
                                            target.dirty = true;
                                        }
                                    } else {
                                        offFlowLinks[target.z] = offFlowLinks[target.z]||[];
                                        offFlowLinks[target.z].push(target);
                                    }
                                }
                            }
                        });
                        var offFlows = Object.keys(offFlowLinks);
                        // offFlows.sort(function(A,B) {
                        //     return tabOrder.indexOf(A) - tabOrder.indexOf(B);
                        // });
                        if (offFlows.length > 0) {
                            activeFlowLinks.push({
                                refresh: Math.floor(Math.random()*10000),
                                node: linkNode,
                                links: offFlowLinks//offFlows.map(function(i) { return {id:i,links:offFlowLinks[i]};})
                            });
                        }
                    }
                }
                if (activeFlowLinks.length === 0 && selectedLinks.length() > 0) {
                    selectedLinks.forEach(function(link) {
                        if (link.link) {
                            activeLinks.push(link);
                            activeLinkNodes[link.source.id] = link.source;
                            link.source.dirty = true;
                            activeLinkNodes[link.target.id] = link.target;
                            link.target.dirty = true;
                        }
                    })
                }
            } else {
                selection.flows = workspaceSelection;
            }
        }
        var selectionJSON = activeWorkspace+":"+JSON.stringify(selection,function(key,value) {
            if (key === 'nodes' || key === 'flows') {
                return value.map(function(n) { return n.id })
            } else if (key === 'link') {
                return value.source.id+":"+value.sourcePort+":"+value.target.id;
            } else if (key === 'links') {
                return value.map(function(link) {
                    return link.source.id+":"+link.sourcePort+":"+link.target.id;
                });
            }
            return value;
        });
        if (selectionJSON !== lastSelection) {
            lastSelection = selectionJSON;
            RED.events.emit("view:selection-changed",selection);
        }
    }

    function editSelection() {
        if (RED.workspaces.isLocked()) { return }
        if (movingSet.length() > 0) {
            var node = movingSet.get(0).n;
            if (node.type === "subflow") {
                RED.editor.editSubflow(activeSubflow);
            } else if (node.type === "group") {
                RED.editor.editGroup(node);
            } else {
                RED.editor.edit(node);
            }
        }
    }
    function deleteSelection(reconnectWires) {
        if (mouse_mode === RED.state.SELECTING_NODE) {
            return;
        }
        if (activeFlowLocked) {
            return
        }
        if (portLabelHover) {
            portLabelHover.remove();
            portLabelHover = null;
        }
        var workspaceSelection = RED.workspaces.selection();
        if (workspaceSelection.length > 0) {
            var workspaceCount = 0;
            workspaceSelection.forEach(function(ws) { if (ws.type === 'tab') { workspaceCount++ } });
            if (workspaceCount === RED.workspaces.count()) {
                // Cannot delete all workspaces
                return;
            }
            var historyEvent = {
                t: 'delete',
                dirty: RED.nodes.dirty(),
                nodes: [],
                links: [],
                groups: [],
                junctions: [],
                workspaces: [],
                subflows: []
            }
            var workspaceOrder = RED.nodes.getWorkspaceOrder().slice(0);

            for (var i=0;i<workspaceSelection.length;i++) {
                var ws = workspaceSelection[i];
                ws._index = workspaceOrder.indexOf(ws.id);
                RED.workspaces.remove(ws);
                var subEvent;
                if (ws.type === 'tab') {
                    historyEvent.workspaces.push(ws);
                    subEvent = RED.nodes.removeWorkspace(ws.id);
                } else {
                    subEvent = RED.subflow.removeSubflow(ws.id);
                    historyEvent.subflows = historyEvent.subflows.concat(subEvent.subflows);
                }
                historyEvent.nodes = historyEvent.nodes.concat(subEvent.nodes);
                historyEvent.links = historyEvent.links.concat(subEvent.links);
                historyEvent.groups = historyEvent.groups.concat(subEvent.groups);
                historyEvent.junctions = historyEvent.junctions.concat(subEvent.junctions);
            }
            RED.history.push(historyEvent);
            RED.nodes.dirty(true);
            updateActiveNodes();
            updateSelection();
            redraw();
        } else if (movingSet.length() > 0 || selectedLinks.length() > 0) {
            var result;
            var node;
            var removedNodes = [];
            var removedLinks = [];
            var removedGroups = [];
            var removedJunctions = [];
            var removedSubflowOutputs = [];
            var removedSubflowInputs = [];
            var removedSubflowStatus;
            var subflowInstances = [];
            var historyEvents = [];
            var addToRemovedLinks = function(links) {
                if(!links) { return; }
                var _links = Array.isArray(links) ? links : [links];
                _links.forEach(function(l) {
                    removedLinks.push(l);
                    selectedLinks.remove(l);
                })
            }
            if (reconnectWires) {
                var reconnectResult = RED.nodes.detachNodes(movingSet.nodes())
                var addedLinks = reconnectResult.newLinks;
                if (addedLinks.length > 0) {
                    historyEvents.push({ t:'add', links: addedLinks })
                }
                addToRemovedLinks(reconnectResult.removedLinks)
            }

            const startDirty = RED.nodes.dirty();
            let movingSelectedGroups = [];
            if (movingSet.length() > 0) {

                for (var i=0;i<movingSet.length();i++) {
                    node = movingSet.get(i).n;
                    if (node.type === "group") {
                        movingSelectedGroups.push(node);
                    }
                }
                // Make sure we have identified all groups about to be deleted
                for (i=0;i<movingSelectedGroups.length;i++) {
                    movingSelectedGroups[i].nodes.forEach(function(n) {
                        if (n.type === "group" && movingSelectedGroups.indexOf(n) === -1) {
                            movingSelectedGroups.push(n);
                        }
                    })
                }
                for (var i=0;i<movingSet.length();i++) {
                    node = movingSet.get(i).n;
                    node.selected = false;
                    if (node.type !== "group" && node.type !== "subflow" && node.type !== 'junction') {
                        if (node.x < 0) {
                            node.x = 25
                        }
                        var removedEntities = RED.nodes.remove(node.id);
                        removedNodes.push(node);
                        removedNodes = removedNodes.concat(removedEntities.nodes);
                        addToRemovedLinks(removedEntities.links);
                        if (node.g) {
                            var group = RED.nodes.group(node.g);
                            if (movingSelectedGroups.indexOf(group) === -1) {
                                // Don't use RED.group.removeFromGroup as that emits
                                // a change event on the node - but we're deleting it
                                var index = group.nodes.indexOf(node);
                                group.nodes.splice(index,1);
                                RED.group.markDirty(group);
                            }
                        }
                    } else if (node.type === 'junction') {
                        var result = RED.nodes.removeJunction(node)
                        removedJunctions.push(node);
                        removedLinks = removedLinks.concat(result.links);
                        if (node.g) {
                            var group = RED.nodes.group(node.g);
                            if (movingSelectedGroups.indexOf(group) === -1) {
                                // Don't use RED.group.removeFromGroup as that emits
                                // a change event on the node - but we're deleting it
                                var index = group.nodes.indexOf(node);
                                group.nodes.splice(index,1);
                                RED.group.markDirty(group);
                            }
                        }
                    } else {
                        if (node.direction === "out") {
                            removedSubflowOutputs.push(node);
                        } else if (node.direction === "in") {
                            removedSubflowInputs.push(node);
                        } else if (node.direction === "status") {
                            removedSubflowStatus = node;
                        }
                        node.dirty = true;
                    }
                }

                // Groups must be removed in the right order - from inner-most
                // to outermost.
                for (i = movingSelectedGroups.length-1; i>=0; i--) {
                    var g = movingSelectedGroups[i];
                    removedGroups.push(g);
                    RED.nodes.removeGroup(g);
                }
                if (removedSubflowOutputs.length > 0) {
                    result = RED.subflow.removeOutput(removedSubflowOutputs);
                    if (result) {
                        addToRemovedLinks(result.links);
                    }
                }
                // Assume 0/1 inputs
                if (removedSubflowInputs.length == 1) {
                    result = RED.subflow.removeInput();
                    if (result) {
                        addToRemovedLinks(result.links);
                    }
                }
                if (removedSubflowStatus) {
                    result = RED.subflow.removeStatus();
                    if (result) {
                        addToRemovedLinks(result.links);
                    }
                }

                var instances = RED.subflow.refresh(true);
                if (instances) {
                    subflowInstances = instances.instances;
                }
                movingSet.clear();
                if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0 || removedSubflowStatus || removedGroups.length > 0 || removedJunctions.length > 0) {
                    RED.nodes.dirty(true);
                }
            }

            if (selectedLinks.length() > 0) {
                selectedLinks.forEach(function(link) {
                    if (link.link) {
                        var sourceId = link.source.id;
                        var targetId = link.target.id;
                        var sourceIdIndex = link.target.links.indexOf(sourceId);
                        var targetIdIndex = link.source.links.indexOf(targetId);
                        historyEvents.push({
                            t: "edit",
                            node: link.source,
                            changed: link.source.changed,
                            changes: {
                                links: $.extend(true,{},{v:link.source.links}).v
                            }
                        })
                        historyEvents.push({
                            t: "edit",
                            node: link.target,
                            changed: link.target.changed,
                            changes: {
                                links: $.extend(true,{},{v:link.target.links}).v
                            }
                        })
                        link.source.changed = true;
                        link.target.changed = true;
                        link.target.links.splice(sourceIdIndex,1);
                        link.source.links.splice(targetIdIndex,1);
                        link.source.dirty = true;
                        link.target.dirty = true;

                    } else {
                        RED.nodes.removeLink(link);
                        removedLinks.push(link);
                    }
                })
            }
            RED.nodes.dirty(true);
            var historyEvent = {
                t:"delete",
                nodes:removedNodes,
                links:removedLinks,
                groups: removedGroups,
                junctions: removedJunctions,
                subflowOutputs:removedSubflowOutputs,
                subflowInputs:removedSubflowInputs,
                subflow: {
                    id: activeSubflow?activeSubflow.id:undefined,
                    instances: subflowInstances
                },
                dirty:startDirty
            };
            if (removedSubflowStatus) {
                historyEvent.subflow.status = removedSubflowStatus;
            }
            if (historyEvents.length > 0) {
                historyEvents.unshift(historyEvent);
                RED.history.push({
                    t:"multi",
                    events: historyEvents
                })
            } else {
                RED.history.push(historyEvent);
            }

            selectedLinks.clear();
            updateActiveNodes();
            updateSelection();
            redraw();
        }
    }

    function copySelection(isCut) {
        if (mouse_mode === RED.state.SELECTING_NODE) {
            return;
        }
        var nodes = [];
        var selection = RED.workspaces.selection();
        if (selection.length > 0) {
            nodes = [];
            selection.forEach(function(n) {
                if (n.type === 'tab') {
                    nodes.push(n);
                    nodes = nodes.concat(RED.nodes.groups(n.id));
                    nodes = nodes.concat(RED.nodes.filterNodes({z:n.id}));
                }
            });
        } else {
            selection = RED.view.selection();
            if (selection.nodes) {
                selection.nodes.forEach(function(n) {
                    nodes.push(n);
                    if (n.type === 'group') {
                        nodes = nodes.concat(RED.group.getNodes(n,true));
                    }
                })
            }
        }

        if (nodes.length > 0) {
            var nns = [];
            var nodeCount = 0;
            var groupCount = 0;
            var junctionCount = 0;
            var handled = {};
            for (var n=0;n<nodes.length;n++) {
                var node = nodes[n];
                if (handled[node.id]) {
                    continue;
                }
                handled[node.id] = true;
                // The only time a node.type == subflow can be selected is the
                // input/output "proxy" nodes. They cannot be copied.
                if (node.type != "subflow") {
                    if (node.type === "group") {
                        groupCount++;
                    } else if (node.type === 'junction') {
                        junctionCount++;
                    } else {
                        nodeCount++;
                    }
                    for (var d in node._def.defaults) {
                        if (node._def.defaults.hasOwnProperty(d)) {
                            if (node._def.defaults[d].type) {
                                var configNode = RED.nodes.node(node[d]);
                                if (configNode && configNode._def.exclusive) {
                                    nns.push(RED.nodes.convertNode(configNode));
                                }
                            }
                        }
                    }
                    nns.push(RED.nodes.convertNode(node));
                    //TODO: if the node has an exclusive config node, it should also be copied, to ensure it remains exclusive...
                }
            }
            clipboard = JSON.stringify(nns);
            clipboardSource = isCut ? 'cut' : 'copy'
            RED.menu.setDisabled("menu-item-edit-paste", false);
            if (nodeCount > 0) {
                RED.notify(RED._("clipboard.nodeCopied",{count:nodeCount}),{id:"clipboard"});
            } else if (groupCount > 0) {
                RED.notify(RED._("clipboard.groupCopied",{count:groupCount}),{id:"clipboard"});
            }
        }
    }


    function detachSelectedNodes() {
        if (RED.workspaces.isLocked()) { return }
        var selection = RED.view.selection();
        if (selection.nodes) {
            const {newLinks, removedLinks} = RED.nodes.detachNodes(selection.nodes);
            if (removedLinks.length || newLinks.length) {
                RED.history.push({
                    t: "multi",
                    events: [
                        { t:'delete', links: removedLinks },
                        { t:'add', links: newLinks }
                    ],
                    dirty: RED.nodes.dirty()
                })
                RED.nodes.dirty(true)
            }
            prepareDrag([selection.nodes[0].x,selection.nodes[0].y]);
            mouse_mode = RED.state.DETACHED_DRAGGING;
            RED.view.redraw(true);
        }
    }

    function calculateTextWidth(str, className) {
        var result = convertLineBreakCharacter(str);
        var width = 0;
        for (var i=0;i<result.length;i++) {
            var calculateTextW=calculateTextDimensions(result[i],className)[0];
            if (width<calculateTextW) {
                width=calculateTextW;
            }
        }
        return width;
    }
    function getLabelParts(str, className) {
        var lines = convertLineBreakCharacter(str);
        var width = 0;
        for (var i=0;i<lines.length;i++) {
            var calculateTextW = calculateTextDimensions(lines[i],className)[0];
            if (width<calculateTextW) {
                width=calculateTextW;
            }
        }
        return {
            lines:lines,
            width: width
        }
    }

    var textDimensionPlaceholder = {};
    var textDimensionCache = {};
    function calculateTextDimensions(str,className) {
        var cacheKey = "!"+str;
        if (!textDimensionPlaceholder[className]) {
            textDimensionPlaceholder[className] = document.createElement("span");
            textDimensionPlaceholder[className].className = className;
            textDimensionPlaceholder[className].style.position = "absolute";
            textDimensionPlaceholder[className].style.top = "-1000px";
            document.getElementById("red-ui-editor").appendChild(textDimensionPlaceholder[className]);
            textDimensionCache[className] = {};
        } else {
            if (textDimensionCache[className][cacheKey]) {
                return textDimensionCache[className][cacheKey]
            }
        }
        textDimensionPlaceholder[className].textContent = (str||"");
        var w = textDimensionPlaceholder[className].offsetWidth;
        var h = textDimensionPlaceholder[className].offsetHeight;
        textDimensionCache[className][cacheKey] = [w,h];
        return textDimensionCache[className][cacheKey];
    }

    function convertLineBreakCharacter(str) {
        var result = [];
        var lines = str.split(/\\n /);
        if (lines.length > 1) {
            var i=0;
            for (i=0;i<lines.length - 1;i++) {
                if (/\\$/.test(lines[i])) {
                    result.push(lines[i]+"\\n "+lines[i+1])
                    i++;
                } else {
                    result.push(lines[i])
                }
            }
            if ( i === lines.length - 1) {
                result.push(lines[lines.length-1]);
            }
        } else {
            result = lines;
        }
        result = result.map(function(l) { return l.replace(/\\\\n /g,"\\n ").trim() })
        return result;
    }

    function resetMouseVars() {
        mousedown_node = null;
        mousedown_group = null;
        mousedown_group_handle = null;
        mouseup_node = null;
        mousedown_link = null;
        mouse_mode = 0;
        mousedown_port_type = null;
        activeSpliceLink = null;
        spliceActive = false;
        groupAddActive = false;
        if (activeHoverGroup) {
            activeHoverGroup.hovered = false;
            activeHoverGroup = null;
        }
        d3.selectAll(".red-ui-flow-link-splice").classed("red-ui-flow-link-splice",false);
        if (spliceTimer) {
            clearTimeout(spliceTimer);
            spliceTimer = null;
        }
        if (groupHoverTimer) {
            clearTimeout(groupHoverTimer);
            groupHoverTimer = null;
        }
    }

    function disableQuickJoinEventHandler(evt) {
        // Check for ctrl (all browsers), "Meta" (Chrome/FF), keyCode 91 (Safari), or Escape
        if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91 || evt.keyCode === 27) {
            resetMouseVars();
            hideDragLines();
            redraw();
            $(window).off('keyup',disableQuickJoinEventHandler);
        }
    }

    function portMouseDown(d,portType,portIndex, evt) {
        if (RED.view.DEBUG) { console.warn("portMouseDown", mouse_mode,d,portType,portIndex); }
        RED.contextMenu.hide();
        evt = evt || d3.event;
        if (evt === 1) {
            return;
        }
        if (mouse_mode === RED.state.SELECTING_NODE) {
            evt.stopPropagation();
            return;
        }
        mousedown_node = d;
        mousedown_port_type = portType;
        mousedown_port_index = portIndex || 0;
        if (mouse_mode !== RED.state.QUICK_JOINING && !activeFlowLocked) {
            mouse_mode = RED.state.JOINING;
            document.body.style.cursor = "crosshair";
            if (evt.ctrlKey || evt.metaKey) {
                mouse_mode = RED.state.QUICK_JOINING;
                showDragLines([{node:mousedown_node,port:mousedown_port_index,portType:mousedown_port_type}]);
                $(window).on('keyup',disableQuickJoinEventHandler);
            }
        }
        evt.stopPropagation();
        evt.preventDefault();
    }


    function portMouseUp(d,portType,portIndex,evt) {
        if (RED.view.DEBUG) { console.warn("portMouseUp", mouse_mode,d,portType,portIndex); }
        evt = evt || d3.event;
        if (mouse_mode === RED.state.SELECTING_NODE) {
            evt.stopPropagation();
            return;
        }
        var i;
        if (mouse_mode === RED.state.QUICK_JOINING && drag_lines.length > 0) {
            if (drag_lines[0].node === d) {
                // Cannot quick-join to self
                return
            }
            if (drag_lines[0].virtualLink &&
                (
                    (drag_lines[0].node.type === 'link in' && d.type !== 'link out') ||
                    (drag_lines[0].node.type === 'link out' && d.type !== 'link in')
                )
            ) {
                return
            }
        }
        document.body.style.cursor = "";

        if (mouse_mode == RED.state.JOINING || mouse_mode == RED.state.QUICK_JOINING) {
            if (typeof TouchEvent != "undefined" && evt instanceof TouchEvent) {
                if (RED.view.DEBUG) { console.warn("portMouseUp: TouchEvent", mouse_mode,d,portType,portIndex); }
                const direction = drag_lines[0].portType === PORT_TYPE_INPUT ? PORT_TYPE_OUTPUT : PORT_TYPE_INPUT
                let found = false;
                for (let nodeIdx = 0; nodeIdx < activeNodes.length; nodeIdx++) {
                    const n = activeNodes[nodeIdx];
                    if (RED.view.tools.isPointInNode(n, mouse_position)) {
                        found = true;
                        mouseup_node = n;
                        // portType = mouseup_node.inputs > 0 ? PORT_TYPE_INPUT : PORT_TYPE_OUTPUT;
                        portType = direction;
                        portIndex = 0;
                        break
                    }
                }

                if (!found && drag_lines.length > 0 && !drag_lines[0].virtualLink) {
                    for (let juncIdx = 0; juncIdx < activeJunctions.length; juncIdx++) {
                        // NOTE: a junction is 10px x 10px but the target area is expanded to 30wx20h by adding padding to the bounding box
                        const jNode = activeJunctions[juncIdx];
                        if (RED.view.tools.isPointInNode(jNode, mouse_position, 20, 10)) {
                            found = true;
                            mouseup_node = jNode;
                            portType = direction;
                            portIndex = 0;
                            break
                        }
                    }
                }

                if (!found && activeSubflow) {
                    var subflowPorts = [];
                    if (activeSubflow.status) {
                        subflowPorts.push(activeSubflow.status)
                    }
                    if (activeSubflow.in) {
                        subflowPorts = subflowPorts.concat(activeSubflow.in)
                    }
                    if (activeSubflow.out) {
                        subflowPorts = subflowPorts.concat(activeSubflow.out)
                    }
                    for (var i = 0; i < subflowPorts.length; i++) {
                        const sf = subflowPorts[i];
                        if (RED.view.tools.isPointInNode(sf, mouse_position)) {
                            found = true;
                            mouseup_node = sf;
                            portType = mouseup_node.direction === "in" ? PORT_TYPE_OUTPUT : PORT_TYPE_INPUT;
                            portIndex = 0;
                            break;
                        }
                    }
                }
            } else {
                mouseup_node = d;
            }
            var addedLinks = [];
            var removedLinks = [];
            var modifiedNodes = []; // joining link nodes

            var select_link = null;

            for (i=0;i<drag_lines.length;i++) {
                if (drag_lines[i].link) {
                    removedLinks.push(drag_lines[i].link)
                }
            }
            var linkEditEvents = [];

            for (i=0;i<drag_lines.length;i++) {
                if (portType != drag_lines[i].portType && mouseup_node !== drag_lines[i].node) {
                    let drag_line = drag_lines[i];
                    let src,dst,src_port;
                    let oldDst;
                    let oldSrc;
                    if (drag_line.portType === PORT_TYPE_OUTPUT) {
                        src = drag_line.node;
                        src_port = drag_line.port;
                        dst = mouseup_node;
                        oldSrc = src;
                        if (drag_line.link) {
                            oldDst = drag_line.link.target;
                        }
                    } else if (drag_line.portType === PORT_TYPE_INPUT) {
                        src = mouseup_node;
                        dst = drag_line.node;
                        src_port = portIndex || 0;
                        oldSrc = dst;
                        if (drag_line.link) {
                            oldDst = drag_line.link.source
                        }
                    }
                    var link = {source: src, sourcePort:src_port, target: dst};
                    if (drag_line.virtualLink) {
                        if (/^link (in|out)$/.test(src.type) && /^link (in|out)$/.test(dst.type) && src.type !== dst.type) {
                            if (src.links.indexOf(dst.id) === -1 && dst.links.indexOf(src.id) === -1) {
                                var oldSrcLinks = [...src.links]
                                var oldDstLinks = [...dst.links]

                                src.links.push(dst.id);
                                dst.links.push(src.id);

                                if (oldDst) {
                                    src.links = src.links.filter(id => id !== oldDst.id)
                                    dst.links = dst.links.filter(id => id !== oldDst.id)
                                    var oldOldDstLinks = [...oldDst.links]
                                    oldDst.links = oldDst.links.filter(id => id !== oldSrc.id)
                                    oldDst.dirty = true;
                                    modifiedNodes.push(oldDst);
                                    linkEditEvents.push({
                                        t:'edit',
                                        node: oldDst,
                                        dirty: RED.nodes.dirty(),
                                        changed: oldDst.changed,
                                        changes: {
                                            links:oldOldDstLinks
                                        }
                                    });
                                    oldDst.changed = true;
                                }

                                src.dirty = true;
                                dst.dirty = true;

                                modifiedNodes.push(src);
                                modifiedNodes.push(dst);

                                link.link = true;
                                activeLinks.push(link);
                                activeLinkNodes[src.id] = src;
                                activeLinkNodes[dst.id] = dst;
                                select_link = link;

                                linkEditEvents.push({
                                    t:'edit',
                                    node: src,
                                    dirty: RED.nodes.dirty(),
                                    changed: src.changed,
                                    changes: {
                                        links:oldSrcLinks
                                    }
                                });
                                linkEditEvents.push({
                                    t:'edit',
                                    node: dst,
                                    dirty: RED.nodes.dirty(),
                                    changed: dst.changed,
                                    changes: {
                                        links:oldDstLinks
                                    }
                                });
                               
                                src.changed = true;
                                dst.changed = true;
                            }
                        }
                    } else {
                        // This is not a virtualLink - which means it started
                        // on a regular node port. Need to ensure the this isn't
                        // connecting to a link node virual port.
                        //
                        // PORT_TYPE_OUTPUT=0
                        // PORT_TYPE_INPUT=1
                        if (!(
                            (d.type === "link out" && portType === PORT_TYPE_OUTPUT) ||
                            (d.type === "link in" && portType === PORT_TYPE_INPUT) ||
                            (portType === PORT_TYPE_OUTPUT && mouseup_node.type !== "subflow" && mouseup_node.outputs === 0) ||
                            (portType === PORT_TYPE_INPUT && mouseup_node.type !== "subflow" && mouseup_node.inputs === 0) ||
                            (drag_line.portType === PORT_TYPE_INPUT && mouseup_node.type === "subflow" && (mouseup_node.direction === "status" || mouseup_node.direction === "out")) ||
                            (drag_line.portType === PORT_TYPE_OUTPUT && mouseup_node.type === "subflow" && mouseup_node.direction === "in")
                        )) {
                            let hasJunctionLoop = false
                            if (link.source.type === 'junction' && link.target.type === 'junction') {
                                // This is joining two junctions together. We want to avoid creating a loop
                                // of pure junction nodes as there is no way to break out of it.

                                const visited = new Set()
                                let toVisit = [link.target]
                                while (toVisit.length > 0) {
                                    const next = toVisit.shift()
                                    if (next === link.source) {
                                        hasJunctionLoop = true
                                        break
                                    }
                                    visited.add(next)
                                    toVisit = toVisit.concat(RED.nodes.getDownstreamNodes(next).filter(n => n.type === 'junction' && !visited.has(n)))
                                }
                            }
                            var existingLink = RED.nodes.filterLinks({source:src,target:dst,sourcePort: src_port}).length !== 0;
                            if (!hasJunctionLoop && !existingLink) {
                                RED.nodes.addLink(link);
                                addedLinks.push(link);
                            }
                        }
                    }
                }
            }
            if (addedLinks.length > 0 || removedLinks.length > 0 || modifiedNodes.length > 0) {
                // console.log(addedLinks);
                // console.log(removedLinks);
                // console.log(modifiedNodes);
                var historyEvent;
                if (modifiedNodes.length > 0) {
                    historyEvent = {
                        t:"multi",
                        events: linkEditEvents,
                        dirty:RED.nodes.dirty()
                    };
                } else {
                    historyEvent = {
                        t:"add",
                        links:addedLinks,
                        removedLinks: removedLinks,
                        dirty:RED.nodes.dirty()
                    };
                }
                if (activeSubflow) {
                    var subflowRefresh = RED.subflow.refresh(true);
                    if (subflowRefresh) {
                        historyEvent.subflow = {
                            id:activeSubflow.id,
                            changed: activeSubflow.changed,
                            instances: subflowRefresh.instances
                        }
                    }
                }
                RED.history.push(historyEvent);
                updateActiveNodes();
                RED.nodes.dirty(true);
            }
            if (mouse_mode === RED.state.QUICK_JOINING) {
                if (addedLinks.length > 0 || modifiedNodes.length > 0) {
                    hideDragLines();
                    if (portType === PORT_TYPE_INPUT && d.outputs > 0) {
                        showDragLines([{node:d,port:0,portType:PORT_TYPE_OUTPUT}]);
                    } else if (portType === PORT_TYPE_OUTPUT && d.inputs > 0) {
                        showDragLines([{node:d,port:0,portType:PORT_TYPE_INPUT}]);
                    } else {
                        resetMouseVars();
                    }
                    mousedown_link = select_link;
                    if (select_link) {
                        selectedLinks.clear();
                        selectedLinks.add(select_link);
                        updateSelection();
                    } else {
                        selectedLinks.clear();
                    }
                }
                redraw();
                return;
            }

            resetMouseVars();
            hideDragLines();
            if (select_link) {
                selectedLinks.clear();
                selectedLinks.add(select_link);
            }
            mousedown_link = select_link;
            if (select_link) {
                updateSelection();
            }
            redraw();
        }
    }

    var portLabelHoverTimeout = null;
    var portLabelHover = null;


    function getElementPosition(node) {
        var d3Node = d3.select(node);
        if (d3Node.attr('class') === 'red-ui-workspace-chart-event-layer') {
            return [0,0];
        }
        var result = [];
        var localPos = [0,0];
        if (node.nodeName.toLowerCase() === 'g') {
            var transform = d3Node.attr("transform");
            if (transform) {
                localPos = d3.transform(transform).translate;
            }
        } else {
            localPos = [d3Node.attr("x")||0,d3Node.attr("y")||0];
        }
        var parentPos = getElementPosition(node.parentNode);
        return [localPos[0]+parentPos[0],localPos[1]+parentPos[1]]

    }

    function getPortLabel(node,portType,portIndex) {
        var result;
        var nodePortLabels = (portType === PORT_TYPE_INPUT)?node.inputLabels:node.outputLabels;
        if (nodePortLabels && nodePortLabels[portIndex]) {
            return nodePortLabels[portIndex];
        }
        var portLabels = (portType === PORT_TYPE_INPUT)?node._def.inputLabels:node._def.outputLabels;
        if (typeof portLabels === 'string') {
            result = portLabels;
        } else if (typeof portLabels === 'function') {
            try {
                result = portLabels.call(node,portIndex);
            } catch(err) {
                console.log("Definition error: "+node.type+"."+((portType === PORT_TYPE_INPUT)?"inputLabels":"outputLabels"),err);
                result = null;
            }
        } else if (Array.isArray(portLabels)) {
            result = portLabels[portIndex];
        }
        return result;
    }
    function showTooltip(x,y,content,direction) {
        var tooltip = eventLayer.append("g")
            .attr("transform","translate("+x+","+y+")")
            .attr("class","red-ui-flow-port-tooltip");

        // First check for a user-provided newline - "\\n "
        var newlineIndex = content.indexOf("\\n ");
        if (newlineIndex > -1 && content[newlineIndex-1] !== '\\') {
            content = content.substring(0,newlineIndex)+"...";
        }

        var lines = content.split("\n");
        var labelWidth = 6;
        var labelHeight = 12;
        var labelHeights = [];
        var lineHeight = 0;
        lines.forEach(function(l,i) {
            var labelDimensions = calculateTextDimensions(l||"&nbsp;", "red-ui-flow-port-tooltip-label");
            labelWidth = Math.max(labelWidth,labelDimensions[0] + 14);
            labelHeights.push(labelDimensions[1]);
            if (i === 0) {
                lineHeight = labelDimensions[1];
            }
            labelHeight += labelDimensions[1];
        });
        var labelWidth1 = (labelWidth/2)-5-2;
        var labelWidth2 = labelWidth - 4;

        var labelHeight1 = (labelHeight/2)-5-2;
        var labelHeight2 = labelHeight - 4;
        var path;
        var lx;
        var ly = -labelHeight/2;
        var anchor;
        if (direction === "left") {
            path = "M0 0 l -5 -5 v -"+(labelHeight1)+" q 0 -2 -2 -2 h -"+labelWidth+" q -2 0 -2 2 v "+(labelHeight2)+" q 0 2 2 2 h "+labelWidth+" q 2 0 2 -2 v -"+(labelHeight1)+" l 5 -5";
            lx = -14;
            anchor = "end";
        } else if (direction === "right") {
            path = "M0 0 l 5 -5 v -"+(labelHeight1)+" q 0 -2 2 -2 h "+labelWidth+" q 2 0 2 2 v "+(labelHeight2)+" q 0 2 -2 2 h -"+labelWidth+" q -2 0 -2 -2 v -"+(labelHeight1)+" l -5 -5"
            lx = 14;
            anchor = "start";
        } else if (direction === "top") {
            path = "M0 0 l 5 -5 h "+(labelWidth1)+" q 2 0 2 -2 v -"+labelHeight+" q 0 -2 -2 -2 h -"+(labelWidth2)+" q -2 0 -2 2 v "+labelHeight+" q 0 2 2 2 h "+(labelWidth1)+" l 5 5"
            lx = -labelWidth/2 + 6;
            ly = -labelHeight-lineHeight+12;
            anchor = "start";
        }
        tooltip.append("path").attr("d",path);
        lines.forEach(function(l,i) {
            ly += labelHeights[i];
            // tooltip.append("path").attr("d","M "+(lx-10)+" "+ly+" l 20 0 m -10 -5 l 0 10 ").attr('r',2).attr("stroke","#f00").attr("stroke-width","1").attr("fill","none")
            tooltip.append("svg:text").attr("class","red-ui-flow-port-tooltip-label")
                .attr("x", lx)
                .attr("y", ly)
                .attr("text-anchor",anchor)
                .text(l||" ")
        });
        return tooltip;
    }

    function portMouseOver(port,d,portType,portIndex) {
        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }
        clearTimeout(portLabelHoverTimeout);
        var active = (mouse_mode!=RED.state.JOINING && mouse_mode != RED.state.QUICK_JOINING) || // Not currently joining - all ports active
                     (
                         drag_lines.length > 0 && // Currently joining
                         drag_lines[0].portType !== portType && // INPUT->OUTPUT OUTPUT->INPUT
                         (
                             !drag_lines[0].virtualLink || // Not a link wire
                             (drag_lines[0].node.type === 'link in' && d.type === 'link out') ||
                             (drag_lines[0].node.type === 'link out' && d.type === 'link in')
                         )
                     )

        if (active && ((portType === PORT_TYPE_INPUT && ((d._def && d._def.inputLabels)||d.inputLabels)) || (portType === PORT_TYPE_OUTPUT && ((d._def && d._def.outputLabels)||d.outputLabels)))) {
            portLabelHoverTimeout = setTimeout(function() {
                const n = port && port.node()
                const nId = n && n.__data__ && n.__data__.id
                //check see if node has been deleted since timeout started
                if(!n || !n.parentNode || !RED.nodes.node(n.__data__.id)) {
                    return; //node is gone!
                }
                var tooltip = getPortLabel(d,portType,portIndex);
                if (!tooltip) {
                    return;
                }
                var pos = getElementPosition(n);
                portLabelHoverTimeout = null;
                portLabelHover = showTooltip(
                    (pos[0]+(portType===PORT_TYPE_INPUT?-2:12)),
                    (pos[1]+5),
                    tooltip,
                    portType===PORT_TYPE_INPUT?"left":"right"
                );
            },500);
        }
        port.classed("red-ui-flow-port-hovered",active);
    }
    function portMouseOut(port,d,portType,portIndex) {
        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }
        clearTimeout(portLabelHoverTimeout);
        if (portLabelHover) {
            portLabelHover.remove();
            portLabelHover = null;
        }
        port.classed("red-ui-flow-port-hovered",false);
    }

    function junctionMouseOver(junction, d, portType) {
        var active = (portType === undefined) ||
                     (mouse_mode !== RED.state.JOINING && mouse_mode !== RED.state.QUICK_JOINING) ||
                     (drag_lines.length > 0 && drag_lines[0].portType !== portType && !drag_lines[0].virtualLink)
        junction.classed("red-ui-flow-junction-hovered", active);
    }
    function junctionMouseOut(junction, d) {
        junction.classed("red-ui-flow-junction-hovered",false);
    }

    function prepareDrag(mouse) {
        mouse_mode = RED.state.MOVING;
        // Called when movingSet should be prepared to be dragged
        for (i=0;i<movingSet.length();i++) {
            var msn = movingSet.get(i);
            msn.ox = msn.n.x;
            msn.oy = msn.n.y;
            msn.dx = msn.n.x-mouse[0];
            msn.dy = msn.n.y-mouse[1];
        }
        try {
            mouse_offset = d3.mouse(document.body);
            if (isNaN(mouse_offset[0])) {
                mouse_offset = d3.touches(document.body)[0];
            }
        } catch(err) {
            mouse_offset = [0,0]
        }
    }

    function nodeMouseUp(d) {
        if (RED.view.DEBUG) { console.warn("nodeMouseUp", mouse_mode,d); }
        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }
        if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < dblClickInterval) {
            mouse_mode = RED.state.DEFAULT;
            if (RED.workspaces.isLocked()) {
                clickElapsed = 0;
                d3.event.stopPropagation();
                return
            }
            // Avoid dbl click causing text selection.
            d3.event.preventDefault()
            document.getSelection().removeAllRanges()
            if (d.type != "subflow") {
                if (/^subflow:/.test(d.type) && isControlPressed(d3.event)) {
                    RED.workspaces.show(d.type.substring(8));
                } else {
                    RED.editor.edit(d);
                }
            } else {
                RED.editor.editSubflow(activeSubflow);
            }
            clickElapsed = 0;
            d3.event.stopPropagation();
            return;
        }
        if (mouse_mode === RED.state.MOVING) {
            // Moving primed, but not active.
            if (!groupNodeSelectPrimed && !d.selected && d.g && RED.nodes.group(d.g).selected) {
                clearSelection();

                selectedGroups.add(RED.nodes.group(d.g), false);

                mousedown_node.selected = true;
                movingSet.add(mousedown_node);
                var mouse = d3.touches(this)[0]||d3.mouse(this);
                mouse[0] += d.x-d.w/2;
                mouse[1] += d.y-d.h/2;
                prepareDrag(mouse);
                updateSelection();
                return;
            }
        }

        groupNodeSelectPrimed = false;

        var direction = d._def? (d.inputs > 0 ? 1: 0) : (d.direction == "in" ? 0: 1)
        var wasJoining = false;
        if (mouse_mode === RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) {
            wasJoining = true;
            if (drag_lines.length > 0) {
                if (drag_lines[0].virtualLink) {
                    if (d.type === 'link in') {
                        direction = 1;
                    } else if (d.type === 'link out') {
                        direction = 0;
                    }
                } else {
                    if (drag_lines[0].portType === 1) {
                        direction = PORT_TYPE_OUTPUT;
                    } else {
                        direction = PORT_TYPE_INPUT;
                    }
                }
            }
        }

        portMouseUp(d, direction, 0);
        if (wasJoining) {
            d3.selectAll(".red-ui-flow-port-hovered").classed("red-ui-flow-port-hovered",false);
        }
    }
    function nodeMouseDown(d) {
        if (RED.view.DEBUG) { console.warn("nodeMouseDown", mouse_mode,d); }
        focusView();
        RED.contextMenu.hide();
        if (d3.event.button === 1) {
            return;
        }
        //var touch0 = d3.event;
        //var pos = [touch0.pageX,touch0.pageY];
        //RED.touch.radialMenu.show(d3.select(this),pos);
        if (mouse_mode == RED.state.IMPORT_DRAGGING || mouse_mode == RED.state.DETACHED_DRAGGING) {
            var historyEvent = RED.history.peek();
             // Check to see if we're dropping into a group
             const {
                addedToGroup,
                removedFromGroup,
                groupMoveEvent,
                rehomedNodes
            } = addMovingSetToGroup()

            if (activeSpliceLink) {
                var linkToSplice = d3.select(activeSpliceLink).data()[0];
                spliceLink(linkToSplice, movingSet.get(0).n, historyEvent)
                updateActiveNodes();
            }
            if (mouse_mode == RED.state.DETACHED_DRAGGING) {
                // Create two lists of nodes:
                //  - nodes that have moved without changing group
                //  - nodes that have moved AND changed group
                const ns = [];
                const rehomedNodeList = [];
                for (var j=0;j<movingSet.length();j++) {
                    var n = movingSet.get(j);
                    if (n.ox !== n.n.x || n.oy !== n.n.y) {
                        ns.push({n:n.n,ox:n.ox,oy:n.oy,moved:n.n.moved});
                        n.n.dirty = true;
                        n.n.moved = true;
                    }
                }
                var event = {
                    t: "multi",
                    events: [
                        historyEvent,
                        { t: "move", nodes: ns }
                    ],
                    dirty: historyEvent.dirty
                };
                if (groupMoveEvent) {
                    event.events.push(groupMoveEvent);
                }
                RED.history.replace(event)
            } else if (groupMoveEvent) {
                var event = { t:"multi", events: [historyEvent, groupMoveEvent], dirty: true};
                RED.history.replace(event);
            }

            updateSelection();
            RED.nodes.dirty(true);
            redraw();
            if (clipboardSource === 'cut') {
                clipboardSource = 'copy'
            }
            resetMouseVars();
            d3.event.stopPropagation();
            return;
        } else if (mouse_mode == RED.state.QUICK_JOINING) {
            d3.event.stopPropagation();
            return;
        } else if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            if (d.type === 'junction') {
                return
            }
            if (selectNodesOptions.single) {
                selectNodesOptions.done(d);
                return;
            }
            if (d.selected) {
                d.selected = false;
                movingSet.remove(d);
            } else {
                if (!selectNodesOptions.filter || selectNodesOptions.filter(d)) {
                    d.selected = true;
                    movingSet.add(d);
                }
            }
            d.dirty = true;
            redraw();
            // if (selectNodesOptions && selectNodesOptions.onselect) {
            //     selectNodesOptions.onselect(moving_set.map(function(n) { return n.n;}))
            // }
            return;
        }

        mousedown_node = d;

        var now = Date.now();
        clickElapsed = now-clickTime;
        clickTime = now;
        dblClickPrimed = lastClickNode == mousedown_node &&
            (d3.event.touches || d3.event.button === 0) &&
            !d3.event.shiftKey && !d3.event.altKey &&
            clickElapsed < dblClickInterval &&
            d.type !== 'junction'
        lastClickNode = mousedown_node;
     
        if (d.selected && isControlPressed(d3.event)) {
            mousedown_node.selected = false;
            movingSet.remove(mousedown_node);
        } else {
            if (d3.event.shiftKey) {
                if (!isControlPressed(d3.event)) {
                    clearSelection();
                }
                var clickPosition = (d3.event.offsetX/scaleFactor - mousedown_node.x)
                var edgeDelta = ((mousedown_node.w||10)/2) - Math.abs(clickPosition);
                var cnodes;
                var targetEdgeDelta = mousedown_node.w > 30 ? 25 : (mousedown_node.w > 0 ? 8 : 3);
                if (edgeDelta < targetEdgeDelta) {
                    if (clickPosition < 0) {
                        cnodes = [mousedown_node].concat(RED.nodes.getAllUpstreamNodes(mousedown_node));
                    } else {
                        cnodes = [mousedown_node].concat(RED.nodes.getAllDownstreamNodes(mousedown_node));
                    }
                } else {
                    cnodes = RED.nodes.getAllFlowNodes(mousedown_node);
                }
                for (var n=0;n<cnodes.length;n++) {
                    cnodes[n].selected = true;
                    cnodes[n].dirty = true;
                    movingSet.add(cnodes[n]);
                }
            } else if (!d.selected) {
                if (!d3.event.ctrlKey && !d3.event.metaKey) {
                    clearSelection();
                }
                mousedown_node.selected = true;
                movingSet.add(mousedown_node);
            }
            // selectedLinks.clear();
            if (d3.event.button != 2) {
                var mouse = d3.touches(this)[0]||d3.mouse(this);
                mouse[0] += d.x-d.w/2;
                mouse[1] += d.y-d.h/2;
                prepareDrag(mouse);
            }
        }
        d.dirty = true;
        updateSelection();
        redraw();
        d3.event.stopPropagation();
    }
    function nodeTouchStart(d) {
        if (RED.view.DEBUG) { console.warn("nodeTouchStart", mouse_mode,d); }
        var obj = d3.select(this);
        var touch0 = d3.event.touches.item(0);
        var pos = [touch0.pageX,touch0.pageY];
        startTouchCenter = [touch0.pageX,touch0.pageY];
        startTouchDistance = 0;
        touchStartTime = setTimeout(function() {
            showTouchMenu(obj,pos);
        },touchLongPressTimeout);
        nodeMouseDown.call(this,d)
        d3.event.preventDefault();
    }
    function nodeTouchEnd(d) {
        if (RED.view.DEBUG) { console.warn("nodeTouchEnd", mouse_mode,d); }
        d3.event.preventDefault();
        clearTimeout(touchStartTime);
        touchStartTime = null;
        if  (RED.touch.radialMenu.active()) {
            d3.event.stopPropagation();
            return;
        }
        nodeMouseUp.call(this,d);
    }

    function nodeMouseOver(d) {
        if (RED.view.DEBUG) { console.warn("nodeMouseOver", mouse_mode,d); }
        if (mouse_mode === 0 || mouse_mode === RED.state.SELECTING_NODE) {
            if (mouse_mode === RED.state.SELECTING_NODE && selectNodesOptions && selectNodesOptions.filter) {
                if (selectNodesOptions.filter(d)) {
                    this.parentNode.classList.add("red-ui-flow-node-hovered");
                }
            } else {
                this.parentNode.classList.add("red-ui-flow-node-hovered");
            }
            clearTimeout(portLabelHoverTimeout);
            if (d.hasOwnProperty('l')?!d.l : (d.type === "link in" || d.type === "link out")) {
                var parentNode = this.parentNode;
                portLabelHoverTimeout = setTimeout(function() {
                    //check see if node has been deleted since timeout started
                    if(!parentNode || !parentNode.parentNode || !RED.nodes.node(parentNode.id)) {
                        return; //node is gone!
                    }
                    var tooltip;
                    if (d._def.label) {
                        tooltip = d._def.label;
                        try {
                            tooltip = (typeof tooltip === "function" ? tooltip.call(d) : tooltip)||"";
                        } catch(err) {
                            console.log("Definition error: "+d.type+".label",err);
                            tooltip = d.type;
                        }
                    }
                    if (tooltip !== "") {
                        var pos = getElementPosition(parentNode);
                        portLabelHoverTimeout = null;
                        portLabelHover = showTooltip(
                            (pos[0] + d.w/2),
                            (pos[1]-1),
                            tooltip,
                            "top"
                        );
                    }
                },500);
            }
        } else if (mouse_mode === RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) {
            if (drag_lines.length > 0) {
                var selectClass;
                var portType;
                if ((drag_lines[0].virtualLink && drag_lines[0].portType === PORT_TYPE_INPUT) || drag_lines[0].portType === PORT_TYPE_OUTPUT) {
                    selectClass = ".red-ui-flow-port-input .red-ui-flow-port";
                    portType = PORT_TYPE_INPUT;
                } else {
                    selectClass = ".red-ui-flow-port-output .red-ui-flow-port";
                    portType = PORT_TYPE_OUTPUT;
                }
                portMouseOver(d3.select(this.parentNode).selectAll(selectClass),d,portType,0);
            }
        }
    }
    function nodeMouseOut(d) {
        if (RED.view.DEBUG) { console.warn("nodeMouseOut", mouse_mode,d); }
        this.parentNode.classList.remove("red-ui-flow-node-hovered");
        clearTimeout(portLabelHoverTimeout);
        if (portLabelHover) {
            portLabelHover.remove();
            portLabelHover = null;
        }
        if (mouse_mode === RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) {
            if (drag_lines.length > 0) {
                var selectClass;
                var portType;
                if ((drag_lines[0].virtualLink && drag_lines[0].portType === PORT_TYPE_INPUT) || drag_lines[0].portType === PORT_TYPE_OUTPUT) {
                    selectClass = ".red-ui-flow-port-input .red-ui-flow-port";
                    portType = PORT_TYPE_INPUT;
                } else {
                    selectClass = ".red-ui-flow-port-output .red-ui-flow-port";
                    portType = PORT_TYPE_OUTPUT;
                }
                portMouseOut(d3.select(this.parentNode).selectAll(selectClass),d,portType,0);
            }
        }
    }

    function portMouseDownProxy(e) {  portMouseDown(this.__data__,this.__portType__,this.__portIndex__, e); }
    function portTouchStartProxy(e) { portMouseDown(this.__data__,this.__portType__,this.__portIndex__, e); e.preventDefault() }
    function portMouseUpProxy(e) { portMouseUp(this.__data__,this.__portType__,this.__portIndex__, e); }
    function portTouchEndProxy(e) { portMouseUp(this.__data__,this.__portType__,this.__portIndex__, e); e.preventDefault() }
    function portMouseOverProxy(e) { portMouseOver(d3.select(this), this.__data__,this.__portType__,this.__portIndex__, e); }
    function portMouseOutProxy(e) { portMouseOut(d3.select(this), this.__data__,this.__portType__,this.__portIndex__, e); }

    function junctionMouseOverProxy(e) { junctionMouseOver(d3.select(this), this.__data__, this.__portType__) }
    function junctionMouseOutProxy(e) { junctionMouseOut(d3.select(this), this.__data__) }

    function linkMouseDown(d) {
        if (RED.view.DEBUG) {
            console.warn("linkMouseDown", { mouse_mode, point: d3.mouse(this), event: d3.event });
        }
        RED.contextMenu.hide();
        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }
        if (d3.event.button === 2) {
            return
        }
        mousedown_link = d;

        if (!isControlPressed(d3.event)) {
            clearSelection();
        }
        if (isControlPressed(d3.event)) {
            if (!selectedLinks.has(mousedown_link)) {
                selectedLinks.add(mousedown_link);
            } else {
                if (selectedLinks.length() !== 1) {
                    selectedLinks.remove(mousedown_link);
                }
            }
        } else {
            selectedLinks.add(mousedown_link);
        }
        updateSelection();
        redraw();
        focusView();
        d3.event.stopPropagation();
        if (!mousedown_link.link && movingSet.length() === 0 && (d3.event.touches || d3.event.button === 0) && selectedLinks.length() === 1 && selectedLinks.has(mousedown_link) && isControlPressed(d3.event)) {
            d3.select(this).classed("red-ui-flow-link-splice",true);
            var point = d3.mouse(this);
            var clickedGroup = getGroupAt(point[0],point[1]);
            showQuickAddDialog({position:point, splice:mousedown_link, group:clickedGroup});
        }
    }
    function linkTouchStart(d) {
        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }
        mousedown_link = d;
        clearSelection();
        selectedLinks.clear();
        selectedLinks.add(mousedown_link);
        updateSelection();
        redraw();
        focusView();
        d3.event.stopPropagation();

        var obj = d3.select(document.body);
        var touch0 = d3.event.touches.item(0);
        var pos = [touch0.pageX,touch0.pageY];
        touchStartTime = setTimeout(function() {
            touchStartTime = null;
            showTouchMenu(obj,pos);
        },touchLongPressTimeout);
        d3.event.preventDefault();
    }

    function groupMouseUp(g) {
        if (RED.view.DEBUG) {
            console.warn("groupMouseUp", { mouse_mode, event: d3.event });
        }
        if (RED.workspaces.isLocked()) { return }
        if (dblClickPrimed && mousedown_group == g && clickElapsed > 0 && clickElapsed < dblClickInterval) {
            mouse_mode = RED.state.DEFAULT;
            RED.editor.editGroup(g);
            d3.event.stopPropagation();
            return;
        }

    }

    function groupMouseDown(g) {
        var mouse = d3.touches(this.parentNode)[0]||d3.mouse(this.parentNode);
        // if (! (mouse[0] < g.x+10 || mouse[0] > g.x+g.w-10 || mouse[1] < g.y+10 || mouse[1] > g.y+g.h-10) ) {
        //     return
        // }

        if (RED.view.DEBUG) {
            console.warn("groupMouseDown", { mouse_mode, point: mouse, event: d3.event });
        }
        RED.contextMenu.hide();
        focusView();
        if (d3.event.button === 1) {
            return;
        }

        if (mouse_mode == RED.state.QUICK_JOINING) {
            return;
        } else if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }

        mousedown_group = g;

        var now = Date.now();
        clickElapsed = now-clickTime;
        clickTime = now;

        dblClickPrimed = (
            lastClickNode == g &&
            (d3.event.touches || d3.event.button === 0) &&
            !d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey &&
            clickElapsed < dblClickInterval
        );
        lastClickNode = g;

        if (g.selected && isControlPressed(d3.event)) {
            selectedGroups.remove(g);
            d3.event.stopPropagation();
        } else {
            if (!g.selected) {
                if (!d3.event.ctrlKey && !d3.event.metaKey) {
                    clearSelection();
                }
                selectedGroups.add(g,true);//!wasSelected);
            }

            if (d3.event.button != 2) {
                var d = g.nodes[0];
                prepareDrag(mouse);
                mousedown_group.dx = mousedown_group.x - mouse[0];
                mousedown_group.dy = mousedown_group.y - mouse[1];
            }
        }

        updateSelection();
        redraw();
        d3.event.stopPropagation();
    }

    function getGroupAt(x, y, ignoreSelected) {
        // x,y expected to be in node-co-ordinate space
        var candidateGroups = {};
        for (var i=0;i<activeGroups.length;i++) {
            var g = activeGroups[i];
            if (ignoreSelected && movingSet.has(g)) {
                // When ignoreSelected is set, do not match any group in the
                // current movingSet. This is used when dragging a selection
                // to find a candidate group for adding the selection to
                continue
            }
            if (x >= g.x && x <= g.x + g.w && y >= g.y && y <= g.y + g.h) {
                candidateGroups[g.id] = g;
            }
        }
        var ids = Object.keys(candidateGroups);
        if (ids.length > 1) {
            ids.forEach(function(id) {
                if (candidateGroups[id] && candidateGroups[id].g) {
                    delete candidateGroups[candidateGroups[id].g]
                }
            })
            ids = Object.keys(candidateGroups);
        }
        if (ids.length === 0) {
            return null;
        } else {
            return candidateGroups[ids[ids.length-1]]
        }
    }

    function isButtonEnabled(d) {
        var buttonEnabled = true;
        var ws = RED.nodes.workspace(RED.workspaces.active());
        if (ws && !ws.disabled && !d.d && !ws.locked) {
            if (d._def.button.hasOwnProperty('enabled')) {
                if (typeof d._def.button.enabled === "function") {
                    buttonEnabled = d._def.button.enabled.call(d);
                } else {
                    buttonEnabled = d._def.button.enabled;
                }
            }
        } else {
            buttonEnabled = false;
        }
        return buttonEnabled;
    }

    function nodeButtonClicked(d) {
        if (mouse_mode === RED.state.SELECTING_NODE) {
            if (d3.event) {
                d3.event.stopPropagation();
            }
            return;
        }
        var activeWorkspace = RED.workspaces.active();
        var ws = RED.nodes.workspace(activeWorkspace);
        if (ws && !ws.disabled && !d.d && !ws.locked) {
            if (d._def.button.toggle) {
                d[d._def.button.toggle] = !d[d._def.button.toggle];
                d.dirty = true;
            }
            if (d._def.button.onclick) {
                try {
                    d._def.button.onclick.call(d);
                } catch(err) {
                    console.log("Definition error: "+d.type+".onclick",err);
                }
            }
            if (d.dirty) {
                redraw();
            }
        } else if (!ws || !ws.locked){
            if (activeSubflow) {
                RED.notify(RED._("notification.warning", {message:RED._("notification.warnings.nodeActionDisabledSubflow")}),"warning");
            } else {
                RED.notify(RED._("notification.warning", {message:RED._("notification.warnings.nodeActionDisabled")}),"warning");
            }
        }
        if (d3.event) {
            d3.event.preventDefault();
        }
    }

    function showTouchMenu(obj,pos) {
        var mdn = mousedown_node;
        var options = [];
        const isActiveLocked = RED.workspaces.isLocked()
        options.push({name:"delete",disabled:(isActiveLocked || movingSet.length()===0 && selectedLinks.length() === 0),onselect:function() {deleteSelection();}});
        options.push({name:"cut",disabled:(isActiveLocked || movingSet.length()===0),onselect:function() {copySelection(true);deleteSelection();}});
        options.push({name:"copy",disabled:(isActiveLocked || movingSet.length()===0),onselect:function() {copySelection();}});
        options.push({name:"paste",disabled:(isActiveLocked || clipboard.length===0),onselect:function() {importNodes(clipboard, {generateIds: true, touchImport: true});}});
        options.push({name:"edit",disabled:(isActiveLocked || movingSet.length() != 1),onselect:function() { RED.editor.edit(mdn);}});
        options.push({name:"select",onselect:function() {selectAll();}});
        options.push({name:"undo",disabled:(RED.history.depth() === 0),onselect:function() {RED.history.pop();}});
        options.push({name:"add",disabled:isActiveLocked, onselect:function() {
            chartPos = chart.offset();
            showQuickAddDialog({
                position:[pos[0]-chartPos.left+chart.scrollLeft(),pos[1]-chartPos.top+chart.scrollTop()],
                touchTrigger:true
            })
        }});

        RED.touch.radialMenu.show(obj,pos,options);
        resetMouseVars();
    }

    function createIconAttributes(iconUrl, icon_group, d) {
        var fontAwesomeUnicode = null;
        if (iconUrl.indexOf("font-awesome/") === 0) {
            var iconName = iconUrl.substr(13);
            var fontAwesomeUnicode = RED.nodes.fontAwesome.getIconUnicode(iconName);
            if (!fontAwesomeUnicode) {
                var iconPath = RED.utils.getDefaultNodeIcon(d._def, d);
                iconUrl = RED.settings.apiRootUrl+"icons/"+iconPath.module+"/"+iconPath.file;
            }
        }
        if (fontAwesomeUnicode) {
            // Since Node-RED workspace uses SVG, i tag cannot be used for font-awesome icon.
            // On SVG, use text tag as an alternative.
            icon_group.append("text")
                .attr("xlink:href",iconUrl)
                .attr("class","fa-lg")
                .attr("x",15)
                .text(fontAwesomeUnicode);
        } else {
            var icon = icon_group.append("image")
                .style("display","none")
                .attr("xlink:href",iconUrl)
                .attr("class","red-ui-flow-node-icon")
                .attr("x",0)
                .attr("width","30")
                .attr("height","30");

            var img = new Image();
            img.src = iconUrl;
            img.onload = function() {
                if (!iconUrl.match(/\.svg$/)) {
                    var largestEdge = Math.max(img.width,img.height);
                    var scaleFactor = 1;
                    if (largestEdge > 30) {
                        scaleFactor = 30/largestEdge;
                    }
                    var width = img.width * scaleFactor;
                    if (width > 20) {
                        scaleFactor *= 20/width;
                        width = 20;
                    }
                    var height = img.height * scaleFactor;
                    icon.attr("width",width);
                    icon.attr("height",height);
                    icon.attr("x",15-width/2);
                    icon.attr("y",(30-height)/2);
                }
                icon.attr("xlink:href",iconUrl);
                icon.style("display",null);
                //if ("right" == d._def.align) {
                //    icon.attr("x",function(d){return d.w-img.width-1-(d.outputs>0?5:0);});
                //    icon_shade.attr("x",function(d){return d.w-30});
                //    icon_shade_border.attr("d",function(d){return "M "+(d.w-30)+" 1 l 0 "+(d.h-2);});
                //}
            }
        }
    }

    function redrawStatus(d,nodeEl) {
        if (d.z !== RED.workspaces.active()) {
            return;
        }
        if (!nodeEl) {
            nodeEl = document.getElementById(d.id);
        }
        if (nodeEl) {
            // Do not show node status if:
            // - global flag set
            // - node has no status
            // - node is disabled
            if (!showStatus || !d.status || d.d === true) {
                nodeEl.__statusGroup__.style.display = "none";
            } else {
                nodeEl.__statusGroup__.style.display = "inline";
                let backgroundWidth = 15
                var fill = status_colours[d.status.fill]; // Only allow our colours for now
                if (d.status.shape == null && fill == null) {
                    backgroundWidth = 0
                    nodeEl.__statusShape__.style.display = "none";
                    nodeEl.__statusBackground__.setAttribute("x", 17)
                    nodeEl.__statusGroup__.setAttribute("transform","translate(-14,"+(d.h+3)+")");
                } else {
                    nodeEl.__statusGroup__.setAttribute("transform","translate(3,"+(d.h+3)+")");
                    var statusClass = "red-ui-flow-node-status-"+(d.status.shape||"dot")+"-"+d.status.fill;
                    nodeEl.__statusShape__.style.display = "inline";
                    nodeEl.__statusShape__.setAttribute("class","red-ui-flow-node-status "+statusClass);
                    nodeEl.__statusBackground__.setAttribute("x", 3)
                }
                if (d.status.hasOwnProperty('text')) {
                    nodeEl.__statusLabel__.textContent = d.status.text;
                } else {
                    nodeEl.__statusLabel__.textContent = "";
                }
                const textSize = nodeEl.__statusLabel__.getBBox()
                backgroundWidth += textSize.width
                if (backgroundWidth > 0 && textSize.width > 0) {
                    backgroundWidth += 6
                }
                nodeEl.__statusBackground__.setAttribute('width', backgroundWidth)
            }
            delete d.dirtyStatus;
        }
    }

    var pendingRedraw;

    function redraw() {
        if (RED.view.DEBUG_SYNC_REDRAW) {
            _redraw();
        } else {
            if (pendingRedraw) {
                cancelAnimationFrame(pendingRedraw);
            }
            pendingRedraw = requestAnimationFrame(_redraw);
        }
    }

    function _redraw() {
        eventLayer.attr("transform","scale("+scaleFactor+")");
        outer.attr("width", space_width*scaleFactor).attr("height", space_height*scaleFactor);

        // Don't bother redrawing nodes if we're drawing links
        if (showAllLinkPorts !== -1 || mouse_mode != RED.state.JOINING) {

            var dirtyNodes = {};

            if (activeSubflow) {
                var subflowOutputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-output").data(activeSubflow.out,function(d,i){ return d.id;});
                subflowOutputs.exit().remove();
                var outGroup = subflowOutputs.enter().insert("svg:g").attr("class","red-ui-flow-node red-ui-flow-subflow-port-output")
                outGroup.each(function(d,i) {
                    var node = d3.select(this);
                    var nodeContents = document.createDocumentFragment();

                    d.h = 40;
                    d.resize = true;
                    d.dirty = true;

                    var mainRect = document.createElementNS("http://www.w3.org/2000/svg","rect");
                    mainRect.__data__ = d;
                    mainRect.setAttribute("class", "red-ui-flow-subflow-port");
                    mainRect.setAttribute("rx", 8);
                    mainRect.setAttribute("ry", 8);
                    mainRect.setAttribute("width", 40);
                    mainRect.setAttribute("height", 40);
                    node[0][0].__mainRect__ = mainRect;
                    d3.select(mainRect)
                        .on("mouseup",nodeMouseUp)
                        .on("mousedown",nodeMouseDown)
                        .on("touchstart",nodeTouchStart)
                        .on("touchend",nodeTouchEnd)
                    nodeContents.appendChild(mainRect);

                    var output_groupEl = document.createElementNS("http://www.w3.org/2000/svg","g");
                    output_groupEl.setAttribute("x",0);
                    output_groupEl.setAttribute("y",0);
                    node[0][0].__outputLabelGroup__ = output_groupEl;

                    var output_output = document.createElementNS("http://www.w3.org/2000/svg","text");
                    output_output.setAttribute("class","red-ui-flow-port-label");
                    output_output.style["font-size"] = "10px";
                    output_output.textContent = "output";
                    output_groupEl.appendChild(output_output);
                    node[0][0].__outputOutput__ = output_output;

                    var output_number = document.createElementNS("http://www.w3.org/2000/svg","text");
                    output_number.setAttribute("class","red-ui-flow-port-label red-ui-flow-port-index");
                    output_number.setAttribute("x",0);
                    output_number.setAttribute("y",0);
                    output_number.textContent = d.i+1;
                    output_groupEl.appendChild(output_number);
                    node[0][0].__outputNumber__ = output_number;

                    var output_border = document.createElementNS("http://www.w3.org/2000/svg","path");
                    output_border.setAttribute("d","M 40 1 l 0 38")
                    output_border.setAttribute("class", "red-ui-flow-node-icon-shade-border")
                    output_groupEl.appendChild(output_border);
                    node[0][0].__outputBorder__ = output_border;

                    nodeContents.appendChild(output_groupEl);

                    var text = document.createElementNS("http://www.w3.org/2000/svg","g");
                    text.setAttribute("class","red-ui-flow-port-label");
                    text.setAttribute("transform","translate(38,0)");
                    text.setAttribute('style', 'fill : #888');  // hard coded here!
                    node[0][0].__textGroup__ = text;
                    nodeContents.append(text);

                    var portEl = document.createElementNS("http://www.w3.org/2000/svg","g");
                    portEl.setAttribute('transform','translate(-5,15)')

                    var port = document.createElementNS("http://www.w3.org/2000/svg","rect");
                    port.setAttribute("class","red-ui-flow-port");
                    port.setAttribute("rx",3);
                    port.setAttribute("ry",3);
                    port.setAttribute("width",10);
                    port.setAttribute("height",10);
                    portEl.appendChild(port);
                    port.__data__ = d;

                    d3.select(port)
                        .on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} )
                        .on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
                        .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);})
                        .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
                        .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
                        .on("mouseout",function(d){portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);});

                    node[0][0].__port__ = portEl
                    nodeContents.appendChild(portEl);
                    node[0][0].appendChild(nodeContents);
                });

                var subflowInputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-input").data(activeSubflow.in,function(d,i){ return d.id;});
                subflowInputs.exit().remove();
                var inGroup = subflowInputs.enter().insert("svg:g").attr("class","red-ui-flow-node red-ui-flow-subflow-port-input").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"});
                inGroup.each(function(d,i) {
                    d.w=40;
                    d.h=40;
                });
                inGroup.append("rect").attr("class","red-ui-flow-subflow-port").attr("rx",8).attr("ry",8).attr("width",40).attr("height",40)
                    // TODO: This is exactly the same set of handlers used for regular nodes - DRY
                    .on("mouseup",nodeMouseUp)
                    .on("mousedown",nodeMouseDown)
                    .on("touchstart",nodeTouchStart)
                    .on("touchend", nodeTouchEnd);

                inGroup.append("g").attr('transform','translate(35,15)').append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
                    .on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_OUTPUT,i);} )
                    .on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_OUTPUT,i);d3.event.preventDefault();} )
                    .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_OUTPUT,i);})
                    .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_OUTPUT,i);d3.event.preventDefault();} )
                    .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_OUTPUT,0);})
                    .on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_OUTPUT,0);});

                inGroup.append("svg:text").attr("class","red-ui-flow-port-label").attr("x",18).attr("y",20).style("font-size","10px").text("input");

                var subflowStatus = nodeLayer.selectAll(".red-ui-flow-subflow-port-status").data(activeSubflow.status?[activeSubflow.status]:[],function(d,i){ return d.id;});
                subflowStatus.exit().remove();

                var statusGroup = subflowStatus.enter().insert("svg:g").attr("class","red-ui-flow-node red-ui-flow-subflow-port-status").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"});
                statusGroup.each(function(d,i) {
                    d.w=40;
                    d.h=40;
                });
                statusGroup.append("rect").attr("class","red-ui-flow-subflow-port").attr("rx",8).attr("ry",8).attr("width",40).attr("height",40)
                    // TODO: This is exactly the same set of handlers used for regular nodes - DRY
                    .on("mouseup",nodeMouseUp)
                    .on("mousedown",nodeMouseDown)
                    .on("touchstart",nodeTouchStart)
                    .on("touchend", nodeTouchEnd);

                statusGroup.append("g").attr('transform','translate(-5,15)').append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
                    .on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} )
                    .on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
                    .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);})
                    .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
                    .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
                    .on("mouseout",function(d){portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);});

                statusGroup.append("svg:text").attr("class","red-ui-flow-port-label").attr("x",22).attr("y",20).style("font-size","10px").text("status");

                subflowOutputs.each(function(d,i) {
                    if (d.dirty) {

                        var port_height = 40;

                        var self = this;
                        var thisNode = d3.select(this);

                        dirtyNodes[d.id] = d;

                        var label = getPortLabel(activeSubflow, PORT_TYPE_OUTPUT, d.i) || "";
                        var hideLabel = (label.length < 1)

                        var labelParts;
                        if (d.resize || this.__hideLabel__ !== hideLabel || this.__label__ !== label) {
                            labelParts = getLabelParts(label, "red-ui-flow-node-label");
                            if (labelParts.lines.length !== this.__labelLineCount__ || this.__label__ !== label) {
                                d.resize = true;
                            }
                            this.__label__ = label;
                            this.__labelLineCount__ = labelParts.lines.length;

                            if (hideLabel) {
                                d.h = Math.max(port_height,(d.outputs || 0) * 15);
                            } else {
                                d.h = Math.max(6+24*labelParts.lines.length,(d.outputs || 0) * 15, port_height);
                            }
                            this.__hideLabel__ = hideLabel;
                        }

                        if (d.resize) {
                            var ow = d.w;
                            if (hideLabel) {
                                d.w = port_height;
                            } else {
                                d.w = Math.max(port_height,20*(Math.ceil((labelParts.width+50+7)/20)) );
                            }
                            if (ow !== undefined) {
                                d.x += (d.w-ow)/2;
                            }
                            d.resize = false;
                        }

                        this.setAttribute("transform", "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")");
                        // This might be the first redraw after a node has been click-dragged to start a move.
                        // So its selected state might have changed since the last redraw.
                        this.classList.toggle("red-ui-flow-node-selected", !!d.selected )
                        if (mouse_mode != RED.state.MOVING_ACTIVE) {
                            this.classList.toggle("red-ui-flow-node-disabled", d.d === true);
                            this.__mainRect__.setAttribute("width", d.w)
                            this.__mainRect__.setAttribute("height", d.h)
                            this.__mainRect__.classList.toggle("red-ui-flow-node-highlighted",!!d.highlighted );

                            if (labelParts) {
                                // The label has changed
                                var sa = labelParts.lines;
                                var sn = labelParts.lines.length;
                                var textLines = this.__textGroup__.childNodes;
                                while(textLines.length > sn) {
                                    textLines[textLines.length-1].remove();
                                }
                                for (var i=0; i<sn; i++) {
                                    if (i===textLines.length) {
                                        var line = document.createElementNS("http://www.w3.org/2000/svg","text");
                                        line.setAttribute("class","red-ui-flow-node-label-text");
                                        line.setAttribute("x",0);
                                        line.setAttribute("y",i*24);
                                        this.__textGroup__.appendChild(line);
                                    }
                                    textLines[i].textContent = sa[i];
                                }
                            }

                            var textClass = "red-ui-flow-node-label"+(hideLabel?" hide":"");
                            this.__textGroup__.setAttribute("class", textClass);
                            var yp = d.h / 2 - (this.__labelLineCount__ / 2) * 24 + 13;

                            // this.__textGroup__.classList.remove("red-ui-flow-node-label-right");
                            this.__textGroup__.setAttribute("transform", "translate(48,"+yp+")");

                            this.__outputBorder__.setAttribute("d","M 40 1 l 0 "+(hideLabel?0:(d.h - 2)));
                            this.__port__.setAttribute("transform","translate(-5,"+((d.h/2)-5)+")");
                            this.__outputOutput__.setAttribute("transform","translate(20,"+((d.h/2)-8)+")");
                            this.__outputNumber__.setAttribute("transform","translate(20,"+((d.h/2)+7)+")");
                            this.__outputNumber__.textContent = d.i+1;
                        }
                        d.dirty = false;
                    }
                });
                subflowInputs.each(function(d,i) {
                    if (d.dirty) {
                        var input = d3.select(this);
                        input.classed("red-ui-flow-node-selected",function(d) { return d.selected; })
                        input.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; });
                        dirtyNodes[d.id] = d;
                        d.dirty = false;
                    }
                });
                subflowStatus.each(function(d,i) {
                    if (d.dirty) {
                        var output = d3.select(this);
                        output.classed("red-ui-flow-node-selected",function(d) { return d.selected; })
                        output.selectAll(".red-ui-flow-port-index").text(function(d){ return d.i+1});
                        output.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; });
                        dirtyNodes[d.id] = d;
                        d.dirty = false;
                    }
                });


            } else {
                nodeLayer.selectAll(".red-ui-flow-subflow-port-output").remove();
                nodeLayer.selectAll(".red-ui-flow-subflow-port-input").remove();
                nodeLayer.selectAll(".red-ui-flow-subflow-port-status").remove();
            }

            var node = nodeLayer.selectAll(".red-ui-flow-node-group").data(activeNodes,function(d){return d.id});
            node.exit().each(function(d,i) {
                RED.hooks.trigger("viewRemoveNode",{node:d,el:this})
            }).remove();

            var nodeEnter = node.enter().insert("svg:g")
                .attr("class", "red-ui-flow-node red-ui-flow-node-group")
                .classed("red-ui-flow-subflow", activeSubflow != null);

            nodeEnter.each(function(d,i) {
                this.__outputs__ = [];
                this.__inputs__ = [];
                var node = d3.select(this);
                var nodeContents = document.createDocumentFragment();
                var isLink = (d.type === "link in" || d.type === "link out")
                var hideLabel = d.hasOwnProperty('l')?!d.l : isLink;
                node.attr("id",d.id);
                d.h = node_height;
                d.resize = true;

                if (d._def.button) {
                    var buttonGroup = document.createElementNS("http://www.w3.org/2000/svg","g");
                    buttonGroup.__data__ = d;
                    buttonGroup.setAttribute("transform", "translate("+((d._def.align == "right") ? 94 : -25)+",2)");
                    buttonGroup.setAttribute("class","red-ui-flow-node-button");
                    node[0][0].__buttonGroup__ = buttonGroup;

                    var bgBackground = document.createElementNS("http://www.w3.org/2000/svg","rect");
                    bgBackground.__data__ = d;
                    bgBackground.setAttribute("class","red-ui-flow-node-button-background");
                    bgBackground.setAttribute("rx",5);
                    bgBackground.setAttribute("ry",5);
                    bgBackground.setAttribute("width",32);
                    bgBackground.setAttribute("height",node_height-4);
                    buttonGroup.appendChild(bgBackground);
                    node[0][0].__buttonGroupBackground__ = bgBackground;

                    var bgButton = document.createElementNS("http://www.w3.org/2000/svg","rect");
                    bgButton.__data__ = d;
                    bgButton.setAttribute("class","red-ui-flow-node-button-button");
                    bgButton.setAttribute("x", d._def.align == "right"? 11:5);
                    bgButton.setAttribute("y",4);
                    bgButton.setAttribute("rx",4);
                    bgButton.setAttribute("ry",4);
                    bgButton.setAttribute("width",16);
                    bgButton.setAttribute("height",node_height-12);
                    bgButton.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def));
                    d3.select(bgButton)
                        .on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}})
                        .on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}})
                        .on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}})
                        .on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) {
                            var op = 1;
                            if (d._def.button.toggle) {
                                op = d[d._def.button.toggle]?1:0.2;
                            }
                            d3.select(this).attr("fill-opacity",op);
                        }})
                        .on("click",nodeButtonClicked)
                        .on("touchstart",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();})
                    buttonGroup.appendChild(bgButton);
                    node[0][0].__buttonGroupButton__ = bgButton;

                    nodeContents.appendChild(buttonGroup);

                }

                var mainRect = document.createElementNS("http://www.w3.org/2000/svg","rect");
                mainRect.__data__ = d;
                mainRect.setAttribute("class", "red-ui-flow-node "+(d.type == "unknown"?"red-ui-flow-node-unknown":""));
                mainRect.setAttribute("rx", 5);
                mainRect.setAttribute("ry", 5);
                mainRect.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def));
                node[0][0].__mainRect__ = mainRect;
                d3.select(mainRect)
                    .on("mouseup",nodeMouseUp)
                    .on("mousedown",nodeMouseDown)
                    .on("touchstart",nodeTouchStart)
                    .on("touchend",nodeTouchEnd)
                    .on("mouseover",nodeMouseOver)
                    .on("mouseout",nodeMouseOut);
                nodeContents.appendChild(mainRect);
                //node.append("rect").attr("class", "node-gradient-top").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-top)").style("pointer-events","none");
                //node.append("rect").attr("class", "node-gradient-bottom").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-bottom)").style("pointer-events","none");

                if (d._def.icon) {
                    var icon_url = RED.utils.getNodeIcon(d._def,d);
                    var icon_groupEl = document.createElementNS("http://www.w3.org/2000/svg","g");
                    icon_groupEl.__data__ = d;
                    icon_groupEl.setAttribute("class","red-ui-flow-node-icon-group"+("right" == d._def.align?" red-ui-flow-node-icon-group-right":""));
                    icon_groupEl.setAttribute("x",0);
                    icon_groupEl.setAttribute("y",0);
                    icon_groupEl.style["pointer-events"] = "none";
                    node[0][0].__iconGroup__ = icon_groupEl;
                    var icon_shade = document.createElementNS("http://www.w3.org/2000/svg","path");
                    icon_shade.setAttribute("x",0);
                    icon_shade.setAttribute("y",0);
                    icon_shade.setAttribute("class","red-ui-flow-node-icon-shade")
                    icon_groupEl.appendChild(icon_shade);
                    node[0][0].__iconShade__ = icon_shade;

                    var icon_group = d3.select(icon_groupEl)
                    createIconAttributes(icon_url, icon_group, d);

                    var icon_shade_border = document.createElementNS("http://www.w3.org/2000/svg","path");
                    icon_shade_border.setAttribute("d","right" != d._def.align ? "M 30 1 l 0 "+(d.h-2) : "M 0 1 l 0 "+(d.h-2)  )
                    icon_shade_border.setAttribute("class", "red-ui-flow-node-icon-shade-border")
                    icon_groupEl.appendChild(icon_shade_border);
                    node[0][0].__iconShadeBorder__ = icon_shade_border;

                    nodeContents.appendChild(icon_groupEl);
                }
                var text = document.createElementNS("http://www.w3.org/2000/svg","g");
                text.setAttribute("class","red-ui-flow-node-label"+(hideLabel?" hide":"")+(d._def.align?" red-ui-flow-node-label-"+d._def.align:""));
                text.setAttribute("transform","translate(38,0)");
                // text.setAttribute("dy", ".3px");
                // text.setAttribute("text-anchor",d._def.align !== "right" ? "start":"end");
                nodeContents.appendChild(text);
                node[0][0].__textGroup__ = text;

                var statusEl = document.createElementNS("http://www.w3.org/2000/svg","g");
                // statusEl.__data__ = d;
                statusEl.setAttribute("class","red-ui-flow-node-status-group");
                statusEl.style.display = "none";
                node[0][0].__statusGroup__ = statusEl;

                var statusBackground = document.createElementNS("http://www.w3.org/2000/svg","rect");
                statusBackground.setAttribute("class","red-ui-flow-node-status-background");
                statusBackground.setAttribute("x",3);
                statusBackground.setAttribute("y",-1);
                statusBackground.setAttribute("width",200);
                statusBackground.setAttribute("height",13);
                statusBackground.setAttribute("rx",2);
                statusBackground.setAttribute("ry",2);
                
                statusEl.appendChild(statusBackground);
                node[0][0].__statusBackground__ = statusBackground;


                var statusIcon = document.createElementNS("http://www.w3.org/2000/svg","rect");
                statusIcon.setAttribute("class","red-ui-flow-node-status");
                statusIcon.setAttribute("x",6);
                statusIcon.setAttribute("y",1);
                statusIcon.setAttribute("width",9);
                statusIcon.setAttribute("height",9);
                statusIcon.setAttribute("rx",2);
                statusIcon.setAttribute("ry",2);
                statusIcon.setAttribute("stroke-width","3");
                statusEl.appendChild(statusIcon);
                node[0][0].__statusShape__ = statusIcon;

                var statusLabel = document.createElementNS("http://www.w3.org/2000/svg","text");
                statusLabel.setAttribute("class","red-ui-flow-node-status-label");
                statusLabel.setAttribute("x",20);
                statusLabel.setAttribute("y",10);
                statusEl.appendChild(statusLabel);
                node[0][0].__statusLabel__ = statusLabel;

                nodeContents.appendChild(statusEl);

                node[0][0].appendChild(nodeContents);

                RED.hooks.trigger("viewAddNode",{node:d,el:this})
            });

            var nodesReordered = false;
            node.each(function(d,i) {
                if (d._reordered) {
                    nodesReordered = true;
                    delete d._reordered;
                }

                if (d.dirty) {
                    var self = this;
                    var thisNode = d3.select(this);

                    var isLink = (d.type === "link in" || d.type === "link out")
                    var hideLabel = d.hasOwnProperty('l')?!d.l : isLink;
                    dirtyNodes[d.id] = d;
                    //if (d.x < -50) deleteSelection();  // Delete nodes if dragged back to palette

                    var label = RED.utils.getNodeLabel(d, d.type);
                    var labelParts;
                    if (d.resize || this.__hideLabel__ !== hideLabel || this.__label__ !== label || this.__outputs__.length !== d.outputs) {
                        labelParts = getLabelParts(label, "red-ui-flow-node-label");
                        if (labelParts.lines.length !== this.__labelLineCount__ || this.__label__ !== label) {
                            d.resize = true;
                        }
                        this.__label__ = label;
                        this.__labelLineCount__ = labelParts.lines.length;

                        if (hideLabel) {
                            d.h = Math.max(node_height,(d.outputs || 0) * 15);
                        } else {
                            d.h = Math.max(6+24*labelParts.lines.length,(d.outputs || 0) * 15, 30);
                        }
                        this.__hideLabel__ = hideLabel;
                    }

                    if (d.resize) {
                        var ow = d.w;
                        if (hideLabel) {
                            d.w = node_height;
                        } else {
                            d.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(d._def.inputs>0?7:0))/20)) );
                        }
                        if (ow !== undefined) {
                            d.x += (d.w-ow)/2;
                        }
                        d.resize = false;
                    }
                    if (d._colorChanged) {
                        var newColor = RED.utils.getNodeColor(d.type,d._def);
                        this.__mainRect__.setAttribute("fill",newColor);
                        if (this.__buttonGroupButton__) {
                            this.__buttonGroupButton__.settAttribute("fill",newColor);
                        }
                        delete d._colorChanged;
                    }
                    //thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}});
                    this.setAttribute("transform", "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")");
                    // This might be the first redraw after a node has been click-dragged to start a move.
                    // So its selected state might have changed since the last redraw.
                    this.classList.toggle("red-ui-flow-node-selected", !!d.selected )
                    if (mouse_mode != RED.state.MOVING_ACTIVE) {
                        this.classList.toggle("red-ui-flow-node-disabled", d.d === true);
                        this.__mainRect__.setAttribute("width", d.w)
                        this.__mainRect__.setAttribute("height", d.h)
                        this.__mainRect__.classList.toggle("red-ui-flow-node-highlighted",!!d.highlighted );

                        if (labelParts) {
                            // The label has changed
                            var sa = labelParts.lines;
                            var sn = labelParts.lines.length;
                            var textLines = this.__textGroup__.childNodes;
                            while(textLines.length > sn) {
                                textLines[textLines.length-1].remove();
                            }
                            for (var i=0; i<sn; i++) {
                                if (i===textLines.length) {
                                    var line = document.createElementNS("http://www.w3.org/2000/svg","text");
                                    line.setAttribute("class","red-ui-flow-node-label-text");
                                    line.setAttribute("x",0);
                                    line.setAttribute("y",i*24);
                                    this.__textGroup__.appendChild(line);
                                }
                                textLines[i].textContent = sa[i];
                            }
                        }

                        var textClass = "";
                        if (d._def.labelStyle) {
                            textClass = d._def.labelStyle;
                            try {
                                textClass = (typeof textClass === "function" ? textClass.call(d) : textClass)||"";
                            } catch(err) {
                                console.log("Definition error: "+d.type+".labelStyle",err);
                                textClass = "";
                            }
                            textClass = " "+textClass;
                        }
                        textClass = "red-ui-flow-node-label"+(d._def.align?" red-ui-flow-node-label-"+d._def.align:"")+textClass+(hideLabel?" hide":"");
                        this.__textGroup__.setAttribute("class", textClass);

                        var yp = d.h / 2 - (this.__labelLineCount__ / 2) * 24 + 13;

                        if ((!d._def.align && d.inputs !== 0 && d.outputs === 0) || "right" === d._def.align) {
                            if (this.__iconGroup__) {
                                this.__iconGroup__.classList.add("red-ui-flow-node-icon-group-right");
                                this.__iconGroup__.setAttribute("transform", "translate("+(d.w-30)+",0)");
                            }
                            this.__textGroup__.classList.add("red-ui-flow-node-label-right");
                            this.__textGroup__.setAttribute("transform", "translate("+(d.w-38)+","+yp+")");
                        } else {
                            if (this.__iconGroup__) {// is null for uknown nodes
                                this.__iconGroup__.classList.remove("red-ui-flow-node-icon-group-right");
                                this.__iconGroup__.setAttribute("transform", "");
                            }
                            this.__textGroup__.classList.remove("red-ui-flow-node-label-right");
                            this.__textGroup__.setAttribute("transform", "translate(38,"+yp+")");
                        }

                        var inputPorts = thisNode.selectAll(".red-ui-flow-port-input");
                        if ((!isLink || (showAllLinkPorts === -1 && !activeLinkNodes[d.id])) && d.inputs === 0 && !inputPorts.empty()) {
                            inputPorts.each(function(d,i) {
                                RED.hooks.trigger("viewRemovePort",{
                                    node:d,
                                    el:self,
                                    port:d3.select(this)[0][0],
                                    portType: "input",
                                    portIndex: 0
                                })
                            }).remove();
                        } else if (((isLink && (showAllLinkPorts===PORT_TYPE_INPUT||activeLinkNodes[d.id]))|| d.inputs === 1) && inputPorts.empty()) {
                            var inputGroup = thisNode.append("g").attr("class","red-ui-flow-port-input");
                            var inputGroupPorts;

                            if (d.type === "link in") {
                                inputGroupPorts = inputGroup.append("circle")
                                    .attr("cx",-1).attr("cy",5)
                                    .attr("r",5)
                                    .attr("class","red-ui-flow-port red-ui-flow-link-port")
                            } else {
                                inputGroupPorts = inputGroup.append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
                            }
                            inputGroup[0][0].__port__ = inputGroupPorts[0][0];
                            inputGroupPorts[0][0].__data__ = this.__data__;
                            inputGroupPorts[0][0].__portType__ = PORT_TYPE_INPUT;
                            inputGroupPorts[0][0].__portIndex__ = 0;
                            inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);})
                                .on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();})
                                .on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} )
                                .on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
                                .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
                                .on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);});
                            RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0})
                        }
                        var numOutputs = d.outputs;
                        if (isLink && d.type === "link out") {
                            if (d.mode !== "return" && (showAllLinkPorts===PORT_TYPE_OUTPUT || activeLinkNodes[d.id])) {
                                numOutputs = 1;
                            } else {
                                numOutputs = 0;
                            }
                        }
                        var y = (d.h/2)-((numOutputs-1)/2)*13;

                        // Remove extra ports
                        while (this.__outputs__.length > numOutputs) {
                            var port = this.__outputs__.pop();
                            RED.hooks.trigger("viewRemovePort",{
                                node:d,
                                el:this,
                                port:port,
                                portType: "output",
                                portIndex: this.__outputs__.length
                            })
                            port.remove();
                        }
                        for(var portIndex = 0; portIndex < numOutputs; portIndex++ ) {
                            var portGroup;
                            if (portIndex === this.__outputs__.length) {
                                portGroup = document.createElementNS("http://www.w3.org/2000/svg","g");
                                portGroup.setAttribute("class","red-ui-flow-port-output");
                                var portPort;
                                if (d.type === "link out") {
                                    portPort = document.createElementNS("http://www.w3.org/2000/svg","circle");
                                    portPort.setAttribute("cx",11);
                                    portPort.setAttribute("cy",5);
                                    portPort.setAttribute("r",5);
                                    portPort.setAttribute("class","red-ui-flow-port red-ui-flow-link-port");
                                } else {
                                    portPort = document.createElementNS("http://www.w3.org/2000/svg","rect");
                                    portPort.setAttribute("rx",3);
                                    portPort.setAttribute("ry",3);
                                    portPort.setAttribute("width",10);
                                    portPort.setAttribute("height",10);
                                    portPort.setAttribute("class","red-ui-flow-port");
                                }
                                portGroup.appendChild(portPort);
                                portGroup.__port__ = portPort;
                                portPort.__data__ = this.__data__;
                                portPort.__portType__ = PORT_TYPE_OUTPUT;
                                portPort.__portIndex__ = portIndex;
                                portPort.addEventListener("mousedown", portMouseDownProxy);
                                portPort.addEventListener("touchstart", portTouchStartProxy);
                                portPort.addEventListener("mouseup", portMouseUpProxy);
                                portPort.addEventListener("touchend", portTouchEndProxy);
                                portPort.addEventListener("mouseover", portMouseOverProxy);
                                portPort.addEventListener("mouseout", portMouseOutProxy);

                                this.appendChild(portGroup);
                                this.__outputs__.push(portGroup);
                                RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex})
                            } else {
                                portGroup = this.__outputs__[portIndex];
                            }
                            var x = d.w - 5;
                            var y = (d.h/2)-((numOutputs-1)/2)*13;
                            portGroup.setAttribute("transform","translate("+x+","+((y+13*portIndex)-5)+")")
                        }
                        if (d._def.icon) {
                            var icon = thisNode.select(".red-ui-flow-node-icon");
                            var faIcon = thisNode.select(".fa-lg");
                            var current_url;
                            if (!icon.empty()) {
                                current_url = icon.attr("xlink:href");
                            } else {
                                current_url = faIcon.attr("xlink:href");
                            }
                            var new_url = RED.utils.getNodeIcon(d._def,d);
                            if (new_url !== current_url) {
                                if (!icon.empty()) {
                                    icon.remove();
                                } else {
                                    faIcon.remove();
                                }
                                var iconGroup = thisNode.select(".red-ui-flow-node-icon-group");
                                createIconAttributes(new_url, iconGroup, d);
                                icon = thisNode.select(".red-ui-flow-node-icon");
                                faIcon = thisNode.select(".fa-lg");
                            }

                            icon.attr("y",function(){return (d.h-d3.select(this).attr("height"))/2;});


                            const iconShadeHeight = d.h
                            const iconShadeWidth = 30
                            this.__iconShade__.setAttribute("d", hideLabel ?
                                `M5 0 h${iconShadeWidth-10} a 5 5 0 0 1 5 5 v${iconShadeHeight-10} a 5 5 0 0 1 -5 5 h-${iconShadeWidth-10} a 5 5 0 0 1 -5 -5  v-${iconShadeHeight-10} a 5 5 0 0 1 5 -5` : (
                                    "right" === d._def.align ?
                                    `M 0 0 h${iconShadeWidth-5} a 5 5 0 0 1 5 5 v${iconShadeHeight-10} a 5 5 0 0 1 -5 5 h-${iconShadeWidth-5} v-${iconShadeHeight}` :
                                    `M5 0 h${iconShadeWidth-5} v${iconShadeHeight} h-${iconShadeWidth-5} a 5 5 0 0 1 -5 -5  v-${iconShadeHeight-10} a 5 5 0 0 1 5 -5`
                                    )
                            )
                            this.__iconShadeBorder__.style.display = hideLabel?'none':''
                            this.__iconShadeBorder__.setAttribute("d",
                                                                  "M " + (((!d._def.align && d.inputs !== 0 && d.outputs === 0) || "right" === d._def.align) ? 0.5 : 29.5) + " "+(d.selected?1:0.5)+" l 0 " + (d.h - (d.selected?2:1))
                                                                 );
                            faIcon.attr("y",(d.h+13)/2);
                        }
                        // this.__changeBadge__.setAttribute("transform", "translate("+(d.w-10)+", -2)");
                        // this.__changeBadge__.classList.toggle("hide", !(d.changed||d.moved));
                        // this.__errorBadge__.setAttribute("transform", "translate("+(d.w-10-((d.changed||d.moved)?14:0))+", -2)");
                        // this.__errorBadge__.classList.toggle("hide", d.valid);

                        thisNode.selectAll(".red-ui-flow-port-input").each(function(d,i) {
                            var port = d3.select(this);
                            port.attr("transform",function(d){return "translate(-5,"+((d.h/2)-5)+")";})
                        });

                        if (d._def.button) {
                            var buttonEnabled = isButtonEnabled(d);
                            this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-disabled", !buttonEnabled);
                            if (RED.runtime && RED.runtime.started !== undefined) {
                                this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-stopped", !RED.runtime.started);
                            }

                            var x = d._def.align == "right"?d.w-6:-25;
                            if (d._def.button.toggle && !d[d._def.button.toggle]) {
                                x = x - (d._def.align == "right"?8:-8);
                            }
                            this.__buttonGroup__.setAttribute("transform", "translate("+x+",2)");

                            if (d._def.button.toggle) {
                                this.__buttonGroupButton__.setAttribute("fill-opacity",d[d._def.button.toggle]?1:0.2)
                                this.__buttonGroupBackground__.setAttribute("fill-opacity",d[d._def.button.toggle]?1:0.2)
                            }

                            if (typeof d._def.button.visible === "function") { // is defined and a function...
                                if (d._def.button.visible.call(d) === false) {
                                    this.__buttonGroup__.style.display = "none";
                                }
                                else {
                                    this.__buttonGroup__.style.display = "inherit";
                                }
                            }
                        }
                        // thisNode.selectAll(".node_badge_group").attr("transform",function(d){return "translate("+(d.w-40)+","+(d.h+3)+")";});
                        // thisNode.selectAll("text.node_badge_label").text(function(d,i) {
                        //     if (d._def.badge) {
                        //         if (typeof d._def.badge == "function") {
                        //             try {
                        //                 return d._def.badge.call(d);
                        //             } catch(err) {
                        //                 console.log("Definition error: "+d.type+".badge",err);
                        //                 return "";
                        //             }
                        //         } else {
                        //             return d._def.badge;
                        //         }
                        //     }
                        //     return "";
                        // });
                    }

                    if (d.dirtyStatus) {
                        redrawStatus(d,this);
                    }
                    d.dirty = false;
                    if (d.g) {
                        if (!dirtyGroups[d.g]) {
                            var gg = d.g;
                            while (gg && !dirtyGroups[gg]) {
                                dirtyGroups[gg] = RED.nodes.group(gg);
                                gg = dirtyGroups[gg].g;
                            }
                        }
                    }
                }

                RED.hooks.trigger("viewRedrawNode",{node:d,el:this})
            });

            if (nodesReordered) {
                node.sort(function(a,b) {
                    return a._index - b._index;
                })
            }

            var junction = junctionLayer.selectAll(".red-ui-flow-junction").data(
                activeJunctions,
                d => d.id
            )
            var junctionEnter = junction.enter().insert("svg:g").attr("class","red-ui-flow-junction")
            junctionEnter.each(function(d,i) {
                var junction = d3.select(this);
                var contents = document.createDocumentFragment();
                // d.added = true;
                var junctionBack = document.createElementNS("http://www.w3.org/2000/svg","rect");
                junctionBack.setAttribute("class","red-ui-flow-junction-background");
                junctionBack.setAttribute("x",-5);
                junctionBack.setAttribute("y",-5);
                junctionBack.setAttribute("width",10);
                junctionBack.setAttribute("height",10);
                junctionBack.setAttribute("rx",3);
                junctionBack.setAttribute("ry",3);
                junctionBack.__data__ = d;
                this.__junctionBack__ = junctionBack;
                contents.appendChild(junctionBack);

                var junctionInput = document.createElementNS("http://www.w3.org/2000/svg","rect");
                junctionInput.setAttribute("class","red-ui-flow-junction-port red-ui-flow-junction-port-input");
                junctionInput.setAttribute("x",-5);
                junctionInput.setAttribute("y",-5);
                junctionInput.setAttribute("width",10);
                junctionInput.setAttribute("height",10);
                junctionInput.setAttribute("rx",3);
                junctionInput.setAttribute("ry",3);
                junctionInput.__data__ = d;
                junctionInput.__portType__ = PORT_TYPE_INPUT;
                junctionInput.__portIndex__ = 0;
                this.__junctionInput__ = junctionOutput;
                contents.appendChild(junctionInput);
                junctionInput.addEventListener("mouseup", portMouseUpProxy);
                junctionInput.addEventListener("mousedown", portMouseDownProxy);


                this.__junctionInput__ = junctionInput;
                contents.appendChild(junctionInput);
                var junctionOutput = document.createElementNS("http://www.w3.org/2000/svg","rect");
                junctionOutput.setAttribute("class","red-ui-flow-junction-port red-ui-flow-junction-port-output");
                junctionOutput.setAttribute("x",-5);
                junctionOutput.setAttribute("y",-5);
                junctionOutput.setAttribute("width",10);
                junctionOutput.setAttribute("height",10);
                junctionOutput.setAttribute("rx",3);
                junctionOutput.setAttribute("ry",3);
                junctionOutput.__data__ = d;
                junctionOutput.__portType__ = PORT_TYPE_OUTPUT;
                junctionOutput.__portIndex__ = 0;
                this.__junctionOutput__ = junctionOutput;
                contents.appendChild(junctionOutput);
                junctionOutput.addEventListener("mouseup", portMouseUpProxy);
                junctionOutput.addEventListener("mousedown", portMouseDownProxy);
                junctionOutput.addEventListener("mouseover", junctionMouseOverProxy);
                junctionOutput.addEventListener("mouseout", junctionMouseOutProxy);
                junctionOutput.addEventListener("touchmove", junctionMouseOverProxy);
                junctionOutput.addEventListener("touchend", portMouseUpProxy);
                junctionOutput.addEventListener("touchstart", portMouseDownProxy);

                junctionInput.addEventListener("mouseover", junctionMouseOverProxy);
                junctionInput.addEventListener("mouseout", junctionMouseOutProxy);
                junctionInput.addEventListener("touchmove", junctionMouseOverProxy);
                junctionInput.addEventListener("touchend", portMouseUpProxy);
                junctionInput.addEventListener("touchstart", portMouseDownProxy);

                junctionBack.addEventListener("mouseover", junctionMouseOverProxy);
                junctionBack.addEventListener("mouseout", junctionMouseOutProxy);
                junctionBack.addEventListener("touchmove", junctionMouseOverProxy);

                // These handlers expect to be registered as d3 events
                d3.select(junctionBack).on("mousedown", nodeMouseDown).on("mouseup", nodeMouseUp);
                d3.select(junctionBack).on("touchstart", nodeMouseDown).on("touchend", nodeMouseUp);

                junction[0][0].appendChild(contents);
            })
            junction.exit().remove();
            junction.each(function(d) {
                var junction = d3.select(this);
                this.setAttribute("transform", "translate(" + (d.x) + "," + (d.y) + ")");
                if (d.dirty) {
                    junction.classed("red-ui-flow-junction-dragging", mouse_mode === RED.state.MOVING_ACTIVE && movingSet.has(d))
                    junction.classed("selected", !!d.selected)
                    dirtyNodes[d.id] = d;

                    if (d.g) {
                        if (!dirtyGroups[d.g]) {
                            var gg = d.g;
                            while (gg && !dirtyGroups[gg]) {
                                dirtyGroups[gg] = RED.nodes.group(gg);
                                gg = dirtyGroups[gg].g;
                            }
                        }
                    }

                }

            })

            var link = linkLayer.selectAll(".red-ui-flow-link").data(
                activeLinks,
                function(d) {
                    return d.source.id+":"+d.sourcePort+":"+d.target.id+":"+d.target.i;
                }
            );
            var linkEnter = link.enter().insert("g",".red-ui-flow-node").attr("class","red-ui-flow-link");

            linkEnter.each(function(d,i) {
                var l = d3.select(this);
                var pathContents = document.createDocumentFragment();

                d.added = true;
                var pathBack = document.createElementNS("http://www.w3.org/2000/svg","path");
                pathBack.__data__ = d;
                pathBack.setAttribute("class","red-ui-flow-link-background red-ui-flow-link-path"+(d.link?" red-ui-flow-link-link":""));
                this.__pathBack__ = pathBack;
                pathContents.appendChild(pathBack);
                d3.select(pathBack)
                    .on("mousedown",linkMouseDown)
                    .on("touchstart",linkTouchStart)
                    .on("mousemove", function(d) {
                        if (mouse_mode === RED.state.SLICING) {

                            selectedLinks.add(d)
                            l.classed("red-ui-flow-link-splice",true)
                            redraw()
                        } else if (mouse_mode === RED.state.SLICING_JUNCTION && !d.link) {
                            if (!l.classed("red-ui-flow-link-splice")) {
                                // Find intersection point
                                var lineLength = pathLine.getTotalLength();
                                var pos;
                                var delta = Infinity;
                                for (var i = 0; i < lineLength; i++) {
                                    var linePos = pathLine.getPointAtLength(i);
                                    var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
                                    var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
                                    var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
                                    if (posDelta < delta) {
                                        pos = linePos
                                        delta = posDelta
                                    }
                                }
                                d._sliceLocation = pos
                                selectedLinks.add(d)
                                l.classed("red-ui-flow-link-splice",true)
                                redraw()
                            }
                        }
                    })

                var pathOutline = document.createElementNS("http://www.w3.org/2000/svg","path");
                pathOutline.__data__ = d;
                pathOutline.setAttribute("class","red-ui-flow-link-outline red-ui-flow-link-path");
                this.__pathOutline__ = pathOutline;
                pathContents.appendChild(pathOutline);

                var pathLine = document.createElementNS("http://www.w3.org/2000/svg","path");
                pathLine.__data__ = d;
                pathLine.setAttribute("class","red-ui-flow-link-line red-ui-flow-link-path"+
                    (d.link?" red-ui-flow-link-link":(activeSubflow?" red-ui-flow-subflow-link":"")));
                this.__pathLine__ = pathLine;
                pathContents.appendChild(pathLine);

                l[0][0].appendChild(pathContents);
            });

            link.exit().remove();
            link.each(function(d) {
                var link = d3.select(this);
                if (d.added || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) {
                    var numOutputs = d.source.outputs || 1;
                    var sourcePort = d.sourcePort || 0;
                    var y = -((numOutputs-1)/2)*13 +13*sourcePort;
                    d.x1 = d.source.x+(d.source.w/2||0);
                    d.y1 = d.source.y+y;
                    d.x2 = d.target.x-(d.target.w/2||0);
                    d.y2 = d.target.y;

                    // return "M "+d.x1+" "+d.y1+
                    //     " C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+
                    //     (d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+
                    //     d.x2+" "+d.y2;
                    var path = generateLinkPath(d.x1,d.y1,d.x2,d.y2,1, !!(d.source.status || d.target.status));
                    if (/NaN/.test(path)) {
                        path = ""
                    }
                    this.__pathBack__.setAttribute("d",path);
                    this.__pathOutline__.setAttribute("d",path);
                    this.__pathLine__.setAttribute("d",path);
                    this.__pathLine__.classList.toggle("red-ui-flow-node-disabled",!!(d.source.d || d.target.d));
                    this.__pathLine__.classList.toggle("red-ui-flow-subflow-link", !d.link && activeSubflow);
                }

                this.classList.toggle("red-ui-flow-link-selected", !!d.selected);

                var connectedToUnknown = !!(d.target.type == "unknown" || d.source.type == "unknown");
                this.classList.toggle("red-ui-flow-link-unknown",!!(d.target.type == "unknown" || d.source.type == "unknown"))
                delete d.added;
            })
            var offLinks = linkLayer.selectAll(".red-ui-flow-link-off-flow").data(
                activeFlowLinks,
                function(d) {
                    return d.node.id+":"+d.refresh
                }
            );

            var offLinksEnter = offLinks.enter().insert("g",".red-ui-flow-node").attr("class","red-ui-flow-link-off-flow");
            offLinksEnter.each(function(d,i) {
                var g = d3.select(this);
                var s = 1;
                var labelAnchor = "start";
                if (d.node.type === "link in") {
                    s = -1;
                    labelAnchor = "end";
                }
                var stemLength = s*30;
                var branchLength = s*20;
                var l = g.append("svg:path").attr("class","red-ui-flow-link-link").attr("d","M 0 0 h "+stemLength);
                var links = d.links;
                var flows = Object.keys(links);
                var tabOrder = RED.nodes.getWorkspaceOrder();
                flows.sort(function(A,B) {
                    return tabOrder.indexOf(A) - tabOrder.indexOf(B);
                });
                var linkWidth = 10;
                var h = node_height;
                var y = -(flows.length-1)*h/2;
                var linkGroups = g.selectAll(".red-ui-flow-link-group").data(flows);
                var enterLinkGroups = linkGroups.enter().append("g").attr("class","red-ui-flow-link-group")
                    .on('mouseover', function() { if (mouse_mode !== 0) { return } d3.select(this).classed('red-ui-flow-link-group-active',true)})
                    .on('mouseout', function() {if (mouse_mode !== 0) { return } d3.select(this).classed('red-ui-flow-link-group-active',false)})
                    .on('mousedown', function() { d3.event.preventDefault(); d3.event.stopPropagation(); })
                    .on('mouseup', function(f) {
                        if (mouse_mode !== 0) {
                            return
                        }
                        d3.event.stopPropagation();
                        var targets = d.links[f];
                        RED.workspaces.show(f);
                        targets.forEach(function(n) {
                            n.selected = true;
                            n.dirty = true;
                            movingSet.add(n);
                            if (targets.length === 1) {
                                RED.view.reveal(n.id);
                            }
                        });
                        updateSelection();
                        redraw();
                    });
                enterLinkGroups.each(function(f) {
                    var linkG = d3.select(this);
                    linkG.append("svg:path")
                        .attr("class","red-ui-flow-link-link")
                        .attr("d",
                            "M "+stemLength+" 0 "+
                            "C "+(stemLength+(1.7*branchLength))+" "+0+
                            " "+(stemLength+(0.1*branchLength))+" "+y+" "+
                            (stemLength+branchLength*1.5)+" "+y+" "
                        );
                    linkG.append("svg:path")
                        .attr("class","red-ui-flow-link-port")
                        .attr("d",
                            "M "+(stemLength+branchLength*1.5+s*(linkWidth+7))+" "+(y-12)+" "+
                            "h "+(-s*linkWidth)+" "+
                            "a 3 3 45 0 "+(s===1?"0":"1")+" "+(s*-3)+" 3 "+
                            "v 18 "+
                            "a 3 3 45 0 "+(s===1?"0":"1")+" "+(s*3)+" 3 "+
                            "h "+(s*linkWidth)
                        );
                    linkG.append("svg:path")
                        .attr("class","red-ui-flow-link-port")
                        .attr("d",
                            "M "+(stemLength+branchLength*1.5+s*(linkWidth+10))+" "+(y-12)+" "+
                            "h "+(s*(linkWidth*3))+" "+
                            "M "+(stemLength+branchLength*1.5+s*(linkWidth+10))+" "+(y+12)+" "+
                            "h "+(s*(linkWidth*3))
                        ).style("stroke-dasharray","12 3 8 4 3");
                    linkG.append("rect").attr("class","red-ui-flow-port red-ui-flow-link-port")
                        .attr("x",stemLength+branchLength*1.5-4+(s*4))
                        .attr("y",y-4)
                        .attr("rx",2)
                        .attr("ry",2)
                        .attr("width",8)
                        .attr("height",8);
                    linkG.append("rect")
                        .attr("x",stemLength+branchLength*1.5-(s===-1?node_width:0))
                        .attr("y",y-12)
                        .attr("width",node_width)
                        .attr("height",24)
                        .style("stroke","none")
                        .style("fill","transparent")
                    var tab = RED.nodes.workspace(f);
                    var label;
                    if (tab) {
                        label = tab.label || tab.id;
                    }
                    linkG.append("svg:text")
                        .attr("class","red-ui-flow-port-label")
                        .attr("x",stemLength+branchLength*1.5+(s*15))
                        .attr("y",y+1)
                        .style("font-size","10px")
                        .style("text-anchor",labelAnchor)
                        .text(label);

                    y += h;
                });
                linkGroups.exit().remove();
            });
            offLinks.exit().remove();
            offLinks = linkLayer.selectAll(".red-ui-flow-link-off-flow");
            offLinks.each(function(d) {
                var s = 1;
                if (d.node.type === "link in") {
                    s = -1;
                }
                var link = d3.select(this);
                link.attr("transform", function(d) { return "translate(" + (d.node.x+(s*d.node.w/2)) + "," + (d.node.y) + ")"; });

            })

            var group = groupLayer.selectAll(".red-ui-flow-group").data(activeGroups,function(d) { return d.id });
            group.exit().each(function(d,i) {
                document.getElementById("group_select_"+d.id).remove()
            }).remove();
            var groupEnter = group.enter().insert("svg:g").attr("class", "red-ui-flow-group")
            var addedGroups = false;
            groupEnter.each(function(d,i) {
                addedGroups = true;
                var g = d3.select(this);
                g.attr("id",d.id);

                var groupBorderRadius = 4;
                var groupOutlineBorderRadius = 6
                var selectGroup = groupSelectLayer.append('g').attr("class", "red-ui-flow-group").attr("id","group_select_"+d.id);
                const groupBackground = selectGroup.append('rect')
                    .classed("red-ui-flow-group-outline-select",true)
                    .classed("red-ui-flow-group-outline-select-background",true)
                    .attr('rx',groupOutlineBorderRadius).attr('ry',groupOutlineBorderRadius)
                    .attr("x",-3)
                    .attr("y",-3);
                selectGroup.append('rect')
                    .classed("red-ui-flow-group-outline-select",true)
                    .classed("red-ui-flow-group-outline-select-outline",true)
                    .attr('rx',groupOutlineBorderRadius).attr('ry',groupOutlineBorderRadius)
                    .attr("x",-3)
                    .attr("y",-3)
                selectGroup.append('rect')
                    .classed("red-ui-flow-group-outline-select",true)
                    .classed("red-ui-flow-group-outline-select-line",true)
                    .attr('rx',groupOutlineBorderRadius).attr('ry',groupOutlineBorderRadius)
                    .attr("x",-3)
                    .attr("y",-3)
                groupBackground.on("mousedown", function() {groupMouseDown.call(g[0][0],d)});
                groupBackground.on("mouseup", function() {groupMouseUp.call(g[0][0],d)});
                groupBackground.on("touchstart", function() {groupMouseDown.call(g[0][0],d); d3.event.preventDefault();});
                groupBackground.on("touchend", function() {groupMouseUp.call(g[0][0],d); d3.event.preventDefault();});

                g.append('rect').classed("red-ui-flow-group-outline",true).attr('rx',0.5).attr('ry',0.5);

                g.append('rect').classed("red-ui-flow-group-body",true)
                    .attr('rx',groupBorderRadius).attr('ry',groupBorderRadius).style({
                        "fill":d.fill||"none",
                        "stroke": d.stroke||"none",
                    })
                g.on("mousedown",groupMouseDown).on("mouseup",groupMouseUp)
                g.on("touchstart", function() {groupMouseDown.call(g[0][0],d); d3.event.preventDefault();});
                g.on("touchend", function() {groupMouseUp.call(g[0][0],d); d3.event.preventDefault();});

                g.append('svg:text').attr("class","red-ui-flow-group-label");
                d.dirty = true;
            });
            if (addedGroups) {
                group.sort(function(a,b) {
                    return a._order - b._order
                })
            }
            group[0].reverse();
            var groupOpCount=0;
            group.each(function(d,i) {
                groupOpCount++
                if (d.resize) {
                    d.minWidth = 0;
                    delete d.resize;
                }
                if (d.dirty || dirtyGroups[d.id]) {
                    var g = d3.select(this);
                    var recalculateLabelOffsets = false;
                    if (d.nodes.length > 0) {
                        // If the group was just moved, all of its contents was
                        // also moved - so no need to recalculate its bounding box
                        if (!d.groupMoved) {
                            var minX = Number.POSITIVE_INFINITY;
                            var minY = Number.POSITIVE_INFINITY;
                            var maxX = 0;
                            var maxY = 0;
                            var margin = 26;
                            d.nodes.forEach(function(n) {
                                groupOpCount++
                                if (n._detachFromGroup) {
                                    // Do not include this node when recalulating
                                    // the group dimensions
                                    return
                                }
                                if (n.type !== "group") {
                                    minX = Math.min(minX,n.x-n.w/2-margin-((n._def.button && n._def.align!=="right")?20:0));
                                    minY = Math.min(minY,n.y-n.h/2-margin);
                                    maxX = Math.max(maxX,n.x+n.w/2+margin+((n._def.button && n._def.align=="right")?20:0));
                                    maxY = Math.max(maxY,n.y+n.h/2+margin);
                                } else {
                                    minX = Math.min(minX,n.x-margin)
                                    minY = Math.min(minY,n.y-margin)
                                    maxX = Math.max(maxX,n.x+n.w+margin)
                                    maxY = Math.max(maxY,n.y+n.h+margin)
                                }
                            });
                            if (minX !== Number.POSITIVE_INFINITY && minY !== Number.POSITIVE_INFINITY) {
                                d.x = minX;
                                d.y = minY;
                                d.w = maxX - minX;
                                d.h = maxY - minY;
                            }
                            recalculateLabelOffsets = true;
                            // if set explicitly to false, this group has just been
                            // imported so needed this initial resize calculation.
                            // Now that's done, delete the flag so the normal
                            // logic kicks in.
                            if (d.groupMoved === false) {
                                delete d.groupMoved;
                            }
                        } else {
                            delete d.groupMoved;
                        }
                    } else {
                        d.w = 40;
                        d.h = 40;
                        recalculateLabelOffsets = true;
                    }
                    if (recalculateLabelOffsets) {
                        if (!d.minWidth) {
                            if (d.style.label && d.name) {
                                var labelParts = getLabelParts(d.name||"","red-ui-flow-group-label");
                                d.minWidth = labelParts.width + 8;
                                d.labels = labelParts.lines;
                            } else {
                                d.minWidth = 40;
                                d.labels = [];
                            }
                        }
                        d.w = Math.max(d.minWidth,d.w);
                        if (d.style.label && d.labels.length > 0) {
                            var labelPos = d.style["label-position"] || "nw";
                            var h = (d.labels.length-1) * 16;
                            if (labelPos[0] === "s") {
                                h += 8;
                            }
                            d.h += h;
                            if (labelPos[0] === "n") {
                                if (d.nodes.length > 0) {
                                    d.y -= h;
                                }
                            }
                        }
                    }

                    g.attr("transform","translate("+d.x+","+d.y+")")
                    g.selectAll(".red-ui-flow-group-outline")
                        .attr("width",d.w)
                        .attr("height",d.h)


                    var selectGroup = document.getElementById("group_select_"+d.id);
                    selectGroup.setAttribute("transform","translate("+d.x+","+d.y+")");
                    if (d.hovered) {
                        selectGroup.classList.add("red-ui-flow-group-hovered")
                    } else {
                        selectGroup.classList.remove("red-ui-flow-group-hovered")
                    }
                    if (d.selected) {
                        selectGroup.classList.add("red-ui-flow-group-selected")
                    } else {
                        selectGroup.classList.remove("red-ui-flow-group-selected")
                    }
                    var selectGroupRect = selectGroup.children[0];
                    // Background
                    selectGroupRect.setAttribute("width",d.w+6)
                    selectGroupRect.setAttribute("height",d.h+6)
                    // Outline
                    selectGroupRect = selectGroup.children[1];
                    selectGroupRect.setAttribute("width",d.w+6)
                    selectGroupRect.setAttribute("height",d.h+6)
                    selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0;
                    // Line
                    selectGroupRect = selectGroup.children[2];
                    selectGroupRect.setAttribute("width",d.w+6)
                    selectGroupRect.setAttribute("height",d.h+6)
                    selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0;

                    if (d.highlighted) {
                        selectGroup.classList.add("red-ui-flow-node-highlighted");
                    } else {
                        selectGroup.classList.remove("red-ui-flow-node-highlighted");
                    }


                    g.selectAll(".red-ui-flow-group-body")
                        .attr("width",d.w)
                        .attr("height",d.h)
                        .style("stroke", d.style.stroke || "")
                        .style("stroke-opacity", d.style.hasOwnProperty('stroke-opacity') ? d.style['stroke-opacity'] : "")
                        .style("fill", d.style.fill || "")
                        .style("fill-opacity", d.style.hasOwnProperty('fill-opacity') ? d.style['fill-opacity'] : "")

                    var label = g.selectAll(".red-ui-flow-group-label");
                    label.classed("hide",!!!d.style.label)
                    if (d.style.label) {
                        var labelPos = d.style["label-position"] || "nw";
                        var labelX = 0;
                        var labelY = 0;

                        if (labelPos[0] === 'n') {
                            labelY = 0+15; // Allow for font-height
                        } else {
                            labelY = d.h - 5 -(d.labels.length -1) * 16;
                        }
                        if (labelPos[1] === 'w') {
                            labelX = 5;
                            labelAnchor = "start"
                        } else if (labelPos[1] === 'e') {
                            labelX = d.w-5;
                            labelAnchor = "end"
                        } else {
                            labelX = d.w/2;
                            labelAnchor = "middle"
                        }
                        if (d.style.hasOwnProperty('color')) {
                            label.style("fill",d.style.color)
                        } else {
                            label.style("fill",null)
                        }
                        label.attr("transform","translate("+labelX+","+labelY+")")
                             .attr("text-anchor",labelAnchor);
                        if (d.labels) {
                            var ypos = 0;
                            g.selectAll(".red-ui-flow-group-label-text").remove();
                            d.labels.forEach(function (name) {
                                label.append("tspan")
                                    .classed("red-ui-flow-group-label-text", true)
                                    .text(name)
                                    .attr("x", 0)
                                    .attr("y", ypos);
                                ypos += 16;
                            });
                        } else {
                            g.selectAll(".red-ui-flow-group-label-text").remove();
                        }
                    }

                    delete dirtyGroups[d.id];
                    delete d.dirty;
                }
            })
        } else {
            // JOINING - unselect any selected links
            linkLayer.selectAll(".red-ui-flow-link-selected").data(
                activeLinks,
                function(d) {
                    return d.source.id+":"+d.sourcePort+":"+d.target.id+":"+d.target.i;
                }
            ).classed("red-ui-flow-link-selected", false);
        }
        RED.view.navigator.refresh();
        if (d3.event) {
            d3.event.preventDefault();
        }
    }

    function focusView() {
        try {
            // Workaround for browser unexpectedly scrolling iframe into full
            // view - record the parent scroll position and restore it after
            // setting the focus
            var scrollX = window.parent.window.scrollX;
            var scrollY = window.parent.window.scrollY;
            chart.trigger("focus");
            window.parent.window.scrollTo(scrollX,scrollY);
        } catch(err) {
            // In case we're iframed into a page of a different origin, just focus
            // the view following the inevitable DOMException
            chart.trigger("focus");
        }
    }


    /**
     * Imports a new collection of nodes from a JSON String.
     *
     *  - all get new IDs assigned
     *  - all "selected"
     *  - attached to mouse for placing - "IMPORT_DRAGGING"
     * @param  {String/Array} newNodesObj nodes to import
     * @param  {Object} options options object
     *
     * Options:
     *  - addFlow - whether to import nodes to a new tab
     *  - touchImport - whether this is a touch import. If not, imported nodes are
     *                  attachedto mouse for placing - "IMPORT_DRAGGING" state
     *  - generateIds - whether to automatically generate new ids for all imported nodes
     *  - generateDefaultNames - whether to automatically update any nodes with clashing
     *                           default names
     */
    function importNodes(newNodesObj,options) {
        options = options || {
            addFlow: false,
            touchImport: false,
            generateIds: false,
            generateDefaultNames: false
        }
        var addNewFlow = options.addFlow
        var touchImport = options.touchImport;

        if (mouse_mode === RED.state.SELECTING_NODE) {
            return;
        }
        const wasDirty = RED.nodes.dirty()
        var nodesToImport;
        if (typeof newNodesObj === "string") {
            if (newNodesObj === "") {
                return;
            }
            try {
                nodesToImport = JSON.parse(newNodesObj);
            } catch(err) {
                var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
                e.code = "NODE_RED";
                throw e;
            }
        } else {
            nodesToImport = newNodesObj;
        }

        if (!Array.isArray(nodesToImport)) {
            nodesToImport = [nodesToImport];
        }
        if (options.generateDefaultNames) {
            RED.actions.invoke("core:generate-node-names", nodesToImport, {
                renameBlank: false,
                renameClash: true,
                generateHistory: false
            })
        }

        try {
            var activeSubflowChanged;
            if (activeSubflow) {
                activeSubflowChanged = activeSubflow.changed;
            }
            var filteredNodesToImport = nodesToImport;
            var globalConfig = null;
            var gconf = null;

            RED.nodes.eachConfig(function (conf) {
                if (conf.type === "global-config") {
                    gconf = conf;
                }
            });
            if (gconf) {
                filteredNodesToImport = nodesToImport.filter(function (n) {
                    return (n.type !== "global-config");
                });
                globalConfig = nodesToImport.find(function (n) {
                    return (n.type === "global-config");
                });
            }
            var result = RED.nodes.import(filteredNodesToImport,{
                generateIds: options.generateIds,
                addFlow: addNewFlow,
                importMap: options.importMap,
                markChanged: true
            });
            if (result) {
                var new_nodes = result.nodes;
                var new_links = result.links;
                var new_groups = result.groups;
                var new_junctions = result.junctions;
                var new_workspaces = result.workspaces;
                var new_subflows = result.subflows;
                var removedNodes = result.removedNodes;
                var new_default_workspace = result.missingWorkspace;
                if (addNewFlow && new_default_workspace) {
                    RED.workspaces.show(new_default_workspace.id);
                }
                var new_ms = new_nodes.filter(function(n) { return n.hasOwnProperty("x") && n.hasOwnProperty("y") && n.z == RED.workspaces.active() });
                new_ms = new_ms.concat(new_groups.filter(function(g) { return g.z === RED.workspaces.active()}))
                new_ms = new_ms.concat(new_junctions.filter(function(j) { return j.z === RED.workspaces.active()}))
                var new_node_ids = new_nodes.map(function(n){ return n.id; });

                clearSelection();
                movingSet.clear();
                movingSet.add(new_ms);


                // TODO: pick a more sensible root node
                if (movingSet.length() > 0) {
                    if (mouse_position == null) {
                        mouse_position = [0,0];
                    }

                    var dx = mouse_position[0];
                    var dy = mouse_position[1];
                    if (movingSet.length() > 0) {
                        var root_node = movingSet.get(0).n;
                        dx = root_node.x;
                        dy = root_node.y;
                    }

                    var minX = 0;
                    var minY = 0;
                    var i;
                    var node,group;
                    var l =movingSet.length();
                    for (i=0;i<l;i++) {
                        node = movingSet.get(i);
                        node.n.selected = true;
                        node.n.changed = true;
                        node.n.moved = true;
                        node.n.x -= dx - mouse_position[0];
                        node.n.y -= dy - mouse_position[1];
                        if (node.n.type !== 'junction') {
                            node.n.w = node_width;
                            node.n.h = node_height;
                            node.n.resize = true;
                        }
                        node.dx = node.n.x - mouse_position[0];
                        node.dy = node.n.y - mouse_position[1];
                        if (node.n.type === "group") {
                            node.n.groupMoved = false;
                            minX = Math.min(node.n.x-5,minX);
                            minY = Math.min(node.n.y-5,minY);
                        } else {
                            minX = Math.min(node.n.x-node_width/2-5,minX);
                            minY = Math.min(node.n.y-node_height/2-5,minY);
                        }
                    }
                    for (i=0;i<l;i++) {
                        node = movingSet.get(i);
                        node.n.x -= minX;
                        node.n.y -= minY;
                        node.dx -= minX;
                        node.dy -= minY;
                        if (node.n._def.onadd) {
                            try {
                                node.n._def.onadd.call(node.n);
                            } catch(err) {
                                console.log("Definition error: "+node.n.type+".onadd:",err);
                            }
                        }

                    }
                    if (!touchImport) {
                        mouse_mode = RED.state.IMPORT_DRAGGING;
                        startSelectionMove()  
                    }
                }

                var historyEvent = {
                    t: "add",
                    nodes: new_node_ids,
                    links: new_links,
                    groups: new_groups,
                    junctions: new_junctions,
                    workspaces: new_workspaces,
                    subflows: new_subflows,
                    dirty: wasDirty
                };
                if (movingSet.length() === 0) {
                    RED.nodes.dirty(true);
                }
                if (activeSubflow) {
                    var subflowRefresh = RED.subflow.refresh(true);
                    if (subflowRefresh) {
                        historyEvent.subflow = {
                            id: activeSubflow.id,
                            changed: activeSubflowChanged,
                            instances: subflowRefresh.instances
                        }
                    }
                }
                if (removedNodes) {
                    var replaceEvent = {
                        t: "replace",
                        config: removedNodes
                    }
                    historyEvent = {
                        t:"multi",
                        events: [
                            replaceEvent,
                            historyEvent
                        ]
                    }
                }

                if (globalConfig) {
                    // merge global env to existing global-config
                    var env0 = gconf.env;
                    var env1 = globalConfig.env;
                    var newEnv = Array.from(env0);
                    var changed = false;

                    env1.forEach(function (item1) {
                        var index = newEnv.findIndex(function (item0) {
                            return (item0.name === item1.name);
                        });
                        if (index >= 0) {
                            var item0 = newEnv[index];
                            if ((item0.type !== item1.type) ||
                                (item0.value !== item1.value)) {
                                newEnv[index] = item1;
                                changed = true;
                            }
                        }
                        else {
                            newEnv.push(item1);
                            changed = true;
                        }
                    });
                    if(changed) {
                        gconf.env = newEnv;
                        var replaceEvent = {
                            t: "edit",
                            node: gconf,
                            changed: true,
                            changes: {
                                env: env0
                            }
                        };
                        historyEvent = {
                            t:"multi",
                            events: [
                                replaceEvent,
                                historyEvent,
                            ]
                        };
                    }
                }

                RED.history.push(historyEvent);

                updateActiveNodes();
                redraw();

                var counts = [];
                var newNodeCount = 0;
                var newConfigNodeCount = 0;
                new_nodes.forEach(function(n) {
                    if (n.hasOwnProperty("x") && n.hasOwnProperty("y")) {
                        newNodeCount++;
                    } else {
                        newConfigNodeCount++;
                    }
                })
                var newGroupCount = new_groups.length;
                var newJunctionCount = new_junctions.length;
                if (new_workspaces.length > 0) {
                    counts.push(RED._("clipboard.flow",{count:new_workspaces.length}));
                }
                if (newNodeCount > 0) {
                    counts.push(RED._("clipboard.node",{count:newNodeCount}));
                }
                if (newGroupCount > 0) {
                    counts.push(RED._("clipboard.group",{count:newGroupCount}));
                }
                if (newConfigNodeCount > 0) {
                    counts.push(RED._("clipboard.configNode",{count:newConfigNodeCount}));
                }
                if (new_subflows.length > 0) {
                    counts.push(RED._("clipboard.subflow",{count:new_subflows.length}));
                }
                if (removedNodes && removedNodes.length > 0) {
                    counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length}));
                }
                if (counts.length > 0) {
                    var countList = "<ul><li>"+counts.join("</li><li>")+"</li></ul>";
                    RED.notify("<p>"+RED._("clipboard.nodesImported")+"</p>"+countList,{id:"clipboard"});
                }

            }
        } catch(error) {
            if (error.code === "import_conflict") {
                // Pass this up for the called to resolve
                throw error;
            } else if (error.code != "NODE_RED") {
                console.log(error.stack);
                RED.notify(RED._("notification.error",{message:error.toString()}),"error");
            } else {
                RED.notify(RED._("notification.error",{message:error.message}),"error");
            }
        }
    }

    function startSelectionMove() {
        spliceActive = false;
        if (movingSet.length() === 1) {
            const node = movingSet.get(0);
            spliceActive = node.n.hasOwnProperty("_def") &&
                           ((node.n.hasOwnProperty("inputs") && node.n.inputs > 0) || (!node.n.hasOwnProperty("inputs") && node.n._def.inputs > 0)) &&
                           ((node.n.hasOwnProperty("outputs") && node.n.outputs > 0) || (!node.n.hasOwnProperty("outputs") && node.n._def.outputs > 0)) &&
                           RED.nodes.filterLinks({ source: node.n }).length === 0 &&
                           RED.nodes.filterLinks({ target: node.n }).length === 0;
        }
        groupAddActive = false
        groupAddParentGroup = null
        if (movingSet.length() > 0 && activeGroups) {
            // movingSet includes the selection AND any nodes inside selected groups
            // So we cannot simply check the `g` of all nodes match.
            // Instead, we have to:
            // - note all groups in movingSet
            // - note all .g values referenced in movingSet
            // - then check for g values for groups not in movingSet
            let isValidSelection = true
            let hasNullGroup = false
            const selectedGroups = []
            const referencedGroups = new Set()
            movingSet.forEach(n => {
                if (n.n.type === 'subflow') {
                    isValidSelection = false
                }
                if (n.n.type === 'group') {
                    selectedGroups.push(n.n.id)
                }
                if (n.n.g) {
                    referencedGroups.add(n.n.g)
                } else {
                    hasNullGroup = true
                }
            })
            if (isValidSelection) {
                selectedGroups.forEach(g => referencedGroups.delete(g))
                // console.log('selectedGroups', selectedGroups)
                // console.log('referencedGroups',referencedGroups)
                // console.log('hasNullGroup', hasNullGroup)
                if (referencedGroups.size === 0) {
                    groupAddActive = true
                } else if (!hasNullGroup && referencedGroups.size === 1) {
                    groupAddParentGroup = referencedGroups.values().next().value
                    groupAddActive = true
                }
            }
            // console.log('groupAddActive', groupAddActive)
            // console.log('groupAddParentGroup', groupAddParentGroup)
        }
    }

    function toggleShowGrid(state) {
        if (state) {
            gridLayer.style("visibility","visible");
        } else {
            gridLayer.style("visibility","hidden");
        }
    }
    function toggleSnapGrid(state) {
        snapGrid = state;
        redraw();
    }
    function toggleStatus(s) {
        showStatus = s;
        RED.nodes.eachNode(function(n) { n.dirtyStatus = true; n.dirty = true;});
        //TODO: subscribe/unsubscribe here
        redraw();
    }
    function setSelectedNodeState(isDisabled) {
        if (mouse_mode === RED.state.SELECTING_NODE) {
            return;
        }
        if (activeFlowLocked) {
            return
        }
        var workspaceSelection = RED.workspaces.selection();
        var changed = false;
        if (workspaceSelection.length > 0) {
            // TODO: toggle workspace state
        } else if (movingSet.length() > 0) {
            var historyEvents = [];
            for (var i=0;i<movingSet.length();i++) {
                var node = movingSet.get(i).n;
                if (node.type !== "group" && node.type !== "subflow") {
                    if (isDisabled != node.d) {
                        historyEvents.push({
                            t: "edit",
                            node: node,
                            changed: node.changed,
                            changes: {
                                d: node.d
                            }
                        });
                        if (isDisabled) {
                            node.d = true;
                        } else {
                            delete node.d;
                        }
                        node.dirty = true;
                        node.dirtyStatus = true;
                        node.changed = true;
                        if (node.type === "junction") {
                            RED.events.emit("junctions:change",node);
                        }
                        else {
                            RED.events.emit("nodes:change",node);
                        }
                    }
                }
            }
            if (historyEvents.length > 0) {
                RED.history.push({
                    t:"multi",
                    events: historyEvents,
                    dirty:RED.nodes.dirty()
                })
                RED.nodes.dirty(true)
            }
        }
        RED.view.redraw();

    }
    function getSelection() {
        var selection = {};

        var allNodes = new Set();

        if (movingSet.length() > 0) {
            movingSet.forEach(function(n) {
                if (n.n.type !== 'group') {
                    allNodes.add(n.n);
                }
            });
        }
        selectedGroups.forEach(function(g) {
            var groupNodes = RED.group.getNodes(g,true);
            groupNodes.forEach(function(n) {
                allNodes.delete(n);
            });
            allNodes.add(g);
        });
        if (allNodes.size > 0) {
            selection.nodes = Array.from(allNodes);
        }
        if (selectedLinks.length() > 0) {
            selection.links = selectedLinks.toArray();
            selection.link = selection.links[0];
        }
        return selection;
    }

    /**
     * Create a node from a type string.
     * **NOTE:** Can throw on error - use `try` `catch` block when calling
     * @param {string} type The node type to create
     * @param {number} [x] (optional) The horizontal position on the workspace
     * @param {number} [y] (optional)The vertical on the workspace
     * @param {string} [z] (optional) The flow tab this node will belong to. Defaults to active workspace.
     * @returns An object containing the `node` and a `historyEvent`
     * @private
     */
     function createNode(type, x, y, z) {
        const wasDirty = RED.nodes.dirty()
        var m = /^subflow:(.+)$/.exec(type);
        var activeSubflow = (z || RED.workspaces.active()) ? RED.nodes.subflow(z || RED.workspaces.active()) : null;

        if (activeSubflow && m) {
            var subflowId = m[1];
            let err
            if (subflowId === activeSubflow.id) {
                err = new Error(RED._("notification.errors.cannotAddSubflowToItself"))
            } else if (RED.nodes.subflowContains(m[1], activeSubflow.id)) {
                err = new Error(RED._("notification.errors.cannotAddCircularReference"))
            }
            if (err) {
                err.code = 'NODE_RED'
                throw err
            }
        }

        var nn = { id: RED.nodes.id(), z: z || RED.workspaces.active() };

        nn.type = type;
        nn._def = RED.nodes.getType(nn.type);

        if (!m) {
            nn.inputs = nn._def.inputs || 0;
            nn.outputs = nn._def.outputs;

            for (var d in nn._def.defaults) {
                if (nn._def.defaults.hasOwnProperty(d)) {
                    if (nn._def.defaults[d].value !== undefined) {
                        nn[d] = JSON.parse(JSON.stringify(nn._def.defaults[d].value));
                    }
                }
            }

            if (nn._def.onadd) {
                try {
                    nn._def.onadd.call(nn);
                } catch (err) {
                    console.log("Definition error: " + nn.type + ".onadd:", err);
                }
            }
        } else {
            var subflow = RED.nodes.subflow(m[1]);
            nn.name = "";
            nn.inputs = subflow.in.length;
            nn.outputs = subflow.out.length;
        }

        nn.changed = true;
        nn.moved = true;

        nn.w = RED.view.node_width;
        nn.h = Math.max(RED.view.node_height, (nn.outputs || 0) * 15);
        nn.resize = true;
        if (x != null && typeof x == "number" && x >= 0) {
            nn.x = x;
        }
        if (y != null && typeof y == "number" && y >= 0) {
            nn.y = y;
        }
        var historyEvent = {
            t: "add",
            nodes: [nn.id],
            dirty: wasDirty
        }
        if (activeSubflow) {
            var subflowRefresh = RED.subflow.refresh(true);
            if (subflowRefresh) {
                historyEvent.subflow = {
                    id: activeSubflow.id,
                    changed: activeSubflow.changed,
                    instances: subflowRefresh.instances
                }
            }
        }
        return {
            node: nn,
            historyEvent: historyEvent
        }
    }

    function calculateNodeDimensions(node) {
        var result = [node_width,node_height];
        try {
        var isLink = (node.type === "link in" || node.type === "link out")
        var hideLabel = node.hasOwnProperty('l')?!node.l : isLink;
        var label = RED.utils.getNodeLabel(node, node.type);
        var labelParts = getLabelParts(label, "red-ui-flow-node-label");
        if (hideLabel) {
            result[1] = Math.max(node_height,(node.outputs || 0) * 15);
        } else {
            result[1] = Math.max(6+24*labelParts.lines.length,(node.outputs || 0) * 15, 30);
        }
        if (hideLabel) {
            result[0] = node_height;
        } else {
            result[0] = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(node._def.inputs>0?7:0))/20)) );
        }
    }catch(err) {
        console.log("Error",node);
    }
        return result;
    }


    function flashNode(n) {
        let node = n;
        if(typeof node === "string") { node = RED.nodes.node(n); }
        if(!node) { return; }

        const flashingNode = flashingNodeId && RED.nodes.node(flashingNodeId);
        if(flashingNode) {
            //cancel current flashing node before flashing new node
            clearInterval(flashingNode.__flashTimer);
            delete flashingNode.__flashTimer;
            flashingNode.dirty = true;
            flashingNode.highlighted = false;
        }
        node.__flashTimer = setInterval(function(flashEndTime, n) {
            n.dirty = true;
            if (flashEndTime >= Date.now()) {
                n.highlighted = !n.highlighted;
            } else {
                clearInterval(n.__flashTimer);
                delete n.__flashTimer;
                flashingNodeId = null;
                n.highlighted = false;
            }
            RED.view.redraw();
        }, 100, Date.now() + 2200, node)
        flashingNodeId = node.id;
        node.highlighted = true;
        RED.view.redraw();
    }
    return {
        init: init,
        state:function(state) {
            if (state == null) {
                return mouse_mode
            } else {
                mouse_mode = state;
            }
        },

        updateActive: updateActiveNodes,
        redraw: function(updateActive, syncRedraw) {
            if (updateActive) {
                updateActiveNodes();
                updateSelection();
            }
            if (syncRedraw) {
                _redraw();
            } else {
                redraw();
            }
        },
        focus: focusView,
        importNodes: importNodes,
        calculateTextWidth: calculateTextWidth,
        select: function(selection) {
            if (typeof selection !== "undefined") {
                clearSelection();
                if (typeof selection == "string") {
                    var selectedNode = RED.nodes.node(selection);
                    if (selectedNode) {
                        selectedNode.selected = true;
                        selectedNode.dirty = true;
                        movingSet.clear();
                        movingSet.add(selectedNode);
                    } else {
                        selectedNode = RED.nodes.group(selection);
                        if (selectedNode) {
                            movingSet.clear();
                            selectedGroups.clear()
                            selectedGroups.add(selectedNode)
                        }
                    }
                } else if (selection) {
                    if (selection.nodes) {
                        updateActiveNodes();
                        movingSet.clear();
                        // TODO: this selection group span groups
                        //  - if all in one group -> activate the group
                        //  - if in multiple groups (or group/no-group)
                        //      -> select the first 'set' of things in the same group/no-group
                        selection.nodes.forEach(function(n) {
                            if (n.type !== "group") {
                                n.selected = true;
                                n.dirty = true;
                                movingSet.add(n);
                            } else {
                                selectedGroups.add(n,true);
                            }
                        })
                    }
                    if (selection.links) {
                        selectedLinks.clear();
                        selection.links.forEach(selectedLinks.add);
                    }
                }
            }
            updateSelection();
            redraw(true);
        },
        selection: getSelection,
        clearSelection: clearSelection,
        createNode: createNode,
        /** default node width */
        get node_width() {
            return node_width;
        },
        /** default node height */
        get node_height() {
            return node_height;
        },
        /** snap to grid option state */
        get snapGrid() {
            return snapGrid;
        },
        /** gets the current scale factor */
        scale: function() {
            return scaleFactor;
        },
        getLinksAtPoint: function(x,y) {
            // x,y must be in SVG co-ordinate space
            // if they come from a node.x/y, they will need to be scaled using
            // scaleFactor first.
            var result = [];
            var links = outer.selectAll(".red-ui-flow-link-background")[0];
            for (var i=0;i<links.length;i++) {
                var bb = links[i].getBBox();
                if (x >= bb.x && y >= bb.y && x <= bb.x+bb.width && y <= bb.y+bb.height) {
                    result.push(links[i])
                }
            }
            return result;
        },
        getGroupAtPoint: getGroupAt,
        getActiveGroup: function() { return null },
        reveal: function(id,triggerHighlight) {
            if (RED.nodes.workspace(id) || RED.nodes.subflow(id)) {
                RED.workspaces.show(id, null, null, true);
            } else {
                var node = RED.nodes.node(id) || RED.nodes.group(id);
                if (node) {
                    if (node.z && (node.type === "group" || node._def.category !== 'config')) {
                        node.dirty = true;
                        RED.workspaces.show(node.z);
                        if (node.type === "group" && !node.w && !node.h) {
                            _redraw();
                        }
                        var screenSize = [chart[0].clientWidth/scaleFactor,chart[0].clientHeight/scaleFactor];
                        var scrollPos = [chart.scrollLeft()/scaleFactor,chart.scrollTop()/scaleFactor];
                        var cx = node.x;
                        var cy = node.y;
                        if (node.type === "group") {
                            cx += node.w/2;
                            cy += node.h/2;
                        }
                        if (cx < scrollPos[0] || cy < scrollPos[1] || cx > screenSize[0]+scrollPos[0] || cy > screenSize[1]+scrollPos[1]) {
                            var deltaX = '-='+(((scrollPos[0] - cx) + screenSize[0]/2)*scaleFactor);
                            var deltaY = '-='+(((scrollPos[1] - cy) + screenSize[1]/2)*scaleFactor);
                            chart.animate({
                                scrollLeft: deltaX,
                                scrollTop: deltaY
                            },200);
                        }
                        if (triggerHighlight !== false) {
                            flashNode(node);
                        }
                    } else if (node._def.category === 'config') {
                        RED.sidebar.config.show(id);
                    }
                }
            }
        },
        gridSize: function(v) {
            if (v === undefined) {
                return gridSize;
            } else {
                gridSize = Math.max(5,v);
                updateGrid();
            }
        },
        getActiveNodes: function() {
            return activeNodes;
        },
        getSubflowPorts: function() {
            var result = [];
            if (activeSubflow) {
                var subflowOutputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-output").data(activeSubflow.out,function(d,i){ return d.id;});
                subflowOutputs.each(function(d,i) { result.push(d) })
                var subflowInputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-input").data(activeSubflow.in,function(d,i){ return d.id;});
                subflowInputs.each(function(d,i) { result.push(d) })
                var subflowStatus = nodeLayer.selectAll(".red-ui-flow-subflow-port-status").data(activeSubflow.status?[activeSubflow.status]:[],function(d,i){ return d.id;});
                subflowStatus.each(function(d,i) { result.push(d) })
            }
            return result;
        },
        selectNodes: function(options) {
            $("#red-ui-workspace-tabs-shade").show();
            $("#red-ui-palette-shade").show();
            $("#red-ui-sidebar-shade").show();
            $("#red-ui-header-shade").show();
            $("#red-ui-workspace").addClass("red-ui-workspace-select-mode");

            mouse_mode = RED.state.SELECTING_NODE;
            clearSelection();
            if (options.selected) {
                options.selected.forEach(function(id) {
                    var n = RED.nodes.node(id);
                    if (n) {
                        n.selected = true;
                        n.dirty = true;
                        movingSet.add(n);
                    }
                })
            }
            redraw();
            selectNodesOptions = options||{};
            var closeNotification = function() {
                clearSelection();
                $("#red-ui-workspace-tabs-shade").hide();
                $("#red-ui-palette-shade").hide();
                $("#red-ui-sidebar-shade").hide();
                $("#red-ui-header-shade").hide();
                $("#red-ui-workspace").removeClass("red-ui-workspace-select-mode");
                resetMouseVars();
                notification.close();
            }
            selectNodesOptions.done = function(selection) {
                closeNotification();
                if (selectNodesOptions.onselect) {
                    selectNodesOptions.onselect(selection);
                }
            }
            var buttons = [{
                text: RED._("common.label.cancel"),
                click: function(e) {
                    closeNotification();
                    if (selectNodesOptions.oncancel) {
                        selectNodesOptions.oncancel();
                    }
                }
            }];
            if (!selectNodesOptions.single) {
                buttons.push({
                    text: RED._("common.label.done"),
                    class: "primary",
                    click: function(e) {
                        var selection = movingSet.nodes()
                        selectNodesOptions.done(selection);
                    }
                });
            }
            var notification = RED.notify(selectNodesOptions.prompt || RED._("workspace.selectNodes"),{
                modal: false,
                fixed: true,
                type: "compact",
                buttons: buttons
            })
        },
        scroll: function(x,y) {
            if (x !== undefined && y !== undefined) {
                chart.scrollLeft(chart.scrollLeft()+x);
                chart.scrollTop(chart.scrollTop()+y)
            } else {
                return [chart.scrollLeft(), chart.scrollTop()]
            }
        },
        clickNodeButton: function(n) {
            if (n._def.button) {
                nodeButtonClicked(n);
            }
        },
        clipboard: function() {
            return clipboard
        },
        redrawStatus: redrawStatus,
        showQuickAddDialog:showQuickAddDialog,
        calculateNodeDimensions: calculateNodeDimensions,
        getElementPosition:getElementPosition,
        showTooltip:showTooltip,
        dimensions: function() {
            return {
                width: space_width,
                height: space_height
            };
        }
    };
})();
;RED.view.annotations = (function() {

    var annotations = {};

    function init() {
        RED.hooks.add("viewRedrawNode.annotations", function(evt) {
            try {
                if (evt.node.__pendingAnnotation__) {
                    addAnnotation(evt.node.__pendingAnnotation__,evt);
                    delete evt.node.__pendingAnnotation__;
                }
                let badgeRDX = 0;
                let badgeLDX = 0;
                const scale = RED.view.scale()
                for (let i=0,l=evt.el.__annotations__.length;i<l;i++) {
                    const annotation = evt.el.__annotations__[i];
                    if (annotations.hasOwnProperty(annotation.id)) {
                        const opts = annotations[annotation.id];
                        let showAnnotation = true;
                        const isBadge = opts.type === 'badge';
                        if (opts.refresh !== undefined) {
                            let refreshAnnotation = false
                            if (typeof opts.refresh === "string") {
                                refreshAnnotation = !!evt.node[opts.refresh]
                                delete evt.node[opts.refresh]
                            } else if (typeof opts.refresh === "function") {
                                refreshAnnotation = opts.refresh(evnt.node)
                            }
                            if (refreshAnnotation) {
                                refreshAnnotationElement(annotation.id, annotation.node, annotation.element)
                            }
                        }
                        if (opts.show !== undefined) {
                            if (typeof opts.show === "string") {
                                showAnnotation = !!evt.node[opts.show]
                            } else if (typeof opts.show === "function"){
                                showAnnotation = opts.show(evt.node)
                            } else {
                                showAnnotation = !!opts.show;
                            }
                            annotation.element.classList.toggle("hide", !showAnnotation);
                        }
                        if (isBadge) {
                            if (showAnnotation) {
                                // getBoundingClientRect is in real-world scale so needs to be adjusted according to
                                // the current scale factor
                                const rectWidth = annotation.element.getBoundingClientRect().width / scale;
                                let annotationX
                                if (!opts.align || opts.align === 'right') {
                                    annotationX = evt.node.w - 3 - badgeRDX - rectWidth
                                    badgeRDX += rectWidth + 4;

                                } else if (opts.align === 'left') {
                                    annotationX = 3 + badgeLDX
                                    badgeLDX += rectWidth + 4;
                                }
                                annotation.element.setAttribute("transform", "translate("+annotationX+", -8)");
                            }
                        // } else {
                        //     if (showAnnotation) {
                        //         var rect = annotation.element.getBoundingClientRect();
                        //         annotation.element.setAttribute("transform", "translate("+(3+controlDX)+", -12)");
                        //         controlDX += rect.width + 4;
                        //     }
                        }
                    } else {
                        annotation.element.parentNode.removeChild(annotation.element);
                        evt.el.__annotations__.splice(i,1);
                        i--;
                        l--;
                    }
                }
        }catch(err) {
            console.log(err)
        }
        });
    }


    /**
     * Register a new node annotation
     * @param {string} id - unique identifier
     * @param {type} opts - annotations options
     *
     * opts: {
     *   type: "badge"
     *   class: "",
     *   element: function(node),
     *   show: string|function(node),
     *   filter: function(node) -> boolean
     * }
     */
    function register(id, opts) {
        if (opts.type !== 'badge') {
            throw new Error("Unsupported annotation type: "+opts.type);
        }
        annotations[id] = opts
        RED.hooks.add("viewAddNode.annotation-"+id, function(evt) {
            if (opts.filter && !opts.filter(evt.node)) {
                return;
            }
            addAnnotation(id,evt);
        });

        var nodes = RED.view.getActiveNodes();
        nodes.forEach(function(n) {
            n.__pendingAnnotation__ = id;
        })
        RED.view.redraw();

    }

    function addAnnotation(id,evt) {
        var opts = annotations[id];
        evt.el.__annotations__ = evt.el.__annotations__ || [];
        var annotationGroup = document.createElementNS("http://www.w3.org/2000/svg","g");
        annotationGroup.setAttribute("class",opts.class || "");
        evt.el.__annotations__.push({
            id:id,
            node: evt.node,
            element: annotationGroup
        });
        refreshAnnotationElement(id, evt.node, annotationGroup)
        evt.el.appendChild(annotationGroup);
    }

    function refreshAnnotationElement(id, node, annotationGroup) {
        const opts = annotations[id];
        const annotation = opts.element(node);
        if (opts.tooltip) {
            annotation.addEventListener("mouseenter", getAnnotationMouseEnter(annotation, node, opts.tooltip));
            annotation.addEventListener("mouseleave", annotationMouseLeave);
        }
        if (annotationGroup.hasChildNodes()) {
            annotationGroup.removeChild(annotationGroup.firstChild)
        }
        annotationGroup.appendChild(annotation);

    }


    function unregister(id) {
        delete annotations[id]
        RED.hooks.remove("*.annotation-"+id);
        RED.view.redraw();
    }

    var badgeHoverTimeout;
    var badgeHover;
    function getAnnotationMouseEnter(annotation,node,tooltip) {
        return function() {
            var text = typeof tooltip === "function"?tooltip(node):tooltip;
            if (text) {
                clearTimeout(badgeHoverTimeout);
                badgeHoverTimeout = setTimeout(function() {
                    var pos = RED.view.getElementPosition(annotation);
                    var rect = annotation.getBoundingClientRect();
                    badgeHoverTimeout = null;
                    badgeHover = RED.view.showTooltip(
                        (pos[0]+rect.width/2),
                        (pos[1]),
                        text,
                        "top"
                    );
                },500);
            }
        }
    }
    function annotationMouseLeave() {
        clearTimeout(badgeHoverTimeout);
        if (badgeHover) {
            badgeHover.remove();
            badgeHover = null;
        }
    }

    return {
        init: init,
        register:register,
        unregister:unregister
    }

})();
;/**
 * Copyright 2016 IBM Corp.
 *
 * 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.
 **/


 RED.view.navigator = (function() {

     var nav_scale = 50;
     var nav_width = 8000/nav_scale;
     var nav_height = 8000/nav_scale;

     var navContainer;
     var navBox;
     var navBorder;
     var navVis;
     var scrollPos;
     var scaleFactor;
     var chartSize;
     var dimensions;
     var isDragging;
     var isShowing = false;

     function refreshNodes() {
         if (!isShowing) {
             return;
         }
         var navNode = navVis.selectAll(".red-ui-navigator-node").data(RED.view.getActiveNodes(),function(d){return d.id});
         navNode.exit().remove();
         navNode.enter().insert("rect")
             .attr('class','red-ui-navigator-node')
             .attr("pointer-events", "none");
         navNode.each(function(d) {
             d3.select(this).attr("x",function(d) { return (d.x-d.w/2)/nav_scale })
             .attr("y",function(d) { return (d.y-d.h/2)/nav_scale })
             .attr("width",function(d) { return Math.max(9,d.w/nav_scale) })
             .attr("height",function(d) { return Math.max(3,d.h/nav_scale) })
             .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def);})
         });
     }
     function onScroll() {
         if (!isDragging) {
             resizeNavBorder();
         }
     }
     function resizeNavBorder() {
         if (navBorder) {
             scaleFactor = RED.view.scale();
             chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
             scrollPos = [$("#red-ui-workspace-chart").scrollLeft(),$("#red-ui-workspace-chart").scrollTop()];
             navBorder.attr('x',scrollPos[0]/nav_scale)
                      .attr('y',scrollPos[1]/nav_scale)
                      .attr('width',chartSize[0]/nav_scale/scaleFactor)
                      .attr('height',chartSize[1]/nav_scale/scaleFactor)
         }
     }
     function toggle() {
         if (!isShowing) {
             isShowing = true;
             $("#red-ui-view-navigate").addClass("selected");
             resizeNavBorder();
             refreshNodes();
             $("#red-ui-workspace-chart").on("scroll",onScroll);
             navContainer.fadeIn(200);
         } else {
             isShowing = false;
             navContainer.fadeOut(100);
             $("#red-ui-workspace-chart").off("scroll",onScroll);
             $("#red-ui-view-navigate").removeClass("selected");
         }
     }

     return {
         init: function() {

             $(window).on("resize", resizeNavBorder);
             RED.events.on("sidebar:resize",resizeNavBorder);
             RED.actions.add("core:toggle-navigator",toggle);
             var hideTimeout;

             navContainer = $('<div>').css({
                 "position":"absolute",
                 "bottom":$("#red-ui-workspace-footer").height(),
                 "right":0,
                 zIndex: 1
             }).appendTo("#red-ui-workspace").hide();

             navBox = d3.select(navContainer[0])
                 .append("svg:svg")
                 .attr("width", nav_width)
                 .attr("height", nav_height)
                 .attr("pointer-events", "all")
                 .attr("id","red-ui-navigator-canvas")

             navBox.append("rect").attr("x",0).attr("y",0).attr("width",nav_width).attr("height",nav_height).style({
                 fill:"none",
                 stroke:"none",
                 pointerEvents:"all"
             }).on("mousedown", function() {
                 // Update these in case they have changed
                 scaleFactor = RED.view.scale();
                 chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
                 dimensions = [chartSize[0]/nav_scale/scaleFactor, chartSize[1]/nav_scale/scaleFactor];
                 var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
                 var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
                 navBorder.attr('x',newX).attr('y',newY);
                 isDragging = true;
                 $("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
                 $("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
             }).on("mousemove", function() {
                 if (!isDragging) { return }
                 if (d3.event.buttons === 0) {
                     isDragging = false;
                     return;
                 }
                 var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
                 var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
                 navBorder.attr('x',newX).attr('y',newY);
                 $("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
                 $("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
             }).on("mouseup", function() {
                 isDragging = false;
             })

             navBorder = navBox.append("rect").attr("class","red-ui-navigator-border")

             navVis = navBox.append("svg:g")

             RED.statusBar.add({
                 id: "view-navigator",
                 align: "right",
                 element: $('<button class="red-ui-footer-button-toggle single" id="red-ui-view-navigate"><i class="fa fa-map-o"></i></button>')
             })

            $("#red-ui-view-navigate").on("click", function(evt) {
                evt.preventDefault();
                toggle();
            })
            RED.popover.tooltip($("#red-ui-view-navigate"),RED._('actions.toggle-navigator'),'core:toggle-navigator');
        },
        refresh: refreshNodes,
        resize: resizeNavBorder,
        toggle: toggle
    }


})();
;/**
 * 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.
 **/

RED.view.tools = (function() {
    'use strict';
    function selectConnected(type) {
        var selection = RED.view.selection();
        var visited = new Set();
        if (selection.nodes && selection.nodes.length > 0) {
            selection.nodes.forEach(function(n) {
                if (!visited.has(n)) {
                    var connected;
                    if (type === 'all') {
                        connected = RED.nodes.getAllFlowNodes(n);
                    } else if (type === 'up') {
                        connected = [n].concat(RED.nodes.getAllUpstreamNodes(n));
                    } else if (type === 'down') {
                        connected = [n].concat(RED.nodes.getAllDownstreamNodes(n));
                    }
                    connected.forEach(function(nn) { visited.add(nn) })
                }
            });
            RED.view.select({nodes:Array.from(visited)});
        }

    }

    function alignToGrid() {
        if (RED.workspaces.isLocked()) {
            return
        }
        var selection = RED.view.selection();
        if (selection.nodes) {
            var changedNodes = [];
            selection.nodes.forEach(function(n) {
                var x = n.w/2 + Math.round((n.x-n.w/2)/RED.view.gridSize())*RED.view.gridSize();
                var y = Math.round(n.y/RED.view.gridSize())*RED.view.gridSize();
                if (n.x !== x || n.y !== y) {
                    changedNodes.push({
                        n:n,
                        ox: n.x,
                        oy: n.y,
                        moved: n.moved
                    });
                    n.x = x;
                    n.y = y;
                    n.dirty = true;
                    n.moved = true;
                }
            });
            if (changedNodes.length > 0) {
                RED.history.push({t:"move",nodes:changedNodes,dirty:RED.nodes.dirty()});
                RED.nodes.dirty(true);
                RED.view.redraw(true);
            }
        }
    }

    var moving_set = null;
    var endMoveSet = false;
    function endKeyboardMove() {
        endMoveSet = false;
        if (moving_set.length > 0) {
            var ns = [];
            for (var i=0;i<moving_set.length;i++) {
                ns.push({n:moving_set[i].n,ox:moving_set[i].ox,oy:moving_set[i].oy,moved:moving_set[i].moved});
                moving_set[i].n.moved = true;
                moving_set[i].n.dirty = true;
                delete moving_set[i].ox;
                delete moving_set[i].oy;
            }
            RED.view.redraw();
            RED.history.push({t:"move",nodes:ns,dirty:RED.nodes.dirty()});
            RED.nodes.dirty(true);
            moving_set = null;
        }
    }

    function moveSelection(dx,dy) {
        if (RED.workspaces.isLocked()) {
            return
        }
        if (moving_set === null) {
            moving_set = [];
            var selection = RED.view.selection();
            if (selection.nodes) {
                while (selection.nodes.length > 0) {
                    var n = selection.nodes.shift();
                    moving_set.push({n:n});
                    if (n.type === "group") {
                        selection.nodes = selection.nodes.concat(n.nodes);
                    }
                }
            }
        }
        if (moving_set && moving_set.length > 0) {
            if (!endMoveSet) {
                $(document).one('keyup',endKeyboardMove);
                endMoveSet = true;
            }
            var dim = RED.view.dimensions();
            var space_width = dim.width;
            var space_height = dim.height;
            var minX = 0;
            var minY = 0;
            var node;

            for (var i=0;i<moving_set.length;i++) {
                node = moving_set[i];
                if (node.ox == null && node.oy == null) {
                    node.ox = node.n.x;
                    node.oy = node.n.y;
                    node.moved = node.n.moved;
                }
                node.n.moved = true;
                node.n.dirty = true;
                node.n.x += dx;
                node.n.y += dy;
                if ((node.n.x +node.n.w/2) >= space_width) {
                    node.n.x = space_width -node.n.w/2;
                }
                if ((node.n.y +node.n.h/2) >= space_height) {
                    node.n.y = space_height -node.n.h/2;
                }
                node.n.dirty = true;
                if (node.n.type === "group") {
                    RED.group.markDirty(node.n);
                    minX = Math.min(node.n.x - 5,minX);
                    minY = Math.min(node.n.y - 5,minY);
                } else {
                    minX = Math.min(node.n.x-node.n.w/2-5,minX);
                    minY = Math.min(node.n.y-node.n.h/2-5,minY);
                }
            }
            if (minX !== 0 || minY !== 0) {
                for (var n = 0; n<moving_set.length; n++) {
                    node = moving_set[n];
                    node.n.x -= minX;
                    node.n.y -= minY;
                }
            }
            RED.view.redraw();
        } else {
            RED.view.scroll(dx*10,dy*10);
        }
    }

    function setSelectedNodeLabelState(labelShown) {
        if (RED.workspaces.isLocked()) {
            return
        }
        var selection = RED.view.selection();
        var historyEvents = [];
        var nodes = [];
        if (selection.nodes) {
            selection.nodes.forEach(function(n) {
                if (n.type !== 'subflow' && n.type !== 'group') {
                    nodes.push(n);
                } else if (n.type === 'group') {
                    nodes = nodes.concat( RED.group.getNodes(n,true));
                }
            });
        }
        nodes.forEach(function(n) {
            var modified = false;
            var oldValue = n.l === undefined?true:n.l;
            var showLabel = n._def.hasOwnProperty("showLabel")?n._def.showLabel:true;

            if (labelShown) {
                if (n.l === false || (!showLabel && !n.hasOwnProperty('l'))) {
                    n.l = true;
                    modified = true;
                }
            } else {
                if ((showLabel && (!n.hasOwnProperty('l') || n.l === true)) || (!showLabel && n.l === true) ) {
                    n.l = false;
                    modified = true;
                }
            }
            if (modified) {
                historyEvents.push({
                    t: "edit",
                    node: n,
                    changed: n.changed,
                    changes: {
                        l: oldValue
                    }
                })
                n.changed = true;
                n.dirty = true;
                n.resize = true;
            }
        })

        if (historyEvents.length > 0) {
            RED.history.push({
                t: "multi",
                events: historyEvents,
                dirty: RED.nodes.dirty()
            })
            RED.nodes.dirty(true);
        }

        RED.view.redraw();


    }

    function selectFirstNode() {
        var canidates;
        var origin = {x:0, y:0};

        var activeGroup = RED.view.getActiveGroup();

        if (!activeGroup) {
            candidates = RED.view.getActiveNodes();
        } else {
            candidates = RED.group.getNodes(activeGroup,false);
            origin = activeGroup;
        }

        var distances = [];
        candidates.forEach(function(node) {
            var deltaX = node.x - origin.x;
            var deltaY = node.y - origin.x;
            var delta = deltaY*deltaY + deltaX*deltaX;
            distances.push({node: node, delta: delta})
        });
        if (distances.length > 0) {
            distances.sort(function(A,B) {
                return A.delta - B.delta
            })
            var newNode = distances[0].node;
            if (newNode) {
                RED.view.select({nodes:[newNode]});
                RED.view.reveal(newNode.id,false);
            }
        }
    }

    function gotoNextNode() {
        var selection = RED.view.selection();
        if (selection.nodes && selection.nodes.length === 1) {
            var origin = selection.nodes[0];
            var links = RED.nodes.filterLinks({source:origin});
            if (links.length > 0) {
                links.sort(function(A,B) {
                    return Math.abs(A.target.y - origin.y) - Math.abs(B.target.y - origin.y)
                })
                var newNode = links[0].target;
                if (newNode) {
                    RED.view.select({nodes:[newNode]});
                    RED.view.reveal(newNode.id,false);
                }
            }
        } else if (RED.workspaces.selection().length === 0) {
            selectFirstNode();
        }
    }
    function gotoPreviousNode() {
        var selection = RED.view.selection();
        if (selection.nodes && selection.nodes.length === 1) {
            var origin = selection.nodes[0];
            var links = RED.nodes.filterLinks({target:origin});
            if (links.length > 0) {
                links.sort(function(A,B) {
                    return Math.abs(A.source.y - origin.y) - Math.abs(B.source.y - origin.y)
                })
                var newNode = links[0].source;
                if (newNode) {
                    RED.view.select({nodes:[newNode]});
                    RED.view.reveal(newNode.id,false);
                }
            }
        } else if (RED.workspaces.selection().length === 0) {
            selectFirstNode();
        }
    }

    function getChildren(node) {
        return RED.nodes.filterLinks({source:node}).map(function(l) { return l.target})
    }
    function getParents(node) {
        return RED.nodes.filterLinks({target:node}).map(function(l) { return l.source})
    }

    function getSiblings(node) {
        var siblings = new Set();
        var parents = getParents(node);
        parents.forEach(function(p) {
            getChildren(p).forEach(function(c) { siblings.add(c) })
        });
        var children = getChildren(node);
        children.forEach(function(p) {
            getParents(p).forEach(function(c) { siblings.add(c) })
        });
        siblings.delete(node);
        return Array.from(siblings);
    }
    function gotoNextSibling() {
        // 'next' defined as nearest on the y-axis below this node
        var selection = RED.view.selection();
        if (selection.nodes && selection.nodes.length === 1) {
            var origin = selection.nodes[0];
            var siblings = getSiblings(origin);
            if (siblings.length > 0) {
                siblings = siblings.filter(function(n) { return n.y > origin. y})
                siblings.sort(function(A,B) {
                    return Math.abs(A.y - origin.y) - Math.abs(B.y - origin.y)
                })
                var newNode = siblings[0];
                if (newNode) {
                    RED.view.select({nodes:[newNode]});
                    RED.view.reveal(newNode.id,false);
                }
            }
        } else if (RED.workspaces.selection().length === 0) {
            selectFirstNode();
        }
    }
    function gotoPreviousSibling() {
        // 'next' defined as nearest on the y-axis above this node
        var selection = RED.view.selection();
        if (selection.nodes && selection.nodes.length === 1) {
            var origin = selection.nodes[0];
            var siblings = getSiblings(origin);
            if (siblings.length > 0) {
                siblings = siblings.filter(function(n) { return n.y < origin. y})
                siblings.sort(function(A,B) {
                    return Math.abs(A.y - origin.y) - Math.abs(B.y - origin.y)
                })
                var newNode = siblings[0];
                if (newNode) {
                    RED.view.select({nodes:[newNode]});
                    RED.view.reveal(newNode.id,false);
                }
            }
        } else if (RED.workspaces.selection().length === 0) {
            selectFirstNode();
        }

    }

    // function addNode() {
    //     var selection = RED.view.selection();
    //     if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].outputs > 0) {
    //         var selectedNode = selection.nodes[0];
    //         RED.view.showQuickAddDialog([
    //             selectedNode.x + selectedNode.w + 50,selectedNode.y
    //         ])
    //     } else {
    //         RED.view.showQuickAddDialog();
    //     }
    // }


    function gotoNearestNode(direction) {
        var selection = RED.view.selection();
        if (selection.nodes && selection.nodes.length === 1) {
            var origin = selection.nodes[0];

            var candidates = RED.nodes.filterNodes({z:origin.z});
            candidates = candidates.concat(RED.view.getSubflowPorts());
            var distances = [];
            candidates.forEach(function(node) {
                if (node === origin) {
                    return;
                }
                var deltaX = node.x - origin.x;
                var deltaY = node.y - origin.y;
                var delta = deltaY*deltaY + deltaX*deltaX;
                var angle = (180/Math.PI)*Math.atan2(deltaY,deltaX);
                if (angle < 0) { angle += 360 }
                if (angle > 360) { angle -= 360 }

                var weight;

                // 0 - right
                // 270 - above
                // 90 - below
                // 180 - left
                switch(direction) {
                    case 'up': if (angle < 210 || angle > 330) { return }
                        weight = Math.max(Math.abs(270 - angle)/60, 0.2);
                        break;
                    case 'down': if (angle < 30 || angle > 150) { return }
                        weight = Math.max(Math.abs(90 - angle)/60, 0.2);
                        break;
                    case 'left': if (angle < 140 || angle > 220) { return }
                        weight = Math.max(Math.abs(180 - angle)/40, 0.1 );
                        break;
                    case 'right': if (angle > 40 && angle < 320) { return }
                        weight = Math.max(Math.abs(angle)/40, 0.1);
                        break;
                }
                weight = Math.max(weight,0.1);
                distances.push({
                    node: node,
                    d: delta,
                    w: weight,
                    delta: delta*weight
                })
            })
            if (distances.length > 0) {
                distances.sort(function(A,B) {
                    return A.delta - B.delta
                })
                var newNode = distances[0].node;
                if (newNode) {
                    RED.view.select({nodes:[newNode]});
                    RED.view.reveal(newNode.id,false);
                }
            }
        } else if (RED.workspaces.selection().length === 0) {
            var candidates = RED.view.getActiveNodes();

            var distances = [];
            candidates.forEach(function(node) {
                var deltaX = node.x;
                var deltaY = node.y;
                var delta = deltaY*deltaY + deltaX*deltaX;
                distances.push({node: node, delta: delta})
            });
            if (distances.length > 0) {
                distances.sort(function(A,B) {
                    return A.delta - B.delta
                })
                var newNode = distances[0].node;
                if (newNode) {
                    RED.view.select({nodes:[newNode]});
                    RED.view.reveal(newNode.id,false);
                }
            }
        }
    }

    function alignSelectionToEdge(direction) {
        if (RED.workspaces.isLocked()) {
            return;
        }
        var selection = RED.view.selection();

        if (selection.nodes && selection.nodes.length > 1) {
            var changedNodes = [];
            var bounds = {
                minX: Number.MAX_SAFE_INTEGER,
                minY: Number.MAX_SAFE_INTEGER,
                maxX: Number.MIN_SAFE_INTEGER,
                maxY: Number.MIN_SAFE_INTEGER
            }
            selection.nodes.forEach(function(n) {
                if (n.type === "group") {
                    bounds.minX = Math.min(bounds.minX, n.x);
                    bounds.minY = Math.min(bounds.minY, n.y);
                    bounds.maxX = Math.max(bounds.maxX, n.x + n.w);
                    bounds.maxY = Math.max(bounds.maxY, n.y + n.h);
                } else {
                    bounds.minX = Math.min(bounds.minX, n.x - n.w/2);
                    bounds.minY = Math.min(bounds.minY, n.y - n.h/2);
                    bounds.maxX = Math.max(bounds.maxX, n.x + n.w/2);
                    bounds.maxY = Math.max(bounds.maxY, n.y + n.h/2);
                }
            });

            bounds.midX = bounds.minX + (bounds.maxX - bounds.minX)/2;
            bounds.midY = bounds.minY + (bounds.maxY - bounds.minY)/2;

            selection.nodes.forEach(function(n) {
                var targetX;
                var targetY;
                var isGroup = n.type==="group";
                switch(direction) {
                    case 'top':
                        targetX = n.x;
                        targetY = bounds.minY + (isGroup?0:(n.h/2));
                        break;
                    case 'bottom':
                        targetX = n.x;
                        targetY = bounds.maxY - (isGroup?n.h:(n.h/2));
                        break;
                    case 'left':
                        targetX = bounds.minX + (isGroup?0:(n.w/2));
                        targetY = n.y;
                        break;
                    case 'right':
                        targetX = bounds.maxX - (isGroup?n.w:(n.w/2));
                        targetY = n.y;
                        break;
                    case 'middle':
                        targetX = n.x;
                        targetY = bounds.midY - (isGroup?n.h/2:0)
                        break;
                    case 'center':
                        targetX = bounds.midX - (isGroup?n.w/2:0)
                        targetY = n.y;
                        break;
                }

                if (n.x !== targetX || n.y !== targetY) {
                    if (!isGroup) {
                        changedNodes.push({
                            n:n,
                            ox: n.x,
                            oy: n.y,
                            moved: n.moved
                        });
                        n.x = targetX;
                        n.y = targetY;
                        n.dirty = true;
                        n.moved = true;
                    } else {
                        var groupNodes = RED.group.getNodes(n, true);
                        var deltaX = n.x - targetX;
                        var deltaY = n.y - targetY;
                        groupNodes.forEach(function(gn) {
                            if (gn.type !== "group" ) {
                                changedNodes.push({
                                    n:gn,
                                    ox: gn.x,
                                    oy: gn.y,
                                    moved: gn.moved
                                });
                                gn.x = gn.x - deltaX;
                                gn.y = gn.y - deltaY;
                                gn.dirty = true;
                                gn.moved = true;
                            }
                        })

                    }
                }
            });
            if (changedNodes.length > 0) {
                RED.history.push({t:"move",nodes:changedNodes,dirty:RED.nodes.dirty()});
                RED.nodes.dirty(true);
                RED.view.redraw(true);
            }
        }
    }

    function distributeSelection(direction) {
        if (RED.workspaces.isLocked()) {
            return
        }
        var selection = RED.view.selection();

        if (selection.nodes && selection.nodes.length > 2) {
            var changedNodes = [];
            var bounds = {
                minX: Number.MAX_SAFE_INTEGER,
                minY: Number.MAX_SAFE_INTEGER,
                maxX: Number.MIN_SAFE_INTEGER,
                maxY: Number.MIN_SAFE_INTEGER
            }
            var startAnchors = [];
            var endAnchors = [];

            selection.nodes.forEach(function(n) {
                var nx,ny;
                if (n.type === "group") {
                    nx = n.x + n.w/2;
                    ny = n.y + n.h/2;
                } else {
                    nx = n.x;
                    ny = n.y;
                }
                if (direction === "h") {
                    if (nx < bounds.minX) {
                        startAnchors = [];
                        bounds.minX = nx;
                    }
                    if (nx === bounds.minX) {
                        startAnchors.push(n);
                    }
                    if (nx > bounds.maxX) {
                        endAnchors = [];
                        bounds.maxX = nx;
                    }
                    if (nx === bounds.maxX) {
                        endAnchors.push(n);
                    }
                } else {
                    if (ny < bounds.minY) {
                        startAnchors = [];
                        bounds.minY = ny;
                    }
                    if (ny === bounds.minY) {
                        startAnchors.push(n);
                    }
                    if (ny > bounds.maxY) {
                        endAnchors = [];
                        bounds.maxY = ny;
                    }
                    if (ny === bounds.maxY) {
                        endAnchors.push(n);
                    }
                }
            });

            var startAnchor = startAnchors[0];
            var endAnchor = endAnchors[0];

            var nodeSpace = 0;
            var nodesToMove = selection.nodes.filter(function(n) {
                if (n.id !== startAnchor.id && n.id !== endAnchor.id) {
                    nodeSpace += direction === 'h'?n.w:n.h;
                    return true;
                }
                return false;
            }).sort(function(A,B) {
                if (direction === 'h') {
                    return A.x - B.x
                } else {
                    return A.y - B.y
                }
            })

            var saX = startAnchor.x + startAnchor.w/2;
            var saY = startAnchor.y + startAnchor.h/2;
            if (startAnchor.type === "group") {
                saX = startAnchor.x + startAnchor.w;
                saY = startAnchor.y + startAnchor.h;
            }
            var eaX = endAnchor.x;
            var eaY = endAnchor.y;
            if (endAnchor.type !== "group") {
                eaX -= endAnchor.w/2;
                eaY -= endAnchor.h/2;
            }
            var spaceToFill = direction === 'h'?(eaX - saX - nodeSpace): (eaY - saY - nodeSpace);
            var spaceBetweenNodes = spaceToFill / (nodesToMove.length + 1);

            var tx = saX;
            var ty = saY;
            while(nodesToMove.length > 0) {
                if (direction === 'h') {
                    tx += spaceBetweenNodes;
                } else {
                    ty += spaceBetweenNodes;
                }
                var nextNode = nodesToMove.shift();
                var isGroup = nextNode.type==="group";

                var nx = nextNode.x;
                var ny = nextNode.y;
                if (!isGroup) {
                    tx += nextNode.w/2;
                    ty += nextNode.h/2;
                }
                if ((direction === 'h' && nx !== tx) || (direction === 'v' && ny !== ty)) {
                    if (!isGroup) {
                        changedNodes.push({
                            n:nextNode,
                            ox: nextNode.x,
                            oy: nextNode.y,
                            moved: nextNode.moved
                        });
                        if (direction === 'h') {
                            nextNode.x = tx;
                        } else {
                            nextNode.y = ty;
                        }
                        nextNode.dirty = true;
                        nextNode.moved = true;
                    } else {
                        var groupNodes = RED.group.getNodes(nextNode, true);
                        var deltaX = direction === 'h'? nx - tx : 0;
                        var deltaY = direction === 'v'? ny - ty : 0;
                        groupNodes.forEach(function(gn) {
                            if (gn.type !== "group" ) {
                                changedNodes.push({
                                    n:gn,
                                    ox: gn.x,
                                    oy: gn.y,
                                    moved: gn.moved
                                });
                                gn.x = gn.x - deltaX;
                                gn.y = gn.y - deltaY;
                                gn.dirty = true;
                                gn.moved = true;
                            }
                        })
                    }
                }
                if (isGroup) {
                    tx += nextNode.w;
                    ty += nextNode.h;
                } else {
                    tx += nextNode.w/2;
                    ty += nextNode.h/2;
                }
            }

            if (changedNodes.length > 0) {
                RED.history.push({t:"move",nodes:changedNodes,dirty:RED.nodes.dirty()});
                RED.nodes.dirty(true);
                RED.view.redraw(true);
            }
        }
    }

    function reorderSelection(dir) {
        if (RED.workspaces.isLocked()) {
            return
        }
        var selection = RED.view.selection();
        if (selection.nodes) {
            var nodesToMove = [];
            selection.nodes.forEach(function(n) {
                if (n.type === "group") {
                    nodesToMove.push(n)
                    nodesToMove = nodesToMove.concat(RED.group.getNodes(n, true))
                } else if (n.type !== "subflow"){
                    nodesToMove.push(n);
                }
            })
            if (nodesToMove.length > 0) {
                var z = nodesToMove[0].z;
                var existingOrder = RED.nodes.getNodeOrder(z);
                var movedNodes;
                if (dir === "forwards") {
                    movedNodes = RED.nodes.moveNodesForwards(nodesToMove);
                } else if (dir === "backwards") {
                    movedNodes = RED.nodes.moveNodesBackwards(nodesToMove);
                } else if (dir === "front") {
                    movedNodes = RED.nodes.moveNodesToFront(nodesToMove);
                } else if (dir === "back") {
                    movedNodes = RED.nodes.moveNodesToBack(nodesToMove);
                }
                if (movedNodes.length > 0) {
                    var newOrder = RED.nodes.getNodeOrder(z);
                    RED.history.push({t:"reorder",nodes:{z:z,from:existingOrder,to:newOrder},dirty:RED.nodes.dirty()});
                    RED.nodes.dirty(true);
                    RED.view.redraw(true);
                }
            }
        }
    }

    function wireSeriesOfNodes() {
        if (RED.workspaces.isLocked()) {
            return
        }
        var selection = RED.view.selection();
        if (selection.nodes) {
            if (selection.nodes.length > 1) {
                var i = 0;
                var newLinks = [];
                while (i < selection.nodes.length - 1) {
                    var nodeA = selection.nodes[i];
                    var nodeB = selection.nodes[i+1];
                    if (nodeA.outputs > 0 && nodeB.inputs > 0) {
                        var existingLinks = RED.nodes.filterLinks({
                            source: nodeA,
                            target: nodeB,
                            sourcePort: 0
                        })
                        if (existingLinks.length === 0) {
                            var newLink = {
                                source: nodeA,
                                target: nodeB,
                                sourcePort: 0
                            }
                            RED.nodes.addLink(newLink);
                            newLinks.push(newLink);
                        }
                    }
                    i++;
                }
                if (newLinks.length > 0) {
                    RED.history.push({
                        t: 'add',
                        links: newLinks,
                        dirty: RED.nodes.dirty()
                    })
                    RED.nodes.dirty(true);
                    RED.view.redraw(true);
                }
            }
        }
    }

    function wireNodeToMultiple() {
        if (RED.workspaces.isLocked()) {
            return
        }
        var selection = RED.view.selection();
        if (selection.nodes) {
            if (selection.nodes.length > 1) {
                var sourceNode = selection.nodes[0];
                if (sourceNode.outputs === 0) {
                    return;
                }
                var i = 1;
                var newLinks = [];
                while (i < selection.nodes.length) {
                    var targetNode = selection.nodes[i];
                    if (targetNode.inputs > 0) {
                        var existingLinks = RED.nodes.filterLinks({
                            source: sourceNode,
                            target: targetNode,
                            sourcePort: Math.min(sourceNode.outputs-1,i-1)
                        })
                        if (existingLinks.length === 0) {
                            var newLink = {
                                source: sourceNode,
                                target: targetNode,
                                sourcePort: Math.min(sourceNode.outputs-1,i-1)
                            }
                            RED.nodes.addLink(newLink);
                            newLinks.push(newLink);
                        }
                    }
                    i++;
                }
                if (newLinks.length > 0) {
                    RED.history.push({
                        t: 'add',
                        links: newLinks,
                        dirty: RED.nodes.dirty()
                    })
                    RED.nodes.dirty(true);
                    RED.view.redraw(true);
                }
            }
        }
    }

    function wireMultipleToNode() {
        if (RED.workspaces.isLocked()) {
            return
        }
        var selection = RED.view.selection();
        if (selection.nodes) {
            if (selection.nodes.length > 1) {
                var targetNode = selection.nodes[selection.nodes.length - 1];
                if (targetNode.inputs === 0) {
                    return;
                }
                var i = 0;
                var newLinks = [];
                for (i = 0; i < selection.nodes.length - 1; i++) {
                    var sourceNode = selection.nodes[i];
                    if (sourceNode.outputs > 0) {
                        
                        // Wire the first output to the target that has no link to the target yet.
                        // This allows for connecting all combinations of inputs/outputs. 
                        // The user may then delete links quickly that aren't needed.
                        var sourceConnectedOutports = RED.nodes.filterLinks({
                            source: sourceNode,
                            target: targetNode
                        });

                        // Get outport indices that have no link yet
                        var sourceOutportIndices = Array.from({ length: sourceNode.outputs }, (_, i) => i);
                        var sourceConnectedOutportIndices = sourceConnectedOutports.map( x => x.sourcePort );
                        var sourceFreeOutportIndices = sourceOutportIndices.filter(x => !sourceConnectedOutportIndices.includes(x));

                        // Does an unconnected source port exist?
                        if (sourceFreeOutportIndices.length == 0) {
                            continue;
                        }
                        
                        // Connect the first free outport to the target
                        var newLink = {
                            source: sourceNode,
                            target: targetNode,
                            sourcePort: sourceFreeOutportIndices[0]
                        }
                        RED.nodes.addLink(newLink);
                        newLinks.push(newLink);
                    }
                }
                if (newLinks.length > 0) {
                    RED.history.push({
                        t: 'add',
                        links: newLinks,
                        dirty: RED.nodes.dirty()
                    })
                    RED.nodes.dirty(true);
                    RED.view.redraw(true);
                }
            }
        }
    }

    /**
     * Splits selected wires and re-joins them with link-out+link-in
     * @param {Object || Object[]} wires The wire(s) to split and replace with link-out, link-in nodes.
     */
    function splitWiresWithLinkNodes(wires) {
        if (RED.workspaces.isLocked()) {
            return
        }
        let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
        if (!wiresToSplit) {
            return
        }
        if (!Array.isArray(wiresToSplit)) {
            wiresToSplit = [wiresToSplit];
        }
        if (wiresToSplit.length < 1) {
            return; //nothing selected
        }

        const history = {
            t: 'multi',
            events: [],
            dirty: RED.nodes.dirty()
        }
        const nodeSrcMap = {};
        const nodeTrgMap = {};
        const _gridSize = RED.view.gridSize();

        for (let wireIdx = 0; wireIdx < wiresToSplit.length; wireIdx++) {
            const wire = wiresToSplit[wireIdx];

            //get source and target nodes of this wire link
            const nSrc = wire.source;
            const nTrg = wire.target;

            var updateNewNodePosXY = function (origNode, newNode, alignLeft, snap, yOffset) {
                const nnSize = RED.view.calculateNodeDimensions(newNode);
                newNode.w = nnSize[0];
                newNode.h = nnSize[1];
                const coords = { x: origNode.x || 0, y: origNode.y || 0, w: origNode.w || RED.view.node_width, h: origNode.h || RED.view.node_height };
                const x = coords.x - (coords.w/2.0);
                if (alignLeft) {
                    coords.x = x - _gridSize - (newNode.w/2.0);
                } else {
                    coords.x = x + coords.w + _gridSize + (newNode.w/2.0);
                }
                newNode.x = coords.x;
                newNode.y = coords.y;
                if (snap !== false) {
                    const offsets = RED.view.tools.calculateGridSnapOffsets(newNode);
                    newNode.x -= offsets.x;
                    newNode.y -= offsets.y;
                }
                newNode.y += (yOffset || 0);
            }
            const srcPort = (wire.sourcePort || 0);
            let linkOutMapId = nSrc.id + ':' + srcPort;
            let nnLinkOut = nodeSrcMap[linkOutMapId];
            //Create a Link Out if one is not already present
            if(!nnLinkOut) {
                const nLinkOut = RED.view.createNode("link out"); //create link node
                nnLinkOut = nLinkOut.node;
                let yOffset = 0;
                if(nSrc.outputs > 1) {

                    const CENTER_PORT = (((nSrc.outputs-1) / 2) + 1);
                    const offsetCount = Math.abs(CENTER_PORT - (srcPort + 1));
                    yOffset = (_gridSize * 2 * offsetCount);
                    if((srcPort + 1) < CENTER_PORT) {
                        yOffset = -yOffset;
                    }
                    updateNewNodePosXY(nSrc, nnLinkOut, false, false, yOffset);
                } else {
                    updateNewNodePosXY(nSrc, nnLinkOut, false, RED.view.snapGrid, yOffset);
                }
                //add created node
                nnLinkOut = RED.nodes.add(nnLinkOut);
                nodeSrcMap[linkOutMapId] = nnLinkOut;
                RED.editor.validateNode(nnLinkOut);
                history.events.push(nLinkOut.historyEvent);
                //connect node to link node
                const link = {
                    source: nSrc,
                    sourcePort: wire.sourcePort || 0,
                    target: nnLinkOut
                };
                RED.nodes.addLink(link);
                history.events.push({
                    t: 'add',
                    links: [link],
                });
            }

            let nnLinkIn = nodeTrgMap[nTrg.id];
            //Create a Link In if one is not already present
            if(!nnLinkIn) {
                const nLinkIn = RED.view.createNode("link in"); //create link node
                nnLinkIn = nLinkIn.node;
                updateNewNodePosXY(nTrg, nnLinkIn, true, RED.view.snapGrid, 0);
                //add created node
                nnLinkIn = RED.nodes.add(nnLinkIn);
                nodeTrgMap[nTrg.id] = nnLinkIn;
                RED.editor.validateNode(nnLinkIn);
                history.events.push(nLinkIn.historyEvent);
                //connect node to link node
                const link = {
                    source: nnLinkIn,
                    sourcePort: 0,
                    target: nTrg
                };
                RED.nodes.addLink(link);
                history.events.push({
                    t: 'add',
                    links: [link],
                });
            }

            //connect the link out/link in virtual wires
            if(nnLinkIn.links.indexOf(nnLinkOut.id) == -1) {
                nnLinkIn.links.push(nnLinkOut.id);
            }
            if(nnLinkOut.links.indexOf(nnLinkIn.id) == -1) {
                nnLinkOut.links.push(nnLinkIn.id);
            }

            //delete the original wire
            RED.nodes.removeLink(wire);
            history.events.push({
                t: "delete",
                links: [wire]
            });
        }
        //add all history events to stack
        RED.history.push(history);

        //select all downstream of new link-in nodes so user can drag to new location
        RED.view.clearSelection();
        RED.view.select({nodes: Object.values(nodeTrgMap) });
        selectConnected("down");

        //update the view
        RED.nodes.dirty(true);
        RED.view.redraw(true);
    }

    /**
     * Calculate the required offsets to snap a node
     * @param {Object} node The node to calculate grid snap offsets for
     * @param {Object} [options] Options: `align` can be "nearest", "left" or "right"
     * @returns `{x:number, y:number}`  as the offsets to deduct from `x` and `y`
     */
    function calculateGridSnapOffsets(node, options) {
        options = options || { align: "nearest" };
        const gridOffset = { x: 0, y: 0 };
        const gridSize = RED.view.gridSize();
        const offsetLeft = node.x - (gridSize * Math.round((node.x - node.w / 2) / gridSize) + node.w / 2);
        const offsetRight = node.x - (gridSize * Math.round((node.x + node.w / 2) / gridSize) - node.w / 2);
        gridOffset.x = offsetRight;
        if (options.align === "right") {
            //skip - already set to right
        } else if (options.align === "left" || Math.abs(offsetLeft) < Math.abs(offsetRight)) {
            gridOffset.x = offsetLeft;
        }
        gridOffset.y = node.y - (gridSize * Math.round(node.y / gridSize));
        return gridOffset;
    }

    /**
     * Generate names for the select nodes.
     *  - it only sets the name if it is currently blank
     *  - it uses `<paletteLabel> <N>` - where N is the next available integer that
     *    doesn't clash with any existing nodes of that type
     * @param {Object} node The node to set the name of - if not provided, uses current selection
     * @param {{ renameBlank: boolean, renameClash: boolean, generateHistory: boolean }} options Possible options are `renameBlank`, `renameClash` and `generateHistory`
     */
    function generateNodeNames(node, options) {
        if (RED.workspaces.isLocked()) {
            return
        }
        options = Object.assign({
            renameBlank: true,
            renameClash: true,
            generateHistory: true
        }, options)
        let nodes = node;
        if (node) {
            if (!Array.isArray(node)) {
                nodes = [ node ]
            }
        } else {
            nodes = RED.view.selection().nodes;
        }
        if (nodes && nodes.length > 0) {
            // Generate history event if using the workspace selection,
            // or if the provided node already exists
            const generateHistory = options.generateHistory && (!node || !!RED.nodes.node(node.id))
            const historyEvents = []
            const typeIndex = {}
            let changed = false;
            nodes.forEach(n => {
                const nodeDef = n._def || RED.nodes.getType(n.type)
                if (nodeDef && nodeDef.defaults && nodeDef.defaults.name) {
                    const paletteLabel = RED.utils.getPaletteLabel(n.type, nodeDef)
                    const defaultNodeNameRE = new RegExp('^'+paletteLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+' (\\d+)$')
                    if (!typeIndex.hasOwnProperty(n.type)) {
                        const existingNodes = RED.nodes.filterNodes({ type: n.type });
                        const existingIds = existingNodes.reduce((ids, node) => {
                            let match = defaultNodeNameRE.exec(node.name);
                            if (match) {
                                const nodeNumber = parseInt(match[1], 10);
                                if (!ids.includes(nodeNumber)) {
                                    ids.push(nodeNumber);
                                }
                            }
                            return ids;
                        }, []).sort((a, b) => a - b);

                        let availableNameNumber = 1;
                        for (let i = 0; i < existingIds.length; i++) {
                            if (existingIds[i] !== availableNameNumber) {
                                break;
                            }
                            availableNameNumber++;
                        }

                        typeIndex[n.type] = availableNameNumber;
                    }
                    if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) {
                        if (generateHistory) {
                            historyEvents.push({
                                t:'edit',
                                node: n,
                                changes: { name: n.name },
                                dirty: RED.nodes.dirty(),
                                changed: n.changed
                            })
                        }
                        n.name = paletteLabel+" "+typeIndex[n.type]
                        n.dirty = true
                        typeIndex[n.type]++
                        changed = true
                    }
                }
            })
            if (changed) {
                if (historyEvents.length > 0) {
                    RED.history.push({
                        t: 'multi',
                        events: historyEvents
                    })
                }
                RED.nodes.dirty(true)
                RED.view.redraw()
            }
        }
    }

    function addJunctionsToWires(options = {}) {
        if (RED.workspaces.isLocked()) {
            return
        }
        let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
        if (!wiresToSplit) {
            return
        }
        if (!Array.isArray(wiresToSplit)) {
            wiresToSplit = [wiresToSplit];
        }
        if (wiresToSplit.length === 0) {
            return;
        }

        var removedLinks = new Set()
        var addedLinks = []
        var addedJunctions = []

        var groupedLinks = {}
        wiresToSplit.forEach(function(l) {
            var sourceId = l.source.id+":"+l.sourcePort
            groupedLinks[sourceId] = groupedLinks[sourceId] || []
            groupedLinks[sourceId].push(l)

            groupedLinks[l.target.id] = groupedLinks[l.target.id] || []
            groupedLinks[l.target.id].push(l)
        });
        var linkGroups = Object.keys(groupedLinks)
        linkGroups.sort(function(A,B) {
            return groupedLinks[B].length - groupedLinks[A].length
        })
        const wasDirty = RED.nodes.dirty()
        linkGroups.forEach(function(gid) {
            var links = groupedLinks[gid]
            var junction = {
                _def: {defaults:{}},
                type: 'junction',
                z: RED.workspaces.active(),
                id: RED.nodes.id(),
                x: 0,
                y: 0,
                w: 0, h: 0,
                outputs: 1,
                inputs: 1,
                dirty: true,
                moved: true
            }
            links = links.filter(function(l) { return !removedLinks.has(l) })
            if (links.length === 0) {
                return
            }
            if (addedJunctions.length === 0 && Object.hasOwn(options, 'x') && Object.hasOwn(options, 'y')) {
                junction.x = options.x
                junction.y = options.y
            } else {
                let pointCount = 0
                links.forEach(function(l) {
                    if (l._sliceLocation) {
                        junction.x += l._sliceLocation.x
                        junction.y += l._sliceLocation.y
                        delete l._sliceLocation
                        pointCount++
                    } else {
                        junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2
                        junction.y += l.source.y + l.target.y
                        pointCount += 2
                    }
                })
                junction.x = Math.round(junction.x/pointCount)
                junction.y = Math.round(junction.y/pointCount)
            }
            if (RED.view.snapGrid) {
                let gridSize = RED.view.gridSize()
                junction.x = (gridSize*Math.round(junction.x/gridSize));
                junction.y = (gridSize*Math.round(junction.y/gridSize));
            }

            var nodeGroups = new Set()

            junction = RED.nodes.addJunction(junction)
            addedJunctions.push(junction)
            let newLink
            if (gid === links[0].source.id+":"+links[0].sourcePort) {
                newLink = {
                    source: links[0].source,
                    sourcePort: links[0].sourcePort,
                    target: junction
                }
            } else {
                newLink = {
                    source: junction,
                    sourcePort: 0,
                    target: links[0].target
                }
            }
            addedLinks.push(newLink)
            RED.nodes.addLink(newLink)
            links.forEach(function(l) {
                removedLinks.add(l)
                RED.nodes.removeLink(l)
                let newLink
                if (gid === l.target.id) {
                    newLink = {
                        source: l.source,
                        sourcePort: l.sourcePort,
                        target: junction
                    }
                } else {
                    newLink = {
                        source: junction,
                        sourcePort: 0,
                        target: l.target
                    }
                }
                addedLinks.push(newLink)
                RED.nodes.addLink(newLink)
                nodeGroups.add(l.source.g || "__NONE__")
                nodeGroups.add(l.target.g || "__NONE__")
            })
            if (nodeGroups.size === 1) {
                var group = nodeGroups.values().next().value
                if (group !== "__NONE__") {
                    RED.group.addToGroup(RED.nodes.group(group), junction)
                }
            }
        })
        if (addedJunctions.length > 0) {
            RED.history.push({
                dirty: wasDirty,
                t: 'add',
                links: addedLinks,
                junctions: addedJunctions,
                removedLinks: Array.from(removedLinks)
            })
            RED.nodes.dirty(true)
            RED.view.select({nodes: addedJunctions });
        }
        RED.view.redraw(true);
    }

    function copyItemUrl(node, isEdit) {
        if (!node) {
            const selection = RED.view.selection();
            if (selection.nodes && selection.nodes.length > 0) {
                node = selection.nodes[0]
            }
        }
        if (node) {
            let thingType = 'node'
            if (node.type === 'group') {
                thingType = 'group'
            } else if (node.type === 'tab' || node.type === 'subflow') {
                thingType = 'flow'
            }
            let url = `${window.location.origin}${window.location.pathname}#${thingType}/${node.id}`
            if (isEdit) {
                url += '/edit'
            }
            if (RED.clipboard.copyText(url)) {
                RED.notify(RED._("sidebar.info.copyURL2Clipboard"), { timeout: 2000 })
            }
        }
    }

    /**
     * Determine if a point is within a node
     * @param {*} node - A Node or Junction node
     * @param {[Number,Number]} mouse_position The x,y position of the mouse
     * @param {Number} [marginX=0] - A margin to add or deduct from the x position (to increase the hit area)
     * @param {Number} [marginY=0] - A margin to add or deduct from the y position (to increase the hit area)
     * @returns 
     */
    function isPointInNode (node, [x, y], marginX, marginY) {
        marginX = marginX || 0
        marginY = marginY || 0

        let w = node.w || 10 // junctions dont have any w or h value
        let h = node.h || 10
        let x1, x2, y1, y2
        
        if (node.type === "junction" || node.type === "group") {
            // x/y is the top left of the node
            x1 = node.x
            y1 = node.y
            x2 = node.x + w
            y2 = node.y + h
        } else {
            // x/y is the center of the node
            const [xMid, yMid] =  [w/2, h/2]
            x1 = node.x - xMid
            y1 = node.y - yMid
            x2 = node.x + xMid
            y2 = node.y + yMid
        }
        return (x >= (x1 - marginX) && x <= (x2 + marginX) && y >= (y1 - marginY) && y <= (y2 + marginY))
    }

    return {
        init: function() {
            RED.actions.add("core:show-selected-node-labels", function() { setSelectedNodeLabelState(true); })
            RED.actions.add("core:hide-selected-node-labels", function() { setSelectedNodeLabelState(false); })

            RED.actions.add("core:scroll-view-up", function() { RED.view.scroll(0,-RED.view.gridSize());});
            RED.actions.add("core:scroll-view-right", function() { RED.view.scroll(RED.view.gridSize(),0);});
            RED.actions.add("core:scroll-view-down", function() { RED.view.scroll(0,RED.view.gridSize());});
            RED.actions.add("core:scroll-view-left", function() { RED.view.scroll(-RED.view.gridSize(),0);});

            RED.actions.add("core:step-view-up", function() { RED.view.scroll(0,-5*RED.view.gridSize());});
            RED.actions.add("core:step-view-right", function() { RED.view.scroll(5*RED.view.gridSize(),0);});
            RED.actions.add("core:step-view-down", function() { RED.view.scroll(0,5*RED.view.gridSize());});
            RED.actions.add("core:step-view-left", function() { RED.view.scroll(-5*RED.view.gridSize(),0);});

            RED.actions.add("core:move-selection-up", function() { moveSelection(0,-1);});
            RED.actions.add("core:move-selection-right", function() { moveSelection(1,0);});
            RED.actions.add("core:move-selection-down", function() { moveSelection(0,1);});
            RED.actions.add("core:move-selection-left", function() { moveSelection(-1,0);});

            RED.actions.add("core:move-selection-forwards", function() { reorderSelection('forwards') })
            RED.actions.add("core:move-selection-backwards", function() { reorderSelection('backwards') })
            RED.actions.add("core:move-selection-to-front", function() { reorderSelection('front') })
            RED.actions.add("core:move-selection-to-back", function() { reorderSelection('back') })


            RED.actions.add("core:step-selection-up", function() { moveSelection(0,-RED.view.gridSize());});
            RED.actions.add("core:step-selection-right", function() { moveSelection(RED.view.gridSize(),0);});
            RED.actions.add("core:step-selection-down", function() { moveSelection(0,RED.view.gridSize());});
            RED.actions.add("core:step-selection-left", function() { moveSelection(-RED.view.gridSize(),0);});

            RED.actions.add("core:select-connected-nodes", function() { selectConnected("all") });
            RED.actions.add("core:select-downstream-nodes", function() { selectConnected("down") });
            RED.actions.add("core:select-upstream-nodes", function() { selectConnected("up") });


            RED.actions.add("core:go-to-next-node", function() { gotoNextNode() })
            RED.actions.add("core:go-to-previous-node", function() { gotoPreviousNode() })
            RED.actions.add("core:go-to-next-sibling", function() { gotoNextSibling() })
            RED.actions.add("core:go-to-previous-sibling", function() { gotoPreviousSibling() })


            RED.actions.add("core:go-to-nearest-node-on-left", function() { gotoNearestNode('left')})
            RED.actions.add("core:go-to-nearest-node-on-right", function() { gotoNearestNode('right')})
            RED.actions.add("core:go-to-nearest-node-above", function() { gotoNearestNode('up') })
            RED.actions.add("core:go-to-nearest-node-below", function() { gotoNearestNode('down') })

            RED.actions.add("core:align-selection-to-grid", alignToGrid);
            RED.actions.add("core:align-selection-to-left", function() { alignSelectionToEdge('left') })
            RED.actions.add("core:align-selection-to-right", function() { alignSelectionToEdge('right') })
            RED.actions.add("core:align-selection-to-top", function() { alignSelectionToEdge('top') })
            RED.actions.add("core:align-selection-to-bottom", function() { alignSelectionToEdge('bottom') })
            RED.actions.add("core:align-selection-to-middle", function() { alignSelectionToEdge('middle') })
            RED.actions.add("core:align-selection-to-center", function() { alignSelectionToEdge('center') })

            RED.actions.add("core:distribute-selection-horizontally", function() { distributeSelection('h') })
            RED.actions.add("core:distribute-selection-vertically", function() { distributeSelection('v') })

            RED.actions.add("core:wire-series-of-nodes", function() { wireSeriesOfNodes() })
            RED.actions.add("core:wire-node-to-multiple", function() { wireNodeToMultiple() })
            RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() })

            RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() });
            RED.actions.add("core:split-wires-with-junctions", function (options) { addJunctionsToWires(options) });

            RED.actions.add("core:generate-node-names", generateNodeNames )

            RED.actions.add("core:copy-item-url", function (node) { copyItemUrl(node) })
            RED.actions.add("core:copy-item-edit-url", function (node) { copyItemUrl(node, true) })

            // RED.actions.add("core:add-node", function() { addNode() })
        },
        /**
         * Aligns all selected nodes to the current grid
         */
        alignSelectionToGrid: alignToGrid,
        /**
         * Moves all of the selected nodes by the specified amount
         * @param  {Number} dx
         * @param  {Number} dy
         */
        moveSelection: moveSelection,
        calculateGridSnapOffsets: calculateGridSnapOffsets,
        isPointInNode: isPointInNode
    }

})();
;/**
 * 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.
 **/
RED.sidebar = (function() {

    //$('#sidebar').tabs();
    var sidebar_tabs;
    var knownTabs = {};

    // We store the current sidebar tab id in localStorage as 'last-sidebar-tab'
    // This is restored when the editor is reloaded.
    // We use sidebar_tabs.onchange to update localStorage. However that will
    // also get triggered when the first tab gets added to the tabs - typically
    // the 'info' tab. So we use the following variable to store the retrieved
    // value from localStorage before we start adding the actual tabs
    var lastSessionSelectedTab = null;


    function addTab(title,content,closeable,visible) {
        var options;
        if (typeof title === "string") {
            // TODO: legacy support in case anyone uses this...
            options = {
                id: content.id,
                label: title,
                name: title,
                content: content,
                closeable: closeable,
                visible: visible
            }
        } else if (typeof title === "object") {
            options = title;
        }

        delete options.closeable;

        options.wrapper = $('<div>',{style:"height:100%"}).appendTo("#red-ui-sidebar-content")
        options.wrapper.append(options.content);
        options.wrapper.hide();

        if (!options.enableOnEdit) {
            options.shade = $('<div>',{class:"red-ui-sidebar-shade hide"}).appendTo(options.wrapper);
        }

        if (options.toolbar) {
            $("#red-ui-sidebar-footer").append(options.toolbar);
            $(options.toolbar).hide();
        }
        var id = options.id;

        RED.menu.addItem("menu-item-view-menu",{
            id:"menu-item-view-menu-"+options.id,
            label:options.name,
            onselect:function() {
                showSidebar(options.id);
            },
            group: "sidebar-tabs"
        });

        options.iconClass = options.iconClass || "fa fa-square-o"

        knownTabs[options.id] = options;

        if (options.visible !== false) {
            sidebar_tabs.addTab(knownTabs[options.id]);
        }
    }

    function removeTab(id) {
        sidebar_tabs.removeTab(id);
        $(knownTabs[id].wrapper).remove();
        if (knownTabs[id].footer) {
            knownTabs[id].footer.remove();
        }
        delete knownTabs[id];
        RED.menu.removeItem("menu-item-view-menu-"+id);
    }

    var sidebarSeparator =  {};
    sidebarSeparator.dragging = false;

    function setupSidebarSeparator() {
        $("#red-ui-sidebar-separator").draggable({
                axis: "x",
                start:function(event,ui) {
                    sidebarSeparator.closing = false;
                    sidebarSeparator.opening = false;
                    var winWidth = $("#red-ui-editor").width();
                    sidebarSeparator.start = ui.position.left;
                    sidebarSeparator.chartWidth = $("#red-ui-workspace").width();
                    sidebarSeparator.chartRight = winWidth-$("#red-ui-workspace").width()-$("#red-ui-workspace").offset().left-2;
                    sidebarSeparator.dragging = true;

                    if (!RED.menu.isSelected("menu-item-sidebar")) {
                        sidebarSeparator.opening = true;
                        var newChartRight = 7;
                        $("#red-ui-sidebar").addClass("closing");
                        $("#red-ui-workspace").css("right",newChartRight);
                        $("#red-ui-editor-stack").css("right",newChartRight+1);
                        $("#red-ui-sidebar").width(0);
                        RED.menu.setSelected("menu-item-sidebar",true);
                        RED.events.emit("sidebar:resize");
                    }
                    sidebarSeparator.width = $("#red-ui-sidebar").width();
                },
                drag: function(event,ui) {
                    var d = ui.position.left-sidebarSeparator.start;
                    var newSidebarWidth = sidebarSeparator.width-d;
                    if (sidebarSeparator.opening) {
                        newSidebarWidth -= 3;
                    }

                    if (newSidebarWidth > 150) {
                        if (sidebarSeparator.chartWidth+d < 200) {
                            ui.position.left = 200+sidebarSeparator.start-sidebarSeparator.chartWidth;
                            d = ui.position.left-sidebarSeparator.start;
                            newSidebarWidth = sidebarSeparator.width-d;
                        }
                    }

                    if (newSidebarWidth < 150) {
                        if (!sidebarSeparator.closing) {
                            $("#red-ui-sidebar").addClass("closing");
                            sidebarSeparator.closing = true;
                        }
                        if (!sidebarSeparator.opening) {
                            newSidebarWidth = 150;
                            ui.position.left = sidebarSeparator.width-(150 - sidebarSeparator.start);
                            d = ui.position.left-sidebarSeparator.start;
                        }
                    } else if (newSidebarWidth > 150 && (sidebarSeparator.closing || sidebarSeparator.opening)) {
                        sidebarSeparator.closing = false;
                        $("#red-ui-sidebar").removeClass("closing");
                    }

                    var newChartRight = sidebarSeparator.chartRight-d;
                    $("#red-ui-workspace").css("right",newChartRight);
                    $("#red-ui-editor-stack").css("right",newChartRight+1);
                    $("#red-ui-sidebar").width(newSidebarWidth);

                    sidebar_tabs.resize();
                    RED.events.emit("sidebar:resize");
                },
                stop:function(event,ui) {
                    sidebarSeparator.dragging = false;
                    if (sidebarSeparator.closing) {
                        $("#red-ui-sidebar").removeClass("closing");
                        RED.menu.setSelected("menu-item-sidebar",false);
                        if ($("#red-ui-sidebar").width() < 180) {
                            $("#red-ui-sidebar").width(180);
                            $("#red-ui-workspace").css("right",187);
                            $("#red-ui-editor-stack").css("right",188);
                        }
                    }
                    $("#red-ui-sidebar-separator").css("left","auto");
                    $("#red-ui-sidebar-separator").css("right",($("#red-ui-sidebar").width()+2)+"px");
                    RED.events.emit("sidebar:resize");
                }
        });

        var sidebarControls = $('<div class="red-ui-sidebar-control-right"><i class="fa fa-chevron-right"</div>').appendTo($("#red-ui-sidebar-separator"));
        sidebarControls.on("click", function() {
            sidebarControls.hide();
            RED.menu.toggleSelected("menu-item-sidebar");
        })
        $("#red-ui-sidebar-separator").on("mouseenter", function() {
            if (!sidebarSeparator.dragging) {
                if (RED.menu.isSelected("menu-item-sidebar")) {
                    sidebarControls.find("i").addClass("fa-chevron-right").removeClass("fa-chevron-left");
                } else {
                    sidebarControls.find("i").removeClass("fa-chevron-right").addClass("fa-chevron-left");
                }
                sidebarControls.toggle("slide", { direction: "right" }, 200);
            }
        })
        $("#red-ui-sidebar-separator").on("mouseleave", function() {
            if (!sidebarSeparator.dragging) {
                sidebarControls.stop(false,true);
                sidebarControls.hide();
            }
        });
    }

    function toggleSidebar(state) {
        if (!state) {
            $("#red-ui-main-container").addClass("red-ui-sidebar-closed");
        } else {
            $("#red-ui-main-container").removeClass("red-ui-sidebar-closed");
            sidebar_tabs.resize();
        }
        RED.events.emit("sidebar:resize");
    }

    function showSidebar(id, skipShowSidebar) {
        if (id === ":first") {
            id = lastSessionSelectedTab || RED.settings.get("editor.sidebar.order",["info", "help", "version-control", "debug"])[0]
        }
        if (id) {
            if (!containsTab(id) && knownTabs[id]) {
                sidebar_tabs.addTab(knownTabs[id]);
            }
            sidebar_tabs.activateTab(id);
            if (!skipShowSidebar && !RED.menu.isSelected("menu-item-sidebar")) {
                RED.menu.setSelected("menu-item-sidebar",true);
            }
        }
    }

    function containsTab(id) {
        return sidebar_tabs.contains(id);
    }

    function init () {
        setupSidebarSeparator();
        sidebar_tabs = RED.tabs.create({
            element: $('<ul id="red-ui-sidebar-tabs"></ul>').appendTo("#red-ui-sidebar"),
            onchange:function(tab) {
                $("#red-ui-sidebar-content").children().hide();
                $("#red-ui-sidebar-footer").children().hide();
                if (tab.onchange) {
                    tab.onchange.call(tab);
                }
                $(tab.wrapper).show();
                if (tab.toolbar) {
                    $(tab.toolbar).show();
                }
                RED.settings.setLocal("last-sidebar-tab", tab.id)
            },
            onremove: function(tab) {
                $(tab.wrapper).hide();
                if (tab.onremove) {
                    tab.onremove.call(tab);
                }
            },
            // minimumActiveTabWidth: 70,
            collapsible: true,
            onreorder: function(order) {
                RED.settings.set("editor.sidebar.order",order);
            },
            order: RED.settings.get("editor.sidebar.order",["info", "help", "version-control", "debug"])
            // scrollable: true
        });

        $('<div id="red-ui-sidebar-content"></div>').appendTo("#red-ui-sidebar");
        $('<div id="red-ui-sidebar-footer" class="red-ui-component-footer"></div>').appendTo("#red-ui-sidebar");
        $('<div id="red-ui-sidebar-shade" class="hide"></div>').appendTo("#red-ui-sidebar");

        RED.actions.add("core:toggle-sidebar",function(state){
            if (state === undefined) {
                RED.menu.toggleSelected("menu-item-sidebar");
            } else {
                toggleSidebar(state);
            }
        });
        RED.popover.tooltip($("#red-ui-sidebar-separator").find(".red-ui-sidebar-control-right"),RED._("keyboard.toggleSidebar"),"core:toggle-sidebar");

        lastSessionSelectedTab = RED.settings.getLocal("last-sidebar-tab")

        RED.sidebar.info.init();
        RED.sidebar.help.init();
        RED.sidebar.config.init();
        RED.sidebar.context.init();
        // hide info bar at start if screen rather narrow...
        if ($("#red-ui-editor").width() < 600) { RED.menu.setSelected("menu-item-sidebar",false); }
    }

    return {
        init: init,
        addTab: addTab,
        removeTab: removeTab,
        show: showSidebar,
        containsTab: containsTab,
        toggleSidebar: toggleSidebar,
    }

})();
;/**
 * 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.
 **/

RED.palette = (function() {

    var exclusion = ['config','unknown','deprecated'];
    var coreCategories = [
        'subflows',
        'common',
        'function',
        'network',
        'input',
        'output',
        'sequence',
        'parser',
        'storage',
        'analysis',
        'social',
        'advanced'
    ];

    var categoryContainers = {};
    var sidebarControls;

    let paletteState = { filter: "", collapsed: [] };

    let filterRefreshTimeout

    function createCategory(originalCategory,rootCategory,category,ns) {
        if ($("#red-ui-palette-base-category-"+rootCategory).length === 0) {
            createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory);
        }
        $("#red-ui-palette-container-"+rootCategory).show();
        if ($("#red-ui-palette-"+category).length === 0) {
            $("#red-ui-palette-base-category-"+rootCategory).append('<div id="red-ui-palette-'+category+'"></div>');
        }
    }
    function createCategoryContainer(originalCategory,category, labelId) {
        var label = RED._(labelId, {defaultValue:category});
        label = (label || category).replace(/_/g, " ");
        var catDiv = $('<div id="red-ui-palette-container-'+category+'" class="red-ui-palette-category hide">'+
            '<div id="red-ui-palette-header-'+category+'" class="red-ui-palette-header"><i class="expanded fa fa-angle-down"></i><span>'+label+'</span></div>'+
            '<div class="red-ui-palette-content" id="red-ui-palette-base-category-'+category+'">'+
            '<div id="red-ui-palette-'+category+'"></div>'+
            '<div id="red-ui-palette-'+category+'-input"></div>'+
            '<div id="red-ui-palette-'+category+'-output"></div>'+
            '<div id="red-ui-palette-'+category+'-function"></div>'+
            '</div>'+
            '</div>').appendTo("#red-ui-palette-container");
        catDiv.data('category',originalCategory);
        catDiv.data('label',label);
        categoryContainers[category] = {
            container: catDiv,
            hide: function (instant) {
                if (instant) {
                    catDiv.hide()
                } else {
                    catDiv.slideUp()
                }
            },
            show: function () {
                catDiv.show()
            },
            isOpen: function () {
                return !!catDiv.hasClass("red-ui-palette-open")
            },
            getNodeCount: function (visibleOnly) {
                const nodes = catDiv.find(".red-ui-palette-node")
                if (visibleOnly) {
                    return nodes.filter(function() { return $(this).css('display') !== 'none'}).length
                } else {
                    return nodes.length
                }
            },
            close: function(instant, skipSaveState) {
                catDiv.removeClass("red-ui-palette-open");
                catDiv.addClass("red-ui-palette-closed");
                if (instant) {
                    $("#red-ui-palette-base-category-"+category).hide();
                } else {
                    $("#red-ui-palette-base-category-"+category).slideUp();
                }
                $("#red-ui-palette-header-"+category+" i").removeClass("expanded");
                if (!skipSaveState) {
                    if (!paletteState.collapsed.includes(category)) {
                        paletteState.collapsed.push(category);
                        savePaletteState();
                    }
                }
            },
            open: function(skipSaveState) {
                catDiv.addClass("red-ui-palette-open");
                catDiv.removeClass("red-ui-palette-closed");
                $("#red-ui-palette-base-category-"+category).slideDown();
                $("#red-ui-palette-header-"+category+" i").addClass("expanded");
                if (!skipSaveState) {
                    if (paletteState.collapsed.includes(category)) {
                        paletteState.collapsed.splice(paletteState.collapsed.indexOf(category), 1);
                        savePaletteState();
                    }
                }
            },
            toggle: function() {
                if (categoryContainers[category].isOpen()) {
                    categoryContainers[category].close();
                } else {
                    categoryContainers[category].open();
                }
            }
        };

        $("#red-ui-palette-header-"+category).on('click', function(e) {
            categoryContainers[category].toggle();
        });
    }

    function setLabel(type, el,label, info) {
        var nodeWidth = 82;
        var nodeHeight = 25;
        var lineHeight = 20;
        var portHeight = 10;

        el.attr("data-palette-label",label);

        label = RED.utils.sanitize(label);


        var words = label.split(/([ -]|\\n )/);

        var displayLines = [];

        var currentLine = "";
        for (var i=0;i<words.length;i++) {
            var word = words[i];
            if (word === "\\n ") {
                displayLines.push(currentLine);
                currentLine = "";
                continue;
            }
            var sep = (i == 0) ? "" : " ";
            var newWidth = RED.view.calculateTextWidth(currentLine+sep+word, "red-ui-palette-label");
            if (newWidth < nodeWidth) {
                currentLine += sep +word;
            } else {
                if (i > 0) {
                    displayLines.push(currentLine);
                }
                while (true) {
                    var wordWidth = RED.view.calculateTextWidth(word, "red-ui-palette-label");
                    if (wordWidth >= nodeWidth) {
                        // break word if too wide
                        for(var j = word.length; j > 0; j--) {
                            var s = word.substring(0, j);
                            var width = RED.view.calculateTextWidth(s, "red-ui-palette-label");
                            if (width < nodeWidth) {
                                displayLines.push(s);
                                word = word.substring(j);
                                break;
                            }
                        }
                    }
                    else {
                        currentLine = word;
                        break;
                    }
                }
            }
        }
        displayLines.push(currentLine);

        var lines = displayLines.join("<br/>");
        var multiLineNodeHeight = 8+(lineHeight*displayLines.length);
        el.css({height:multiLineNodeHeight+"px"});

        var labelElement = el.find(".red-ui-palette-label");
        labelElement.html(lines).attr('dir', RED.text.bidi.resolveBaseTextDir(lines));

        el.find(".red-ui-palette-port").css({top:(multiLineNodeHeight/2-5)+"px"});

        var popOverContent;
        try {
            var l = "<p><b>"+RED.text.bidi.enforceTextDirectionWithUCC(label)+"</b></p>";
            popOverContent = $('<div></div>').append($(l+(info?info:RED.nodes.getNodeHelp(type)||"<p>"+RED._("palette.noInfo")+"</p>").trim())
                                .filter(function(n) {
                                    return (this.nodeType == 1 && this.nodeName == "P") || (this.nodeType == 3 && this.textContent.trim().length > 0)
                                }).slice(0,2));
            popOverContent.find("a").each(function(){
                var linkText = $(this).text();
                $(this).before(linkText);
                $(this).remove();
            });

            var typeInfo = RED.nodes.getType(type);

            if (typeInfo) {
                var metaData = "";
                if (typeInfo && !/^subflow:/.test(type)) {
                    metaData = typeInfo.set.module+" : ";
                }
                metaData += type;

                const safeType = type.replace(/'/g,"\\'");
                const searchType = type.indexOf(' ') > -1 ? '&quot;' + type + '&quot;' : type

                if (/^subflow:/.test(type)) {
                    $('<button type="button" onclick="RED.workspaces.show(\''+type.substring(8).replace(/'/g,"\\'")+'\'); return false;" class="red-ui-button red-ui-button-small" style="float: right; margin-left: 5px;"><i class="fa fa-pencil"></i></button>').appendTo(popOverContent)
                }

                $('<button type="button" onclick="RED.search.show(\'type:'+searchType+'\'); return false;" class="red-ui-button red-ui-button-small" style="float: right; margin-left: 5px;"><i class="fa fa-search"></i></button>').appendTo(popOverContent)

                $('<button type="button" onclick="RED.sidebar.help.show(\''+safeType+'\'); return false;" class="red-ui-button red-ui-button-small" style="float: right; margin-left: 5px;"><i class="fa fa-book"></i></button>').appendTo(popOverContent)

                $('<p>',{style:"font-size: 0.8em"}).text(metaData).appendTo(popOverContent);
            }
        } catch(err) {
            // Malformed HTML may cause errors. TODO: need to understand what can break
            // NON-NLS: internal debug
            console.log("Error generating pop-over label for ",type);
            console.log(err.toString());
            popOverContent = "<p><b>"+label+"</b></p><p>"+RED._("palette.noInfo")+"</p>";
        }

        el.data('popover').setContent(popOverContent);
    }

    function setIcon(element,sf) {
        var icon_url = RED.utils.getNodeIcon(sf._def);
        var iconContainer = element.find(".red-ui-palette-icon-container");
        var currentIcon = iconContainer.attr("data-palette-icon");
        if (currentIcon !== icon_url) {
            iconContainer.attr("data-palette-icon", icon_url);
            RED.utils.createIconElement(icon_url, iconContainer, true);
        }
    }

    function getPaletteNode(type) {
        return $(".red-ui-palette-node[data-palette-type='"+type+"']");
    }

    function escapeCategory(category) {
        return category.replace(/[\x00-\x2c\x2e-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]/g,"_");
    }
    function addNodeType(nt,def) {
        if (getPaletteNode(nt).length) {
            return;
        }
        var nodeCategory = def.category;

        if (exclusion.indexOf(nodeCategory)===-1) {

            var originalCategory = nodeCategory;
            var category = escapeCategory(nodeCategory);
            var rootCategory = category.split("-")[0];

            var d = $('<div>',{class:"red-ui-palette-node"}).attr("data-palette-type",nt).data('category',rootCategory);

            var label = RED.utils.getPaletteLabel(nt, def);///^(.*?)([ -]in|[ -]out)?$/.exec(nt)[1];

            $('<div/>', {
                class: "red-ui-palette-label"+(((!def.align && def.inputs !== 0 && def.outputs === 0) || "right" === def.align) ? " red-ui-palette-label-right" : "")
            }).appendTo(d);


            if (def.icon) {
                var icon_url = RED.utils.getNodeIcon(def);
                var iconContainer = $('<div/>', {
                    class: "red-ui-palette-icon-container"+(((!def.align && def.inputs !== 0 && def.outputs === 0) || "right" === def.align) ? " red-ui-palette-icon-container-right" : "")
                }).appendTo(d);
                iconContainer.attr("data-palette-icon", icon_url);
                RED.utils.createIconElement(icon_url, iconContainer, true);
            }

            d.css("backgroundColor", RED.utils.getNodeColor(nt,def));

            if (def.outputs > 0) {
                var portOut = document.createElement("div");
                portOut.className = "red-ui-palette-port red-ui-palette-port-output";
                d.append(portOut);
            }

            if (def.inputs > 0) {
                var portIn = document.createElement("div");
                portIn.className = "red-ui-palette-port red-ui-palette-port-input";
                d.append(portIn);
            }

            createCategory(nodeCategory,rootCategory,category,(coreCategories.indexOf(rootCategory) !== -1)?"node-red":def.set.id);

            $("#red-ui-palette-"+category).append(d);

            d.on("mousedown", function(e) { e.preventDefault();});

            var popover = RED.popover.create({
                target:d,
                trigger: "hover",
                interactive: true,
                width: "300px",
                content: "hi",
                delay: { show: 750, hide: 50 }
            });

            d.data('popover',popover);

            var chart = $("#red-ui-workspace-chart");
            var chartSVG = $("#red-ui-workspace-chart>svg").get(0);
            var activeSpliceLink;
            var mouseX;
            var mouseY;
            var spliceTimer;
            var groupTimer;
            var activeGroup;
            var hoverGroup;
            var paletteWidth;
            var paletteTop;
            var dropEnabled;
            $(d).draggable({
                helper: 'clone',
                appendTo: '#red-ui-editor',
                revert: 'invalid',
                revertDuration: 200,
                containment:'#red-ui-main-container',
                start: function() {
                    dropEnabled = !(RED.nodes.workspace(RED.workspaces.active())?.locked);
                    paletteWidth = $("#red-ui-palette").width();
                    paletteTop = $("#red-ui-palette").parent().position().top + $("#red-ui-palette-container").position().top;
                    hoverGroup = null;
                    activeGroup = RED.view.getActiveGroup();
                    if (activeGroup) {
                        document.getElementById("group_select_"+activeGroup.id).classList.add("red-ui-flow-group-active-hovered");
                    }
                    RED.view.focus();
                },
                stop: function() {
                    if (dropEnabled) {
                        d3.select('.red-ui-flow-link-splice').classed('red-ui-flow-link-splice',false);
                        if (hoverGroup) {
                            document.getElementById("group_select_"+hoverGroup.id).classList.remove("red-ui-flow-group-hovered");
                        }
                        if (activeGroup) {
                            document.getElementById("group_select_"+activeGroup.id).classList.remove("red-ui-flow-group-active-hovered");
                        }
                        if (spliceTimer) { clearTimeout(spliceTimer); spliceTimer = null; }
                        if (groupTimer) { clearTimeout(groupTimer); groupTimer = null; }
                    }
                },
                drag: function(e,ui) {
                    var paletteNode = getPaletteNode(nt);
                    ui.originalPosition.left = paletteNode.offset().left;
                    if (dropEnabled) {
                        mouseX = ui.position.left - paletteWidth + (ui.helper.width()/2) + chart.scrollLeft();
                        mouseY = ui.position.top - paletteTop + (ui.helper.height()/2) + chart.scrollTop() + 10;
                        if (!groupTimer) {
                            groupTimer = setTimeout(function() {
                                var mx = mouseX / RED.view.scale();
                                var my = mouseY / RED.view.scale();
                                var group = RED.view.getGroupAtPoint(mx,my);
                                if (group !== hoverGroup) {
                                    if (hoverGroup) {
                                        document.getElementById("group_select_"+hoverGroup.id).classList.remove("red-ui-flow-group-hovered");
                                    }
                                    if (group) {
                                        document.getElementById("group_select_"+group.id).classList.add("red-ui-flow-group-hovered");
                                    }
                                    hoverGroup = group;
                                    if (hoverGroup) {
                                        $(ui.helper).data('group',hoverGroup);
                                    } else {
                                        $(ui.helper).removeData('group');
                                    }
                                }
                                groupTimer = null;

                            },200)
                        }
                        if (def.inputs > 0 && def.outputs > 0) {
                            if (!spliceTimer) {
                                spliceTimer = setTimeout(function() {
                                    var nodes = [];
                                    var bestDistance = Infinity;
                                    var bestLink = null;
                                    if (chartSVG.getIntersectionList) {
                                        var svgRect = chartSVG.createSVGRect();
                                        svgRect.x = mouseX;
                                        svgRect.y = mouseY;
                                        svgRect.width = 1;
                                        svgRect.height = 1;
                                        nodes = chartSVG.getIntersectionList(svgRect,chartSVG);
                                    } else {
                                        // Firefox doesn't do getIntersectionList and that
                                        // makes us sad
                                        nodes = RED.view.getLinksAtPoint(mouseX,mouseY);
                                    }
                                    var mx = mouseX / RED.view.scale();
                                    var my = mouseY / RED.view.scale();
                                    for (var i=0;i<nodes.length;i++) {
                                        var node = d3.select(nodes[i]);
                                        if (node.classed('red-ui-flow-link-background') && !node.classed('red-ui-flow-link-link')) {
                                            var length = nodes[i].getTotalLength();
                                            for (var j=0;j<length;j+=10) {
                                                var p = nodes[i].getPointAtLength(j);
                                                var d2 = ((p.x-mx)*(p.x-mx))+((p.y-my)*(p.y-my));
                                                if (d2 < 200 && d2 < bestDistance) {
                                                    bestDistance = d2;
                                                    bestLink = nodes[i];
                                                }
                                            }
                                        }
                                    }
                                    if (activeSpliceLink && activeSpliceLink !== bestLink) {
                                        d3.select(activeSpliceLink.parentNode).classed('red-ui-flow-link-splice',false);
                                    }
                                    if (bestLink) {
                                        d3.select(bestLink.parentNode).classed('red-ui-flow-link-splice',true)
                                    } else {
                                        d3.select('.red-ui-flow-link-splice').classed('red-ui-flow-link-splice',false);
                                    }
                                    if (activeSpliceLink !== bestLink) {
                                        if (bestLink) {
                                            $(ui.helper).data('splice',d3.select(bestLink).data()[0]);
                                        } else {
                                            $(ui.helper).removeData('splice');
                                        }
                                    }
                                    activeSpliceLink = bestLink;
                                    spliceTimer = null;
                                },200);
                            }
                        }
                    }
                }
            });

            var nodeInfo = null;
            if (nt.indexOf("subflow:") === 0) {
                d.on("dblclick", function(e) {
                    RED.workspaces.show(nt.substring(8));
                    e.preventDefault();
                });
                var subflow = RED.nodes.subflow(nt.substring(8));
                nodeInfo = RED.utils.renderMarkdown(subflow.info||"");
            }
            setLabel(nt,d,label,nodeInfo);

            var categoryNode = $("#red-ui-palette-container-"+rootCategory);
            if (categoryNode.find(".red-ui-palette-node").length === 1) {
                if (!paletteState?.collapsed?.includes(rootCategory)) {
                    categoryContainers[rootCategory].open();
                } else {
                    categoryContainers[rootCategory].close(true);
                }
            }
            clearTimeout(filterRefreshTimeout)
            filterRefreshTimeout = setTimeout(() => {
                refreshFilter()
            }, 200)

        }
    }

    function removeNodeType(nt) {
        var paletteNode = getPaletteNode(nt);
        var categoryNode = paletteNode.closest(".red-ui-palette-category");
        paletteNode.remove();
        if (categoryNode.find(".red-ui-palette-node").length === 0) {
            if (categoryNode.find("i").hasClass("expanded")) {
                categoryNode.find(".red-ui-palette-content").slideToggle();
                categoryNode.find("i").toggleClass("expanded");
            }
            categoryNode.hide();
        }
    }

    function hideNodeType(nt) {
        var paletteNode = getPaletteNode(nt);
        paletteNode.hide();
        var categoryNode = paletteNode.closest(".red-ui-palette-category");
        var cl = categoryNode.find(".red-ui-palette-node");
        var c = 0;
        for (var i = 0; i < cl.length; i++) {
            if ($(cl[i]).css('display') === 'none') { c += 1; }
        }
        if (c === cl.length) { categoryNode.hide(); }
    }

    function showNodeType(nt) {
        var paletteNode = getPaletteNode(nt);
        var categoryNode = paletteNode.closest(".red-ui-palette-category");
        categoryNode.show();
        paletteNode.show();
    }
    function refreshNodeTypes() {
        RED.nodes.eachSubflow(refreshSubflow)
    }
    function refreshSubflow(sf) {
        var paletteNode = getPaletteNode('subflow:'+sf.id);
        var portInput = paletteNode.find(".red-ui-palette-port-input");
        var portOutput = paletteNode.find(".red-ui-palette-port-output");

        var paletteLabel = paletteNode.find(".red-ui-palette-label");
        paletteLabel.attr("class","red-ui-palette-label" + (((!sf._def.align && sf.in.length !== 0 && sf.out.length === 0) || "right" === sf._def.align) ? " red-ui-palette-label-right" : ""));

        var paletteIconContainer = paletteNode.find(".red-ui-palette-icon-container");
        paletteIconContainer.attr("class","red-ui-palette-icon-container" + (((!sf._def.align && sf.in.length !== 0 && sf.out.length === 0) || "right" === sf._def.align) ? " red-ui-palette-icon-container-right" : ""));

        if (portInput.length === 0 && sf.in.length > 0) {
            var portIn = document.createElement("div");
            portIn.className = "red-ui-palette-port red-ui-palette-port-input";
            paletteNode.append(portIn);
        } else if (portInput.length !== 0 && sf.in.length === 0) {
            portInput.remove();
        }

        if (portOutput.length === 0 && sf.out.length > 0) {
            var portOut = document.createElement("div");
            portOut.className = "red-ui-palette-port red-ui-palette-port-output";
            paletteNode.append(portOut);
        } else if (portOutput.length !== 0 && sf.out.length === 0) {
            portOutput.remove();
        }
        var currentLabel = paletteNode.attr("data-palette-label");
        var currentInfo = paletteNode.attr("data-palette-info");

        if (currentLabel !== sf.name || currentInfo !== sf.info || sf.in.length > 0 || sf.out.length > 0) {
            paletteNode.attr("data-palette-info",sf.info);
            setLabel(sf.type+":"+sf.id,paletteNode,sf.name,RED.utils.renderMarkdown(sf.info||""));
        }
        setIcon(paletteNode,sf);

        var currentCategory = paletteNode.data('category');
        var newCategory = (sf.category||"subflows");
        if (currentCategory !== newCategory) {
            var category = escapeCategory(newCategory);
            createCategory(newCategory,category,category,"node-red");

            var currentCategoryNode = paletteNode.closest(".red-ui-palette-category");
            var newCategoryNode = $("#red-ui-palette-"+category);
            newCategoryNode.append(paletteNode);
            if (newCategoryNode.find(".red-ui-palette-node").length === 1) {
                categoryContainers[category].open();
            }

            paletteNode.data('category',newCategory);
            if (currentCategoryNode.find(".red-ui-palette-node").length === 0) {
                if (currentCategoryNode.find("i").hasClass("expanded")) {
                    currentCategoryNode.find(".red-ui-palette-content").slideToggle();
                    currentCategoryNode.find("i").toggleClass("expanded");
                }
                currentCategoryNode.hide();
            }
        }

        paletteNode.css("backgroundColor", sf.color);
    }

    function refreshFilter() {
        const val = $("#red-ui-palette-search input").val()
        var re = new RegExp(val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),'i');
        $("#red-ui-palette-container .red-ui-palette-node").each(function(i,el) {
            var currentLabel = $(el).attr("data-palette-label");
            var type = $(el).attr("data-palette-type");
            if (val === "" || re.test(type) || re.test(currentLabel)) {
                $(this).show();
            } else {
                $(this).hide();
            }
        });

        for (let category in categoryContainers) {
            if (categoryContainers.hasOwnProperty(category)) {
                const categorySection = categoryContainers[category]
                if (categorySection.getNodeCount(true) === 0) {
                    categorySection.hide()
                } else {
                    categorySection.show()
                    if (val) {
                        // There is a filter being applied and it has matched
                        // something in this category - show the contents
                        categorySection.open(true)
                    } else {
                        // No filter. Only show the category if it isn't in lastState
                        if (!paletteState.collapsed.includes(category)) {
                            categorySection.open(true)
                        } else if (categorySection.isOpen()) {
                            // This section should be collapsed but isn't - so make it so
                            categorySection.close(true, true)
                        }
                    }
                }
            }
        }
    }

    function init() {

        $('<img src="red/images/spin.svg" class="red-ui-palette-spinner hide"/>').appendTo("#red-ui-palette");
        $('<div id="red-ui-palette-search" class="red-ui-palette-search hide"><input type="text" data-i18n="[placeholder]palette.filter"></input></div>').appendTo("#red-ui-palette");
        $('<div id="red-ui-palette-container" class="red-ui-palette-scroll hide"></div>').appendTo("#red-ui-palette");
        $('<div class="red-ui-component-footer"></div>').appendTo("#red-ui-palette");
        $('<div id="red-ui-palette-shade" class="hide"></div>').appendTo("#red-ui-palette");

        $("#red-ui-palette > .red-ui-palette-spinner").show();

        RED.events.on('logout', function () {
            RED.settings.removeLocal('palette-state')
        })

        RED.events.on('registry:node-type-added', function(nodeType) {
            var def = RED.nodes.getType(nodeType);
            addNodeType(nodeType,def);
            if (def.onpaletteadd && typeof def.onpaletteadd === "function") {
                def.onpaletteadd.call(def);
            }
        });
        RED.events.on('registry:node-type-removed', function(nodeType) {
            removeNodeType(nodeType);
        });
        RED.events.on('registry:node-set-enabled', function(nodeSet) {
            for (var j=0;j<nodeSet.types.length;j++) {
                showNodeType(nodeSet.types[j]);
                var def = RED.nodes.getType(nodeSet.types[j]);
                if (def && def.onpaletteadd && typeof def.onpaletteadd === "function") {
                    def.onpaletteadd.call(def);
                }
            }
        });
        RED.events.on('registry:node-set-disabled', function(nodeSet) {
            for (var j=0;j<nodeSet.types.length;j++) {
                hideNodeType(nodeSet.types[j]);
                var def = RED.nodes.getType(nodeSet.types[j]);
                if (def && def.onpaletteremove && typeof def.onpaletteremove === "function") {
                    def.onpaletteremove.call(def);
                }
            }
        });
        RED.events.on('registry:node-set-removed', function(nodeSet) {
            if (nodeSet.added) {
                for (var j=0;j<nodeSet.types.length;j++) {
                    removeNodeType(nodeSet.types[j]);
                    var def = RED.nodes.getType(nodeSet.types[j]);
                    if (def && def.onpaletteremove && typeof def.onpaletteremove === "function") {
                        def.onpaletteremove.call(def);
                    }
                }
            }
        });

        RED.events.on("subflows:change",refreshSubflow);

        $("#red-ui-palette-search input").searchBox({
            delay: 100,
            change: function() {
                refreshFilter();
                paletteState.filter = $(this).val();
                savePaletteState();
            }
        });

        sidebarControls = $('<div class="red-ui-sidebar-control-left"><i class="fa fa-chevron-left"></i></div>').appendTo($("#red-ui-palette"));
        RED.popover.tooltip(sidebarControls,RED._("keyboard.togglePalette"),"core:toggle-palette");

        sidebarControls.on("click", function() {
            RED.menu.toggleSelected("menu-item-palette");
        })
        $("#red-ui-palette").on("mouseenter", function() {
            sidebarControls.toggle("slide", { direction: "left" }, 200);
        })
        $("#red-ui-palette").on("mouseleave", function() {
            sidebarControls.stop(false,true);
            sidebarControls.hide();
        })
        var userCategories = [];
        if (RED.settings.paletteCategories) {
            userCategories = RED.settings.paletteCategories;
        } else if (RED.settings.theme('palette.categories')) {
            userCategories = RED.settings.theme('palette.categories');
        }
        if (!Array.isArray(userCategories)) {
            userCategories = [];
        }

        var addedCategories = {};
        userCategories.forEach(function(category){
            addedCategories[category] = true;
            createCategoryContainer(category, escapeCategory(category), "palette.label."+escapeCategory(category));
        });
        coreCategories.forEach(function(category){
            if (!addedCategories[category]) {
                createCategoryContainer(category, escapeCategory(category), "palette.label."+escapeCategory(category));
            }
        });

        var paletteFooterButtons = $('<span class="button-group"></span>').appendTo("#red-ui-palette .red-ui-component-footer");
        var paletteCollapseAll = $('<button type="button" class="red-ui-footer-button"><i class="fa fa-angle-double-up"></i></button>').appendTo(paletteFooterButtons);
        paletteCollapseAll.on("click", function(e) {
            e.preventDefault();
            for (var cat in categoryContainers) {
                if (categoryContainers.hasOwnProperty(cat)) {
                    categoryContainers[cat].close();
                }
            }
        });
        RED.popover.tooltip(paletteCollapseAll,RED._('palette.actions.collapse-all'));

        var paletteExpandAll = $('<button type="button" class="red-ui-footer-button"><i class="fa fa-angle-double-down"></i></button>').appendTo(paletteFooterButtons);
        paletteExpandAll.on("click", function(e) {
            e.preventDefault();
            for (var cat in categoryContainers) {
                if (categoryContainers.hasOwnProperty(cat)) {
                    categoryContainers[cat].open();
                }
            }
        });
        RED.popover.tooltip(paletteExpandAll,RED._('palette.actions.expand-all'));

        RED.actions.add("core:toggle-palette", function(state) {
            if (state === undefined) {
                RED.menu.toggleSelected("menu-item-palette");
            } else {
                togglePalette(state);
            }
        });

        try {
            paletteState = JSON.parse(RED.settings.getLocal("palette-state") || '{"filter":"", "collapsed": []}');
            if (paletteState.filter) {
                // Apply the category filter
                $("#red-ui-palette-search input").searchBox("value", paletteState.filter);
            }
        } catch (error) {
            console.error("Unexpected error loading palette state from localStorage: ", error);
        }
        setTimeout(() => {
            // Lazily tidy up any categories that haven't been reloaded
            paletteState.collapsed = paletteState.collapsed.filter(category => !!categoryContainers[category])
            savePaletteState()
        }, 10000)
    }

    function togglePalette(state) {
        if (!state) {
            $("#red-ui-main-container").addClass("red-ui-palette-closed");
            sidebarControls.hide();
            sidebarControls.find("i").addClass("fa-chevron-right").removeClass("fa-chevron-left");
        } else {
            $("#red-ui-main-container").removeClass("red-ui-palette-closed");
            sidebarControls.find("i").removeClass("fa-chevron-right").addClass("fa-chevron-left");
        }
        setTimeout(function() { $(window).trigger("resize"); } ,200);
    }

    function getCategories() {
        var categories = [];
        $("#red-ui-palette-container .red-ui-palette-category").each(function(i,d) {
            categories.push({id:$(d).data('category'),label:$(d).data('label')});
        })
        return categories;
    }

    function savePaletteState() {
        try {
            RED.settings.setLocal("palette-state", JSON.stringify(paletteState));
        } catch (error) {
            console.error("Unexpected error saving palette state to localStorage: ", error);
        }
    }

    return {
        init: init,
        add:addNodeType,
        remove:removeNodeType,
        hide:hideNodeType,
        show:showNodeType,
        refresh:refreshNodeTypes,
        getCategories: getCategories
    };
})();
;/**
 * 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.
 **/
RED.sidebar.info = (function() {

    var content;
    var panels;
    var infoSection;

    var propertiesPanelContent;
    var propertiesPanelHeader;
    var propertiesPanelHeaderIcon;
    var propertiesPanelHeaderLabel;
    var propertiesPanelHeaderReveal;
    var propertiesPanelHeaderHelp;
    var propertiesPanelHeaderCopyLink;

    var selectedObject;

    var tipContainer;
    var tipBox;

    // TODO: remove this
    var expandedSections = {
        "property": false
    };

    function resizeStack() {
        if (panels) {
            var h = $(content).parent().height() - tipContainer.outerHeight();
            panels.resize(h)
        }
    }
    function init() {

        content = document.createElement("div");
        content.className = "red-ui-sidebar-info"

        RED.actions.add("core:show-info-tab",show);

        var stackContainer = $("<div>",{class:"red-ui-sidebar-info-stack"}).appendTo(content);

        var outlinerPanel = $("<div>").css({
            "overflow": "hidden",
            "height": "calc(70%)"
        }).appendTo(stackContainer);
        var propertiesPanel = $("<div>").css({
            "overflow":"hidden",
            "height":"100%",
            "display": "flex",
            "flex-direction": "column"
        }).appendTo(stackContainer);
        propertiesPanelHeader = $("<div>", {class:"red-ui-palette-header red-ui-info-header"}).css({
            "flex":"0 0 auto"
        }).appendTo(propertiesPanel);

        propertiesPanelHeaderIcon = $("<span>").appendTo(propertiesPanelHeader);
        propertiesPanelHeaderLabel = $("<span>").appendTo(propertiesPanelHeader);

        propertiesPanelHeaderCopyLink = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-link"></button>').css({
            position: 'absolute',
            top: '12px',
            right: '32px'
        }).on("click", function(evt) {
            RED.actions.invoke('core:copy-item-url',selectedObject)
        }).appendTo(propertiesPanelHeader);
        RED.popover.tooltip(propertiesPanelHeaderCopyLink,RED._("sidebar.info.copyItemUrl"));

        propertiesPanelHeaderHelp = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-book"></button>').css({
            position: 'absolute',
            top: '12px',
            right: '56px'
        }).on("click", function(evt) {
            evt.preventDefault();
            evt.stopPropagation();
            if (selectedObject) {
                RED.sidebar.help.show(selectedObject.type);
            }
        }).appendTo(propertiesPanelHeader);
        RED.popover.tooltip(propertiesPanelHeaderHelp,RED._("sidebar.help.showHelp"));

        propertiesPanelHeaderReveal = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-search"></button>').css({
            position: 'absolute',
            top: '12px',
            right: '8px'
        }).on("click", function(evt) {
            evt.preventDefault();
            evt.stopPropagation();
            if (selectedObject) {
                RED.sidebar.info.outliner.reveal(selectedObject);
                RED.view.reveal(selectedObject.id);
            }
        }).appendTo(propertiesPanelHeader);
        RED.popover.tooltip(propertiesPanelHeaderReveal,RED._("sidebar.help.showInOutline"));


        propertiesPanelContent = $("<div>").css({
            "flex":"1 1 auto",
            "overflow-y":"auto",
        }).appendTo(propertiesPanel);


        panels = RED.panels.create({container: stackContainer})
        panels.ratio(0.6);
        RED.sidebar.info.outliner.build().appendTo(outlinerPanel);


        RED.sidebar.addTab({
            id: "info",
            label: RED._("sidebar.info.label"),
            name: RED._("sidebar.info.name"),
            iconClass: "fa fa-info",
            action:"core:show-info-tab",
            content: content,
            pinned: true,
            enableOnEdit: true
        });

        RED.events.on("sidebar:resize", resizeStack);

        $(window).on("resize", resizeStack);
        $(window).on("focus", resizeStack);


        // Tip Box
        tipContainer = $('<div class="red-ui-help-tips"></div>').appendTo(content);
        tipBox = $('<div class="red-ui-help-tip"></div>').appendTo(tipContainer);
        var tipButtons = $('<div class="red-ui-help-tips-buttons"></div>').appendTo(tipContainer);

        var tipRefresh = $('<a href="#" class="red-ui-footer-button"><i class="fa fa-refresh"></a>').appendTo(tipButtons);
        tipRefresh.on("click", function(e) {
            e.preventDefault();
            tips.next();
        })

        var tipClose = $('<a href="#" class="red-ui-footer-button"><i class="fa fa-times"></a>').appendTo(tipButtons);
        tipClose.on("click", function(e) {
            e.preventDefault();
            RED.actions.invoke("core:toggle-show-tips");
            RED.notify(RED._("sidebar.info.showTips"));
        });
        if (tips.enabled()) {
            tips.start();
        } else {
            tips.stop();
        }

    }

    function show() {
        RED.sidebar.show("info");
    }

    // TODO: DRY - projects.js
    function addTargetToExternalLinks(el) {
        $(el).find("a").each(function(el) {
            var href = $(this).attr('href');
            if (/^https?:/.test(href)) {
                $(this).attr('target','_blank');
            }
        });
        return el;
    }

    function refresh(node) {
        if (node === undefined) {
            refreshSelection();
            return;
        }
        $(propertiesPanelContent).empty();

        var propRow;

        var table = $('<table class="red-ui-info-table"></table>').appendTo(propertiesPanelContent);
        var tableBody = $('<tbody>').appendTo(table);

        var subflowNode;
        var subflowUserCount;

        if (node === null) {
            RED.sidebar.info.outliner.select(null);
            propertiesPanelHeaderIcon.empty();
            propertiesPanelHeaderLabel.text("");
            propertiesPanelHeaderReveal.hide();
            propertiesPanelHeaderHelp.hide();
            propertiesPanelHeaderCopyLink.hide();
            return;
        } else if (Array.isArray(node)) {
            // Multiple things selected
            // - hide help and info sections
            RED.sidebar.info.outliner.select(node);

            propertiesPanelHeaderIcon.empty();
            RED.utils.createNodeIcon({type:"_selection_"}).appendTo(propertiesPanelHeaderIcon);
            propertiesPanelHeaderLabel.text(RED._("sidebar.info.selection"));
            propertiesPanelHeaderReveal.hide();
            propertiesPanelHeaderHelp.hide();
            propertiesPanelHeaderCopyLink.hide();
            selectedObject = null;

            var types = {
                nodes:0,
                flows:0,
                subflows:0,
                groups: 0
            }
            node.forEach(function(n) {
                if (n.type === 'tab') {
                    types.flows++;
                    types.nodes += RED.nodes.filterNodes({z:n.id}).length;
                } else if (n.type === 'subflow') {
                    types.subflows++;
                } else if (n.type === 'group') {
                    types.groups++;
                } else {
                    types.nodes++;
                }
            });
            // infoSection.container.hide();
            // - show the count of selected nodes
            propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("sidebar.info.selection")+"</td><td></td></tr>").appendTo(tableBody);

            var counts = $('<div>').appendTo($(propRow.children()[1]));
            if (types.flows > 0) {
                $('<div>').text(RED._("clipboard.flow",{count:types.flows})).appendTo(counts);
            }
            if (types.subflows > 0) {
                $('<div>').text(RED._("clipboard.subflow",{count:types.subflows})).appendTo(counts);
            }
            if (types.nodes > 0) {
                $('<div>').text(RED._("clipboard.node",{count:types.nodes})).appendTo(counts);
            }
            if (types.groups > 0) {
                $('<div>').text(RED._("clipboard.group",{count:types.groups})).appendTo(counts);
            }
        } else {
            // A single 'thing' selected.

            RED.sidebar.info.outliner.select(node);

            // Check to see if this is a subflow or subflow instance
            var subflowRegex = /^subflow(:(.+))?$/.exec(node.type);
            if (subflowRegex) {
                if (subflowRegex[2]) {
                    subflowNode = RED.nodes.subflow(subflowRegex[2]);
                } else {
                    subflowNode = node;
                }

                var subflowType = "subflow:"+subflowNode.id;
                subflowUserCount = subflowNode.instances.length;
            }

            propertiesPanelHeaderIcon.empty();
            RED.utils.createNodeIcon(node).appendTo(propertiesPanelHeaderIcon);
            var objectLabel = RED.utils.getNodeLabel(node, node.type+": "+node.id)
            var newlineIndex = objectLabel.indexOf("\\n");
            if (newlineIndex > -1) {
                objectLabel = objectLabel.substring(0,newlineIndex)+"...";
            }
            propertiesPanelHeaderLabel.text(objectLabel);
            propertiesPanelHeaderReveal.show();
            selectedObject = node;

            propRow = $('<tr class="red-ui-help-info-row"><td></td><td></td></tr>').appendTo(tableBody);
            var objectType = "node";
            if (node.type === "subflow" || subflowRegex) {
                objectType = "subflow";
            } else if (node.type === "tab") {
                objectType = "flow";
            }else if (node.type === "group") {
                objectType = "group";
            }
            $(propRow.children()[0]).text(RED._("sidebar.info."+objectType))
            RED.utils.createObjectElement(node.id,{sourceId: node.id}).appendTo(propRow.children()[1]);

            if (node.type === "tab" || node.type === "subflow") {
                // If nothing is selected, but we're on a flow or subflow tab.
                propertiesPanelHeaderHelp.hide();
                propertiesPanelHeaderCopyLink.show();

            } else if (node.type === "group") {
                propertiesPanelHeaderHelp.hide();
                propertiesPanelHeaderCopyLink.show();

                propRow = $('<tr class="red-ui-help-info-row"><td>&nbsp;</td><td></td></tr>').appendTo(tableBody);

                var typeCounts = {
                    nodes:0,
                    groups: 0
                }
                var allNodes = RED.group.getNodes(node,true);
                allNodes.forEach(function(n) {
                    if (n.type === "group") {
                        typeCounts.groups++;
                    } else {
                        typeCounts.nodes++
                    }
                });
                var counts = $('<div>').appendTo($(propRow.children()[1]));
                if (typeCounts.nodes > 0) {
                    $('<div>').text(RED._("clipboard.node",{count:typeCounts.nodes})).appendTo(counts);
                }
                if (typeCounts.groups > 0) {
                    $('<div>').text(RED._("clipboard.group",{count:typeCounts.groups})).appendTo(counts);
                }
            } else if (node.type === 'junction') {
                propertiesPanelHeaderHelp.hide();
                propertiesPanelHeaderCopyLink.hide();
            } else {
                propertiesPanelHeaderHelp.show();
                propertiesPanelHeaderCopyLink.show();

                if (!subflowRegex) {
                    propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("sidebar.info.type")+'</td><td></td></tr>').appendTo(tableBody);
                    $(propRow.children()[1]).text((node.type === "unknown")?node._orig.type:node.type);
                    if (node.type === "unknown") {
                        $('<span style="float: right; font-size: 0.8em"><i class="fa fa-warning"></i></span>').prependTo($(propRow.children()[1]))
                    }
                }

                var count = 0;
                if (!subflowRegex && node.type != "subflow" && node.type != "group") {

                    var blankRow = $('<tr class="red-ui-help-property-expand blank"><td colspan="2"></td></tr>').appendTo(tableBody);

                    var defaults;
                    if (node.type === 'unknown') {
                        defaults = {};
                        Object.keys(node._orig).forEach(function(k) {
                            if (k !== 'type') {
                                defaults[k] = {};
                            }
                        })
                    } else if (node._def) {
                        defaults = node._def.defaults;
                        propRow = $('<tr class="red-ui-help-info-property-row'+(expandedSections.property?"":" hide")+'"><td>'+RED._("sidebar.info.module")+"</td><td></td></tr>").appendTo(tableBody);
                        $(propRow.children()[1]).text(RED.nodes.getType(node.type).set.module);
                        count++;
                    }

                    if (defaults) {
                        for (var n in defaults) {
                            if (n != "name" && n != "info" && defaults.hasOwnProperty(n)) {
                                var val = node[n];
                                var type = typeof val;
                                count++;
                                propRow = $('<tr class="red-ui-help-info-property-row'+(expandedSections.property?"":" hide")+'"><td></td><td></td></tr>').appendTo(tableBody);
                                $(propRow.children()[0]).text(n);
                                if (defaults[n].type && !defaults[n]._type.array) {
                                    var configNode = RED.nodes.node(val);
                                    if (!configNode) {
                                        RED.utils.createObjectElement(undefined).appendTo(propRow.children()[1]);
                                    } else {
                                        var configLabel = RED.utils.getNodeLabel(configNode,val);
                                        var container = propRow.children()[1];

                                        var div = $('<span>',{class:""}).appendTo(container);
                                        var nodeDiv = $('<div>',{class:"red-ui-palette-node red-ui-palette-node-small"}).appendTo(div);
                                        var colour = RED.utils.getNodeColor(configNode.type,configNode._def);
                                        var icon_url = RED.utils.getNodeIcon(configNode._def);
                                        nodeDiv.css({'backgroundColor':colour, "cursor":"pointer"});
                                        var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
                                        $('<div/>',{class:"red-ui-palette-icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer);
                                        var nodeContainer = $('<span></span>').css({"verticalAlign":"top","marginLeft":"6px"}).text(configLabel).appendTo(container);

                                        nodeDiv.on('dblclick',function() {
                                            RED.editor.editConfig("", configNode.type, configNode.id);
                                        })

                                    }
                                } else {
                                    RED.utils.createObjectElement(val,{sourceId: node.id}).appendTo(propRow.children()[1]);
                                }
                            }
                        }
                    }
                    if (count > 0) {
                        $('<a href="#" class="node-info-property-header'+(expandedSections.property?" expanded":"")+'"><span class="red-ui-help-property-more">'+RED._("sidebar.info.showMore")+'</span><span class="red-ui-help-property-less">'+RED._("sidebar.info.showLess")+'</span> <i class="fa fa-caret-down"></i></a>').appendTo(blankRow.children()[0]);
                    }
                }
                if (node.type !== 'tab') {
                    if (subflowRegex) {
                        $('<tr class="blank"><th colspan="2">'+RED._("sidebar.info.subflow")+'</th></tr>').appendTo(tableBody);
                        $('<tr class="node-info-subflow-row"><td>'+RED._("common.label.name")+'</td><td><span class="red-ui-text-bidi-aware" dir=\"'+RED.text.bidi.resolveBaseTextDir(subflowNode.name)+'">'+RED.utils.sanitize(subflowNode.name)+'</span></td></tr>').appendTo(tableBody);
                    }
                }
            }
            if (subflowRegex) {
                propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("subflow.category")+'</td><td></td></tr>').appendTo(tableBody);
                var category = subflowNode.category||"subflows";
                $(propRow.children()[1]).text(RED._("palette.label."+category,{defaultValue:category}))
                $('<tr class="node-info-subflow-row"><td>'+RED._("sidebar.info.instances")+"</td><td>"+subflowUserCount+'</td></tr>').appendTo(tableBody);
                if (subflowNode.meta) {
                    propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("subflow.module")+'</td><td></td></tr>').appendTo(tableBody);
                    $(propRow.children()[1]).text(subflowNode.meta.module||"")
                    propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("subflow.version")+'</td><td></td></tr>').appendTo(tableBody);
                    $(propRow.children()[1]).text(subflowNode.meta.version||"")
                }
            }

            var infoText = "";

            if (node._def && node._def.info) {
                var info = node._def.info;
                var textInfo = (typeof info === "function" ? info.call(node) : info);
                infoText = infoText + RED.utils.renderMarkdown(textInfo);
            }
            if (node.info) {
                infoText = infoText + RED.utils.renderMarkdown(node.info || "")
            }
            var infoSectionContainer = $("<div>").css("padding","0 6px 6px").appendTo(propertiesPanelContent)

            setInfoText(infoText, infoSectionContainer);

            $(".red-ui-sidebar-info-stack").scrollTop(0);
            $(".node-info-property-header").on("click", function(e) {
                e.preventDefault();
                expandedSections["property"] = !expandedSections["property"];
                $(this).toggleClass("expanded",expandedSections["property"]);
                $(".red-ui-help-info-property-row").toggle(expandedSections["property"]);
            });
        }


        // $('<tr class="blank"><th colspan="2"></th></tr>').appendTo(tableBody);
        // propRow = $('<tr class="red-ui-help-info-row"><td>Actions</td><td></td></tr>').appendTo(tableBody);
        // var actionBar = $(propRow.children()[1]);
        //
        // // var actionBar = $('<div>',{style:"background: #fefefe; padding: 3px;"}).appendTo(propertiesPanel);
        // $('<button type="button" class="red-ui-button"><i class="fa fa-code"></i></button>').appendTo(actionBar);
        // $('<button type="button" class="red-ui-button"><i class="fa fa-code"></i></button>').appendTo(actionBar);
        // $('<button type="button" class="red-ui-button"><i class="fa fa-code"></i></button>').appendTo(actionBar);
        // $('<button type="button" class="red-ui-button"><i class="fa fa-code"></i></button>').appendTo(actionBar);



    }

    function setInfoText(infoText,target) {
        var info = addTargetToExternalLinks($('<div class="red-ui-help"><span class="red-ui-text-bidi-aware" dir=\"'+RED.text.bidi.resolveBaseTextDir(infoText)+'">'+infoText+'</span></div>')).appendTo(target);
        info.find(".red-ui-text-bidi-aware").contents().filter(function() { return this.nodeType === 3 && this.textContent.trim() !== "" }).wrap( "<span></span>" );
        var foldingHeader = "H3";
        info.find(foldingHeader).wrapInner('<a class="red-ui-help-info-header expanded" href="#"></a>')
            .find("a").prepend('<i class="fa fa-angle-right">').on("click", function(e) {
                e.preventDefault();
                var isExpanded = $(this).hasClass('expanded');
                var el = $(this).parent().next();
                while(el.length === 1 && el[0].nodeName !== foldingHeader) {
                    el.toggle(!isExpanded);
                    el = el.next();
                }
                $(this).toggleClass('expanded',!isExpanded);
            });
        RED.editor.mermaid.render()
    }

    var tips = (function() {
        var enabled = true;
        var startDelay = 1000;
        var cycleDelay = 15000;
        var startTimeout;
        var refreshTimeout;
        var tipCount = -1;

        RED.actions.add("core:toggle-show-tips",function(state) {
            if (state === undefined) {
                RED.userSettings.toggle("view-show-tips");
            } else {
                enabled = state;
                if (enabled) {
                    startTips();
                } else {
                    stopTips();
                }
            }
        });

        function setTip() {
            var r = Math.floor(Math.random() * tipCount);
            var tip = RED._("infotips:info.tip"+r);

            var m;
            while ((m=/({{(.*?)}})/.exec(tip))) {
                var shortcut = RED.keyboard.getShortcut(m[2]);
                if (shortcut) {
                    tip = tip.replace(m[1],RED.keyboard.formatKey(shortcut.key));
                } else {
                    return;
                }
            }
            while ((m=/(\[([a-z]*?)\])/.exec(tip))) {
                tip = tip.replace(m[1],RED.keyboard.formatKey(m[2]));
            }
            tipBox.html(tip).fadeIn(200);
            if (startTimeout) {
                startTimeout = null;
                refreshTimeout = setInterval(cycleTips,cycleDelay);
            }
        }
        function cycleTips() {
            tipBox.fadeOut(300,function() {
                setTip();
            })
        }
        function startTips() {
            $(".red-ui-sidebar-info").addClass('show-tips');
            resizeStack();
            if (enabled) {
                if (!startTimeout && !refreshTimeout) {
                    if (tipCount === -1) {
                        do {
                            tipCount++;
                        } while(RED._("infotips:info.tip"+tipCount)!=="info.tip"+tipCount);
                    }
                    startTimeout = setTimeout(setTip,startDelay);
                }
            }
        }
        function stopTips() {
            $(".red-ui-sidebar-info").removeClass('show-tips');
            resizeStack();
            clearInterval(refreshTimeout);
            clearTimeout(startTimeout);
            refreshTimeout = null;
            startTimeout = null;
        }
        function nextTip() {
            clearInterval(refreshTimeout);
            startTimeout = true;
            setTip();
        }
        return {
            start: startTips,
            stop: stopTips,
            next: nextTip,
            enabled: function() { return enabled; }
        }
    })();

    function clear() {
        // sections.hide();
        refresh(null);
    }

    function set(html,title) {
        console.warn("Deprecated use of RED.sidebar.info.set - use RED.sidebar.help.set instead")
        RED.sidebar.help.set(html,title);
    }

    function refreshSelection(selection) {
        if (selection === undefined) {
            selection = RED.view.selection();
        }
        if (selection.nodes) {
            if (selection.nodes.length == 1) {
                var node = selection.nodes[0];
                if (node.type === "subflow" && node.direction) {
                    refresh(RED.nodes.subflow(node.z));
                } else {
                    refresh(node);
                }
            } else {
                refresh(selection.nodes);
            }
        } else if (selection.flows || selection.subflows) {
            refresh(selection.flows);
        } else {
            var activeWS = RED.workspaces.active();

            var flow = RED.nodes.workspace(activeWS) || RED.nodes.subflow(activeWS);
            if (flow) {
                refresh(flow);
            } else {
                var workspace = RED.nodes.workspace(RED.workspaces.active());
                if (workspace && workspace.info) {
                    refresh(workspace);
                } else {
                    refresh(null)
                    // clear();
                }
            }
        }
    }


    RED.events.on("view:selection-changed",refreshSelection);

    return {
        init: init,
        show: show,
        refresh: refresh,
        clear: clear,
        set: set
    }
})();
;RED.sidebar.info.outliner = (function() {

    var treeList;
    var searchInput;
    var activeSearch;
    var projectInfo;
    var projectInfoLabel;
    var flowList;
    var subflowList;
    var globalConfigNodes;

    var objects = {};
    var missingParents = {};
    var configNodeTypes;


    function getFlowData() {
        var flowData = [
            {
                label: RED._("menu.label.flows"),
                expanded: true,
                children: []
            },
            {
                id: "__subflow__",
                label: RED._("menu.label.subflows"),
                children: [
                    getEmptyItem("__subflow__")
                ]
            },
            {
                id: "__global__",
                flow: "__global__",
                label: RED._("sidebar.info.globalConfig"),
                types: {},
                children: [
                    getEmptyItem("__global__")
                ]
            }
        ]

        flowList = flowData[0];
        subflowList = flowData[1];
        globalConfigNodes = flowData[2];
        configNodeTypes = { __global__: globalConfigNodes};

        return flowData;
    }

    function getProjectLabel(p) {
        var div = $('<div>',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"});
        div.css("width", "calc(100% - 40px)");
        var contentDiv = $('<div>',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div);
        contentDiv.text(p.name);
        var controls = $('<div>',{class:"red-ui-info-outline-item-controls"}).appendTo(div);
        var editProjectButton = $('<button class="red-ui-button red-ui-button-small" style="position:absolute;right:5px;top: 3px;"><i class="fa fa-ellipsis-h"></i></button>')
            .appendTo(controls)
            .on("click", function(evt) {
                evt.preventDefault();
                RED.projects.editProject();
            });
        RED.popover.tooltip(editProjectButton,RED._('sidebar.project.showProjectSettings'));
        return div;
    }

    var empties = {};
    function getEmptyItem(id) {
        var item = {
            empty: true,
            element: $('<div class="red-ui-info-outline-item red-ui-info-outline-item-empty">').text(RED._("sidebar.info.empty")),
        }
        empties[id] = item;
        return item;
    }

    function getNodeLabel(n) {
        var div = $('<div>',{class:"red-ui-node-list-item red-ui-info-outline-item"});
        RED.utils.createNodeIcon(n, true).appendTo(div);
        div.find(".red-ui-node-label").addClass("red-ui-info-outline-item-label")
        addControls(n, div);
        return div;
    }

    function getFlowLabel(n) {
        var div = $('<div>',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"});
        var contentDiv = $('<div>',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div);
        var label = (typeof n === "string")? n : n.label;
        var newlineIndex = label.indexOf("\\n");
        if (newlineIndex > -1) {
            label = label.substring(0,newlineIndex)+"...";
        }
        contentDiv.text(label);
        addControls(n, div);
        return div;
    }

    function addControls(n,div) {
        var controls = $('<div>',{class:"red-ui-info-outline-item-controls red-ui-info-outline-item-hover-controls"}).appendTo(div);

        if (n.type === "subflow") {
            var subflowInstanceBadge = $('<button type="button" class="red-ui-info-outline-item-control-users red-ui-button red-ui-button-small"><i class="fa fa-toggle-right"></i></button>').text(n.instances.length).appendTo(controls).on("click",function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                RED.search.show("type:subflow:"+n.id);
            })
            RED.popover.tooltip(subflowInstanceBadge,function() { return RED._('subflow.subflowInstances',{count:n.instances.length})});
        }
        if (n._def.category === "config" && n.type !== "group") {
            var userCountBadge = $('<button type="button" class="red-ui-info-outline-item-control-users red-ui-button red-ui-button-small"><i class="fa fa-toggle-right"></i></button>').text(n.users.length).appendTo(controls).on("click",function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                RED.search.show("uses:"+n.id);
            })
            RED.popover.tooltip(userCountBadge,function() { return RED._('editor.nodesUse',{count:n.users.length})});
        }

        if (n._def.button) {
            var triggerButton = $('<button type="button" class="red-ui-info-outline-item-control-action red-ui-button red-ui-button-small"><i class="fa fa-toggle-right"></i></button>').appendTo(controls).on("click",function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                RED.view.clickNodeButton(n);
            })
            RED.popover.tooltip(triggerButton,RED._("sidebar.info.triggerAction"));
        }

        if (n.type === "tab") {
            var toggleVisibleButton = $('<button type="button" class="red-ui-info-outline-item-control-hide red-ui-button red-ui-button-small"><i class="fa fa-eye"></i><i class="fa fa-eye-slash"></i></button>').appendTo(controls).on("click",function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                var isHidden = !div.hasClass("red-ui-info-outline-item-hidden");
                div.toggleClass("red-ui-info-outline-item-hidden",isHidden);
                if (isHidden) {
                    RED.workspaces.hide(n.id);
                } else {
                    RED.workspaces.show(n.id, null, true);
                }
            });
            RED.popover.tooltip(toggleVisibleButton, function () {
                var isHidden = !div.hasClass("red-ui-info-outline-item-hidden");
                return RED._("sidebar.info." + (isHidden ? "hideFlow" : "showFlow"));
            });
        }
        if (n.type !== 'subflow') {
            var toggleButton = $('<button type="button" class="red-ui-info-outline-item-control-disable red-ui-button red-ui-button-small"><i class="fa fa-circle-thin"></i><i class="fa fa-ban"></i></button>').appendTo(controls).on("click",function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                if (n.type === 'tab') {
                    if (n.disabled) {
                        RED.workspaces.enable(n.id)
                    } else {
                        RED.workspaces.disable(n.id)
                    }
                } else if (n.type === 'group') {
                    var groupNodes = RED.group.getNodes(n,true);
                    var groupHistoryEvent = {
                        t:'multi',
                        events:[],
                        dirty: RED.nodes.dirty()
                    }
                    var targetState;
                    groupNodes.forEach(function(n) {
                        if (n.type !== 'group') {
                            if (targetState === undefined) {
                                targetState = !n.d;
                            }
                            var state = !!n.d;
                            if (state !== targetState) {
                                var historyEvent = {
                                    t: "edit",
                                    node: n,
                                    changed: n.changed,
                                    changes: {
                                        d: n.d
                                    }
                                }
                                if (n.d) {
                                    delete n.d;
                                } else {
                                    n.d = true;
                                }
                                n.dirty = true;
                                n.dirtyStatus = true;
                                n.changed = true;
                                RED.events.emit("nodes:change",n);
                                groupHistoryEvent.events.push(historyEvent);
                            }
                        }
                        if (groupHistoryEvent.events.length > 0) {
                            RED.history.push(groupHistoryEvent);
                            RED.nodes.dirty(true)
                            RED.view.redraw();
                        }
                    })
                } else {
                    // TODO: this ought to be a utility function in RED.nodes
                    var historyEvent = {
                        t: "edit",
                        node: n,
                        changed: n.changed,
                        changes: {
                            d: n.d
                        },
                        dirty:RED.nodes.dirty()
                    }
                    if (n.d) {
                        delete n.d;
                    } else {
                        n.d = true;
                    }
                    n.dirty = true;
                    n.dirtyStatus = true;
                    n.changed = true;
                    RED.events.emit("nodes:change",n);
                    RED.history.push(historyEvent);
                    RED.nodes.dirty(true)
                    RED.view.redraw();
                }
            });
            RED.popover.tooltip(toggleButton,function() {
                if (n.type === "group") {
                    return RED._("common.label.enable")+" / "+RED._("common.label.disable")
                }
                return RED._("common.label."+(((n.type==='tab' && n.disabled) || (n.type!=='tab' && n.d))?"enable":"disable"));
            });
        } else {
            $('<div class="red-ui-info-outline-item-control-spacer">').appendTo(controls)
        }
        if (n.type === 'tab') {
            var lockToggleButton = $('<button type="button" class="red-ui-info-outline-item-control-lock red-ui-button red-ui-button-small"><i class="fa fa-unlock-alt"></i><i class="fa fa-lock"></i></button>').appendTo(controls).on("click",function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                if (n.locked) {
                    RED.workspaces.unlock(n.id)
                } else {
                    RED.workspaces.lock(n.id)
                }
            })
            RED.popover.tooltip(lockToggleButton,function() {
                return RED._("common.label."+(n.locked?"unlock":"lock"));
            });
        } else {
            $('<div class="red-ui-info-outline-item-control-spacer">').appendTo(controls)
        }
        controls.find("button").on("dblclick", function(evt) {
            evt.preventDefault();
            evt.stopPropagation();
        })
    }

    function onProjectLoad(activeProject) {
        objects = {};
        var newFlowData = getFlowData();
        projectInfoLabel.empty();
        getProjectLabel(activeProject).appendTo(projectInfoLabel);
        projectInfo.show();
        treeList.treeList('data',newFlowData);
    }

    function build() {
        var container = $("<div>", {class:"red-ui-info-outline"}).css({'height': '100%'});
        var toolbar = $("<div>", {class:"red-ui-sidebar-header red-ui-info-toolbar"}).appendTo(container);

        searchInput = $('<input type="text" data-i18n="[placeholder]menu.label.search">').appendTo(toolbar).searchBox({
            style: "compact",
            delay: 500,
            change: function() {
                var val = $(this).val();
                var searchResults = RED.search.search(val);
                if (val) {
                    activeSearch = val;
                    var resultMap = {};
                    for (var i=0,l=searchResults.length;i<l;i++) {
                        resultMap[searchResults[i].node.id] = true;
                    }
                    var c = treeList.treeList('filter',function(item) {
                        if (item.depth === 0) {
                            return true;
                        }
                        return item.id &&  objects[item.id] && resultMap[item.id]
                    },true)
                } else {
                    activeSearch = null;
                    treeList.treeList('filter',null);
                    var selected = treeList.treeList('selected');
                    if (selected.id) {
                        treeList.treeList('show',selected.id);
                    }

                }
            },
            options: RED.search.getSearchOptions()
        });

        projectInfo = $('<div class="red-ui-treeList-label red-ui-info-outline-project"><span class="red-ui-treeList-icon"><i class="fa fa-archive"></i></span></div>').hide().appendTo(container)
        projectInfoLabel = $('<span>').appendTo(projectInfo);

        // <div class="red-ui-info-outline-item red-ui-info-outline-item-flow" style=";"><div class="red-ui-search-result-description red-ui-info-outline-item-label">Space Monkey</div><div class="red-ui-info-outline-item-controls"><button class="red-ui-button red-ui-button-small" style="position:absolute;right:5px;"><i class="fa fa-ellipsis-h"></i></button></div></div></div>').appendTo(container)

        treeList = $("<div>").css({width: "100%"}).appendTo(container).treeList({
            data:getFlowData()
        })
        treeList.on('treelistselect', function(e,item) {
            var node = RED.nodes.node(item.id) || RED.nodes.group(item.id) || RED.nodes.workspace(item.id) || RED.nodes.subflow(item.id);
            if (node) {
                RED.sidebar.info.refresh(node);
                // if (node.type === 'group' || node._def.category !== "config") {
                //     // RED.view.select({nodes:[node]})
                // } else if (node._def.category === "config") {
                //     RED.sidebar.info.refresh(node);
                // } else {
                //     // RED.view.select({nodes:[]})
                // }
            } else {
                RED.sidebar.info.refresh(null);
            }
        })
        treeList.on('treelistconfirm', function(e,item) {
            var node = RED.nodes.node(item.id);
            if (node) {
                if (node._def.category === "config") {
                    RED.editor.editConfig("", node.type, node.id);
                } else {
                    RED.editor.edit(node);
                }
            }
        })

        RED.events.on("projects:load", onProjectLoad)

        RED.events.on("flows:add", onFlowAdd)
        RED.events.on("flows:remove", onObjectRemove)
        RED.events.on("flows:change", onFlowChange)
        RED.events.on("flows:reorder", onFlowsReorder)

        RED.events.on("subflows:add", onSubflowAdd)
        RED.events.on("subflows:remove", onObjectRemove)
        RED.events.on("subflows:change", onSubflowChange)

        RED.events.on("nodes:add",onNodeAdd);
        RED.events.on("nodes:remove",onObjectRemove);
        RED.events.on("nodes:change",onNodeChange);
        // RED.events.on("nodes:reorder",onNodesReorder);

        RED.events.on("groups:add",onNodeAdd);
        RED.events.on("groups:remove",onObjectRemove);
        RED.events.on("groups:change",onNodeChange);

        RED.events.on("workspace:show", onWorkspaceShow);
        RED.events.on("workspace:hide", onWorkspaceHide);
        RED.events.on("workspace:clear", onWorkspaceClear);

        return container;
    }
    function onWorkspaceClear() {
        treeList.treeList('data',getFlowData());
    }
    function onWorkspaceShow(event) {
        var existingObject = objects[event.workspace];
        if (existingObject) {
            existingObject.element.removeClass("red-ui-info-outline-item-hidden")
        }
    }
    function onWorkspaceHide(event) {
        var existingObject = objects[event.workspace];
        if (existingObject) {
            existingObject.element.addClass("red-ui-info-outline-item-hidden")
        }
    }
    function onFlowAdd(ws) {
        objects[ws.id] = {
            id: ws.id,
            element: getFlowLabel(ws),
            children:[],
            deferBuild: true,
            icon: "red-ui-icons red-ui-icons-flow",
            gutter: getGutter(ws)
        }
        if (missingParents[ws.id]) {
            objects[ws.id].children = missingParents[ws.id];
            delete missingParents[ws.id]
        } else {
            objects[ws.id].children.push(getEmptyItem(ws.id));
        }
        flowList.treeList.addChild(objects[ws.id])
        objects[ws.id].element.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled)
        objects[ws.id].treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled)
        objects[ws.id].element.toggleClass("red-ui-info-outline-item-locked", !!ws.locked)
        objects[ws.id].treeList.container.toggleClass("red-ui-info-outline-item-locked", !!ws.locked)
        updateSearch();

    }
    function onFlowChange(n) {
        var existingObject = objects[n.id];

        var label = n.label || n.id;
        var newlineIndex = label.indexOf("\\n");
        if (newlineIndex > -1) {
            label = label.substring(0,newlineIndex)+"...";
        }
        existingObject.element.find(".red-ui-info-outline-item-label").text(label);
        existingObject.element.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled)
        existingObject.treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled)
        existingObject.element.toggleClass("red-ui-info-outline-item-locked", !!n.locked)
        existingObject.treeList.container.toggleClass("red-ui-info-outline-item-locked", !!n.locked)
        updateSearch();
    }
    function onFlowsReorder(order) {
        var indexMap = {};
        order.forEach(function(id,index) {
            indexMap[id] = index;
        })

        flowList.treeList.sortChildren(function(A,B) {
            if (A.id === "__global__") { return -1 }
            if (B.id === "__global__") { return 1 }
            return indexMap[A.id] - indexMap[B.id]
        })
    }
    // function onNodesReorder(event) {
    //     //
    //     var nodes = RED.nodes.getNodeOrder(event.z);
    //     var indexMap = {};
    //     nodes.forEach(function(id,index) {
    //         indexMap[id] = index;
    //     })
    //     var existingObject = objects[event.z];
    //     existingObject.treeList.sortChildren(function(A,B) {
    //         if (A.children && !B.children) { return -1 }
    //         if (!A.children && B.children) { return 1 }
    //         if (A.children && B.children) { return -1 }
    //         return indexMap[A.id] - indexMap[B.id]
    //     })
    // }
    function onSubflowAdd(sf) {
        objects[sf.id] = {
            id: sf.id,
            element: getNodeLabel(sf),
            children:[],
            deferBuild: true,
            gutter: getGutter(sf)
        }
        if (missingParents[sf.id]) {
            objects[sf.id].children = missingParents[sf.id];
            delete missingParents[sf.id]
        } else {
            objects[sf.id].children.push(getEmptyItem(sf.id));
        }
        if (empties["__subflow__"]) {
            empties["__subflow__"].treeList.remove();
            delete empties["__subflow__"];
        }
        subflowList.treeList.addChild(objects[sf.id])
        updateSearch();
    }
    function onSubflowChange(sf) {
        var existingObject = objects[sf.id];
        existingObject.treeList.replaceElement(getNodeLabel(sf));
        // existingObject.element.find(".red-ui-info-outline-item-label").text(n.name || n.id);
        RED.nodes.eachNode(function(n) {
            if (n.type == "subflow:"+sf.id) {
                var sfInstance = objects[n.id];
                sfInstance.treeList.replaceElement(getNodeLabel(n));
            }
        });
        updateSearch();
    }

    function onNodeChange(n) {
        var existingObject = objects[n.id];
        var parent = n.g||n.z||"__global__";

        var nodeLabelText = RED.utils.getNodeLabel(n,n.name || (n.type+": "+n.id));
        if (nodeLabelText) {
            existingObject.element.find(".red-ui-info-outline-item-label").text(nodeLabelText);
        } else {
            existingObject.element.find(".red-ui-info-outline-item-label").html("&nbsp;");
        }
        var existingParent = existingObject.parent.id;
        if (!existingParent) {
            existingParent = existingObject.parent.parent.flow
        }
        if (parent !== existingParent) {
            var parentItem = existingObject.parent;
            existingObject.treeList.remove(true);
            if (parentItem.children.length === 0) {
                if (parentItem.config) {
                    // this is a config
                    parentItem.treeList.remove();
                    // console.log("Removing",n.type,"from",parentItem.parent.id||parentItem.parent.parent.id)

                    delete configNodeTypes[parentItem.parent.id||parentItem.parent.parent.id].types[n.type];


                    if (parentItem.parent.children.length === 0) {
                        if (parentItem.parent.id === "__global__") {
                            parentItem.parent.treeList.addChild(getEmptyItem(parentItem.parent.id));
                        } else {
                            delete configNodeTypes[parentItem.parent.parent.id];
                            parentItem.parent.treeList.remove();
                            if (parentItem.parent.parent.children.length === 0) {
                                parentItem.parent.parent.treeList.addChild(getEmptyItem(parentItem.parent.parent.id));
                            }

                        }
                    }
                } else {
                    parentItem.treeList.addChild(getEmptyItem(parentItem.id));
                }
            }
            if (n._def.category === 'config' && n.type !== 'group') {
                // This must be a config node that has been rescoped
                createFlowConfigNode(parent,n.type);
                configNodeTypes[parent].types[n.type].treeList.addChild(objects[n.id]);
            } else {
                // This is a node that has moved groups
                if (empties[parent]) {
                    empties[parent].treeList.remove();
                    delete empties[parent];
                }
                objects[parent].treeList.addChild(existingObject)
            }

            // if (parent === "__global__") {
            //     // Global always exists here
            //     if (!configNodeTypes[parent][n.type]) {
            //         configNodeTypes[parent][n.type] = {
            //             config: true,
            //             label: n.type,
            //             children: []
            //         }
            //         globalConfigNodes.treeList.addChild(configNodeTypes[parent][n.type])
            //     }
            //     configNodeTypes[parent][n.type].treeList.addChild(existingObject);
            // } else {
            //     if (empties[parent]) {
            //         empties[parent].treeList.remove();
            //         delete empties[parent];
            //     }
            //     objects[parent].treeList.addChild(existingObject)
            // }
        }
        existingObject.element.toggleClass("red-ui-info-outline-item-disabled", !!n.d)

        if (n._def.category === "config" && n.type !== 'group') {
            existingObject.element.find(".red-ui-info-outline-item-control-users").text(n.users.length);
        }

        updateSearch();
    }
    function onObjectRemove(n) {
        var existingObject = objects[n.id];
        existingObject.treeList.remove();
        delete objects[n.id]

        if (/^subflow:/.test(n.type)) {
            var sfType = n.type.substring(8);
            if (objects[sfType]) {
                objects[sfType].element.find(".red-ui-info-outline-item-control-users").text(RED.nodes.subflow(sfType).instances.length);
            }
        }

        // If this is a group being removed, it may have an empty item
        if (empties[n.id]) {
            delete empties[n.id];
        }
        var parent = existingObject.parent;
        if (parent.children.length === 0) {
            if (parent.config) {
                // this is a config
                parent.treeList.remove();
                delete configNodeTypes[parent.parent.id||n.z].types[n.type];
                if (parent.parent.children.length === 0) {
                    if (parent.parent.id === "__global__") {
                        parent.parent.treeList.addChild(getEmptyItem(parent.parent.id));
                    } else {
                        delete configNodeTypes[n.z];
                        parent.parent.treeList.remove();
                        if (parent.parent.parent.children.length === 0) {
                            parent.parent.parent.treeList.addChild(getEmptyItem(parent.parent.parent.id));
                        }
                    }
                }
            } else {
                parent.treeList.addChild(getEmptyItem(parent.id));
            }
        }
    }
    function getGutter(n) {
        var span = $("<span>",{class:"red-ui-info-outline-gutter red-ui-treeList-gutter-float"});
        var revealButton = $('<button type="button" class="red-ui-info-outline-item-control-reveal red-ui-button red-ui-button-small"><i class="fa fa-search"></i></button>').appendTo(span).on("click",function(evt) {
            evt.preventDefault();
            evt.stopPropagation();
            RED.view.reveal(n.id);
        })
        RED.popover.tooltip(revealButton,RED._("sidebar.info.find"));
        return span;
    }

    function createFlowConfigNode(parent,type) {
        // console.log("createFlowConfig",parent,type,configNodeTypes[parent]);
        if (empties[parent]) {
            empties[parent].treeList.remove();
            delete empties[parent];
        }
        if (!configNodeTypes[parent]) {
            // There is no 'config nodes' item in the parent flow
            configNodeTypes[parent] = {
                config: true,
                flow: parent,
                types: {},
                label: RED._("menu.label.displayConfig"),
                children: []
            }
            objects[parent].treeList.insertChildAt(configNodeTypes[parent],0);
            // console.log("CREATED", parent)
        }
        if (!configNodeTypes[parent].types[type]) {
            configNodeTypes[parent].types[type] = {
                config: true,
                label: type,
                children: []
            }
            configNodeTypes[parent].treeList.addChild(configNodeTypes[parent].types[type]);
            // console.log("CREATED", parent,type)
        }
    }
    function onNodeAdd(n) {
        objects[n.id] = {
            id: n.id,
            element: getNodeLabel(n),
            gutter: getGutter(n)
        }
        if (n.type === "group") {
            objects[n.id].children = [];
            objects[n.id].deferBuild = true;
            if (missingParents[n.id]) {
                objects[n.id].children = missingParents[n.id];
                delete missingParents[n.id]
            }
            if (objects[n.id].children.length === 0) {
                objects[n.id].children.push(getEmptyItem(n.id));
            }
        }
        var parent = n.g||n.z||"__global__";

        if (n._def.category !== "config" || n.type === 'group') {
            if (objects[parent]) {
                if (empties[parent]) {
                    empties[parent].treeList.remove();
                    delete empties[parent];
                }
                if (objects[parent].treeList) {
                    objects[parent].treeList.addChild(objects[n.id]);
                } else {
                    objects[parent].children.push(objects[n.id])
                }
            } else {
                missingParents[parent] = missingParents[parent]||[];
                missingParents[parent].push(objects[n.id])
            }
        } else {
            createFlowConfigNode(parent,n.type);
            configNodeTypes[parent].types[n.type].treeList.addChild(objects[n.id]);
        }
        objects[n.id].element.toggleClass("red-ui-info-outline-item-disabled", !!n.d)
        if (/^subflow:/.test(n.type)) {
            var sfType = n.type.substring(8);
            if (objects[sfType]) {
                objects[sfType].element.find(".red-ui-info-outline-item-control-users").text(RED.nodes.subflow(sfType).instances.length);
            }
        }
        updateSearch();
    }

    var updateSearchTimer;
    function updateSearch() {
        if (updateSearchTimer) {
            clearTimeout(updateSearchTimer)
        }
        if (activeSearch) {
            updateSearchTimer = setTimeout(function() {
                searchInput.searchBox("change");
            },100);
        }
    }
    function onSelectionChanged(selection) {
        // treeList.treeList('clearSelection');
    }

    return {
        build: build,
        search: function(val) {
            searchInput.searchBox('value',val)
        },
        select: function(node) {
            if (node) {
                if (Array.isArray(node)) {
                    treeList.treeList('select', node.map(function(n) { return objects[n.id] }), false)
                } else {
                    treeList.treeList('select', objects[node.id], false)

                }
            } else {
                treeList.treeList('clearSelection')
            }
        },
        reveal: function(node) {
            treeList.treeList('show', objects[node.id])
        }
    }
})();
;/**
 * 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.
 **/
RED.sidebar.help = (function() {

    var content;
    var toolbar;
    var helpSection;
    var panels;
    var panelRatio;
    var treeList;
    var tocPanel;

    function resizeStack() {
        var h = $(content).parent().height() - toolbar.outerHeight();
        panels.resize(h)
    }

    function init() {

        content = document.createElement("div");
        content.className = "red-ui-sidebar-info"

        toolbar = $("<div>", {class:"red-ui-sidebar-header red-ui-info-toolbar"}).appendTo(content);
        $('<span class="button-group"><a id="red-ui-sidebar-help-show-toc" class="red-ui-button red-ui-button-small selected" href="#"><i class="fa fa-list-ul"></i></a></span>').appendTo(toolbar)
        var showTOCButton = toolbar.find('#red-ui-sidebar-help-show-toc')
        RED.popover.tooltip(showTOCButton, function () {
            if ($(showTOCButton).hasClass('selected')) {
                return RED._("sidebar.help.hideTopics");
            } else {
                return RED._("sidebar.help.showTopics");
            }
        });
        showTOCButton.on("click",function(e) {
            e.preventDefault();
            if ($(this).hasClass('selected')) {
                hideTOC();
            } else {
                showTOC();
            }
        });

        var stackContainer = $("<div>",{class:"red-ui-sidebar-help-stack"}).appendTo(content);

        tocPanel = $("<div>", {class: "red-ui-sidebar-help-toc"}).appendTo(stackContainer);
        var helpPanel = $("<div>").css({
            "overflow-y": "auto"
        }).appendTo(stackContainer);

        panels = RED.panels.create({
            container: stackContainer
        })
        panels.ratio(0.3);

        helpSearch = $('<input type="text" data-i18n="[placeholder]sidebar.help.search">').appendTo(toolbar).searchBox({
            style: "compact",
            delay: 100,
            change: function() {
                const searchFor = $(this).val().toLowerCase();
                if (searchFor) {
                    showTOC();
                    treeList.treeList('filter',function(item) {
                        if (item.depth === 0) {
                            return true;
                        }
                        let found = item.nodeType && item.nodeType.toLowerCase().indexOf(searchFor) > -1;
                        found = found || item.subflowLabel && item.subflowLabel.toLowerCase().indexOf(searchFor) > -1;
                        found = found || item.palleteLabel && item.palleteLabel.toLowerCase().indexOf(searchFor) > -1;
                        return found;
                    },true)
                } else {
                    treeList.treeList('filter',null);
                    var selected = treeList.treeList('selected');
                    if (selected.id) {
                        treeList.treeList('show',selected.id);
                    }

                }
            }
        })

        helpSection = $("<div>",{class:"red-ui-help"}).css({
            "padding":"6px",
        }).appendTo(helpPanel)

        $('<span class="red-ui-help-info-none">'+RED._("sidebar.help.noHelp")+'</span>').appendTo(helpSection);

        treeList = $("<div>").css({width: "100%"}).appendTo(tocPanel).treeList({data: []})
        var pendingContentLoad;
        treeList.on('treelistselect', function(e,item) {
            pendingContentLoad = item;
            if (item.tour) {
                RED.tourGuide.run(item.tour);
            }
            else if (item.nodeType) {
                showNodeTypeHelp(item.nodeType);
            } else if (item.content) {
                helpSection.empty();
                if (typeof item.content === "string") {
                    setInfoText(item.label, item.content);
                } else if (typeof item.content === "function") {
                    if (item.content.length === 0) {
                        setInfoText(item.label, item.content());
                    } else {
                        setInfoText(item.label, '<div class="red-ui-component-spinner red-ui-component-spinner-contain"><img src="red/images/spin.svg" /></div>',helpSection)
                        item.content(function(content) {
                            if (pendingContentLoad === item) {
                                helpSection.empty();
                                setInfoText(item.label, content);
                            }
                        })
                    }
                }
            }
        })

        RED.sidebar.addTab({
            id: "help",
            label: RED._("sidebar.help.label"),
            name: RED._("sidebar.help.name"),
            iconClass: "fa fa-book",
            action:"core:show-help-tab",
            content: content,
            pinned: true,
            enableOnEdit: true,
            onchange: function() {
                resizeStack()
            }
        });

        $(window).on("resize", resizeStack);
        $(window).on("focus", resizeStack);

        RED.events.on('registry:node-type-added', queueRefresh);
        RED.events.on('registry:node-type-removed', queueRefresh);
        RED.events.on('subflows:change', refreshSubflow);

        RED.actions.add("core:show-help-tab", show);
        RED.actions.add("core:show-node-help", showNodeHelp)

    }

    var refreshTimer;
    function queueRefresh() {
        if (!refreshTimer) {
            refreshTimer = setTimeout(function() {
                refreshTimer = null;
                refreshHelpIndex();
            },500);
        }
    }

    function refreshSubflow(sf) {
        var item = treeList.treeList('get',"node-type:subflow:"+sf.id);
        if (item) {
            item.subflowLabel = sf._def.label().toLowerCase();
            item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()}));
        }
    }

    function hideTOC() {
        var tocButton = $('#red-ui-sidebar-help-show-toc')
        if (tocButton.hasClass('selected')) {
            tocButton.removeClass('selected');
            panelRatio = panels.ratio();
            tocPanel.css({"transition":"height 0.2s"})
            panels.ratio(0)
            setTimeout(function() {
                tocPanel.css({"transition":""})
            },250);
        }
    }
    function showTOC() {
        var tocButton = $('#red-ui-sidebar-help-show-toc')
        if (!tocButton.hasClass('selected')) {
            tocButton.addClass('selected');
            tocPanel.css({"transition":"height 0.2s"})
            panels.ratio(Math.max(0.3,Math.min(panelRatio,0.7)));
            setTimeout(function() {
                tocPanel.css({"transition":""})
                var selected = treeList.treeList('selected');
                if (selected.id) {
                    treeList.treeList('show',selected);
                }
            },250);
        }
    }

    function refreshHelpIndex() {
        var modules = RED.nodes.registry.getModuleList();
        var moduleNames = Object.keys(modules);
        moduleNames.sort();

        var nodeHelp = {
            label: RED._("sidebar.help.nodeHelp"),
            children: [],
            expanded: true
        };
        var tours = RED.tourGuide.list().map(function (item) {
            return {
                icon: "fa fa-play-circle-o",
                label: item.label,
                tour: item.path,
            };
        });
        var helpData = [
            {
                label: "Node-RED",
                children: [
                    {
                        id: 'changelog',
                        label: RED._("sidebar.help.changeLog"),
                        content: getChangelog
                    },
                    {
                        label: RED._("tourGuide.welcomeTours"),
                        children: tours
                    }

                ]
            },
            nodeHelp
        ];
        var subflows = RED.nodes.registry.getNodeTypes().filter(function(t) {return /subflow/.test(t)});
        if (subflows.length > 0) {
            nodeHelp.children.push({
                label: RED._("menu.label.subflows"),
                children: []
            })
            subflows.forEach(function(nodeType) {
                var sf = RED.nodes.getType(nodeType);
                nodeHelp.children[0].children.push({
                    id:"node-type:"+nodeType,
                    nodeType: nodeType,
                    subflowLabel: sf.label().toLowerCase(),
                    element: getNodeLabel({_def:sf,type:sf.label()})
                })
            })
        }


        moduleNames.forEach(function(moduleName) {
            const module = modules[moduleName];
            const nodeTypes = [];
            const moduleSets = module.sets;
            const setNames = Object.keys(moduleSets);
            setNames.forEach(function(setName) {
                const moduleSet = moduleSets[setName];
                moduleSet.types.forEach(function(nodeType) {
                    if ($("script[data-help-name='"+nodeType+"']").length) {
                        const n =  {_def:RED.nodes.getType(nodeType),type:nodeType}
                        n.name = getNodePaletteLabel(n);
                        nodeTypes.push({
                            id: "node-type:"+nodeType,
                            nodeType: nodeType,
                            palleteLabel: n.name,
                            element: getNodeLabel(n)
                        })
                    }
                })
            })
            if (nodeTypes.length > 0) {
                nodeTypes.sort(function(A,B) {
                    return A.nodeType.localeCompare(B.nodeType)
                })
                nodeHelp.children.push({
                    id: moduleName,
                    icon: "fa fa-cube",
                    label: moduleName,
                    children: nodeTypes
                })
            }
        });
        treeList.treeList("data",helpData);
    }

    function getNodePaletteLabel(n) {
        let label = n.name;
        if (!label && n._def && n._def.paletteLabel) {
            try {
                label = (typeof n._def.paletteLabel === "function" ? n._def.paletteLabel.call(n._def) : n._def.paletteLabel)||"";
            } catch (err) {
            }
        }
        return label || n.type;
    }

    function getNodeLabel(n) {
        const div = $('<div>',{class:"red-ui-node-list-item"});
        const icon = RED.utils.createNodeIcon(n).appendTo(div);
        $('<div>',{class:"red-ui-node-label"}).text(getNodePaletteLabel(n)).appendTo(icon);
        return div;
    }

    function showNodeTypeHelp(nodeType) {
        helpSection.empty();
        var helpText;
        var title;
        var m = /^subflow(:(.+))?$/.exec(nodeType);
        if (m && m[2]) {
            var subflowNode = RED.nodes.subflow(m[2]);
            helpText = (RED.utils.renderMarkdown(subflowNode.info||"")||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>'));
            title = subflowNode.name || nodeType;
        } else {
            helpText = RED.nodes.getNodeHelp(nodeType)||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
            var _def = RED.nodes.registry.getNodeType(nodeType);
            title = (_def && _def.paletteLabel)?_def.paletteLabel:nodeType;
            if (typeof title === "function") {
                try {
                    title = _def.paletteLabel.call(_def);
                } catch(err) {
                    title = nodeType;
                }
            }
        }
        setInfoText(title, helpText);

        var ratio = panels.ratio();
        if (ratio > 0.7) {
            panels.ratio(0.7)
        }
        treeList.treeList("show","node-type:"+nodeType)
        treeList.treeList("select","node-type:"+nodeType, false);

    }

    function show(type, bringToFront) {
        if (bringToFront !== false) {
            RED.sidebar.show("help");
        }
        if (type) {
            // hideTOC();
            showNodeTypeHelp(type);
        }
        resizeStack();
    }

    function showNodeHelp(node) {
        if (!node) {
            const selection = RED.view.selection()
            if (selection.nodes && selection.nodes.length > 0) {
                node = selection.nodes.find(n => n.type !== 'group' && n.type !== 'junction')
            }
        }
        if (node) {
            show(node.type, true)
        }
    }


    // TODO: DRY - projects.js
    function addTargetToExternalLinks(el) {
        $(el).find("a").each(function(el) {
            var href = $(this).attr('href');
            if (/^https?:/.test(href)) {
                $(this).attr('target','_blank');
            }
        });
        return el;
    }

    function setInfoText(title, infoText) {
        helpSection.empty();
        if (title) {
            $("<h1>",{class:"red-ui-help-title"}).text(title).appendTo(helpSection);
        }
        var info = addTargetToExternalLinks($('<div class="red-ui-help"><span class="red-ui-text-bidi-aware" dir=\"'+RED.text.bidi.resolveBaseTextDir(infoText)+'">'+infoText+'</span></div>')).appendTo(helpSection);
        info.find(".red-ui-text-bidi-aware").contents().filter(function() { return this.nodeType === 3 && this.textContent.trim() !== "" }).wrap( "<span></span>" );
        var foldingHeader = "H3";
        info.find(foldingHeader).wrapInner('<a class="red-ui-help-info-header expanded" href="#"></a>')
            .find("a").prepend('<i class="fa fa-angle-right">').on("click", function(e) {
                e.preventDefault();
                var isExpanded = $(this).hasClass('expanded');
                var el = $(this).parent().next();
                while(el.length === 1 && el[0].nodeName !== foldingHeader) {
                    el.toggle(!isExpanded);
                    el = el.next();
                }
                $(this).toggleClass('expanded',!isExpanded);
            })
        helpSection.parent().scrollTop(0);
        RED.editor.mermaid.render()
    }

    function set(html,title) {
        $(helpSection).empty();
        setInfoText(title,html);
        hideTOC();
        show();
    }

    function refreshSelection(selection) {
        if (selection === undefined) {
            selection = RED.view.selection();
        }
        if (selection.nodes) {
            if (selection.nodes.length == 1) {
                var node = selection.nodes[0];
                if (node.type === "subflow" && node.direction) {
                    // ignore subflow virtual ports
                } else if (node.type !== 'group' && node.type !== 'junction'){
                    showNodeTypeHelp(node.type);
                }
            }
        }
    }
    RED.events.on("view:selection-changed",refreshSelection);

    function getChangelog(done) {
        $.get('red/about', function(data) {
            // data will be strictly markdown. Any HTML should be escaped.
            data = RED.utils.sanitize(data);
            RED.tourGuide.load("./tours/welcome.js", function(err, tour) {
                var tourHeader = '<div><img width="50px" src="red/images/node-red-icon.svg" /></div>';
                if (tour) {
                    var currentVersionParts = RED.settings.version.split(".");
                    var tourVersionParts = tour.version.split(".");
                    if (tourVersionParts[0] === currentVersionParts[0] && tourVersionParts[1] === currentVersionParts[1]) {
                        tourHeader = '<div><button type="button" onclick="RED.actions.invoke(\'core:show-welcome-tour\')" class="red-ui-button">' + RED._("tourGuide.takeATour") + '</button></div>';
                    }
                }
                var aboutHeader = '<div style="text-align:center;">'+tourHeader+'</div>'
                done(aboutHeader+RED.utils.renderMarkdown(data))
            });

        });
    }
    function showAbout() {
        treeList.treeList("show","changelog")
        treeList.treeList("select","changelog");
        show();
    }
    function showWelcomeTour(lastSeenVersion, done) {
        done = done || function() {};
        RED.tourGuide.load("./tours/welcome.js", function(err, tour) {
            if (err) {
                console.warn("Failed to load welcome tour",err);
                done()
                return;
            }
            var currentVersionParts = RED.settings.version.split(".");
            var tourVersionParts = tour.version.split(".");

            // Only display the tour if its MAJ.MIN versions the current version
            // This means if we update MAJ/MIN without updating the tour, the old tour won't get shown
            if (tourVersionParts[0] !== currentVersionParts[0] || tourVersionParts[1] !== currentVersionParts[1]) {
                done()
                return;
            }

            if (lastSeenVersion) {
                // Previously displayed a welcome tour.
                if (lastSeenVersion === RED.settings.version) {
                    // Exact match - don't show the tour
                    done()
                    return;
                }
                var lastSeenParts = lastSeenVersion.split(".");
                if (currentVersionParts[0] < lastSeenParts[0] || (currentVersionParts[0] === lastSeenParts[0] && currentVersionParts[1] < lastSeenParts[1])) {
                    // Running an *older* version than last displayed tour.
                    done()
                    return;
                }
                if (currentVersionParts[0] === lastSeenParts[0] && currentVersionParts[1] === lastSeenParts[1]) {
                    if (lastSeenParts.length === 3 && currentVersionParts.length === 3) {
                        // Matching non-beta MAJ.MIN - don't repeat tour
                        done()
                        return;
                    }
                    if (currentVersionParts.length === 4 && (lastSeenParts.length === 3 || currentVersionParts[3] < lastSeenParts[3])) {
                        // Running an *older* beta than last displayed tour.
                        done()
                        return
                    }
                }
            }
            RED.tourGuide.run("./tours/welcome.js", function(err) {
                RED.settings.set("editor.tours.welcome", RED.settings.version)
                done()
            })
        })

    }
    RED.actions.add("core:show-about", showAbout);
    RED.actions.add("core:show-welcome-tour", showWelcomeTour);

    return {
        init: init,
        show: show,
        set: set
    }
})();
;/**
 * 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.
 **/
RED.sidebar.config = (function() {

    let flashingConfigNode;
    let flashingConfigNodeTimer;

    var content = document.createElement("div");
    content.className = "red-ui-sidebar-node-config";
    content.id = "red-ui-sidebar-node-config";
    content.tabIndex = 0;

    $('<div class="red-ui-sidebar-header"><span class="button-group">'+
      '<a class="red-ui-sidebar-header-button-toggle selected" id="red-ui-sidebar-config-filter-all" href="#"><span data-i18n="sidebar.config.filterAll"></span></a>'+
      '<a class="red-ui-sidebar-header-button-toggle" id="red-ui-sidebar-config-filter-unused" href="#"><span data-i18n="sidebar.config.filterUnused"></span></a> '+
      '</span></div>'
    ).appendTo(content);


    var toolbar = $('<div>'+
        '<a class="red-ui-footer-button" id="red-ui-sidebar-config-collapse-all" href="#"><i class="fa fa-angle-double-up"></i></a> '+
        '<a class="red-ui-footer-button" id="red-ui-sidebar-config-expand-all" href="#"><i class="fa fa-angle-double-down"></i></a>'+
        '</div>');

    var globalCategories = $("<div>").appendTo(content);
    var flowCategories = $("<div>").appendTo(content);
    var subflowCategories = $("<div>").appendTo(content);

    var showUnusedOnly = false;

    var categories = {};

    function getOrCreateCategory(name,parent,label,isLocked) {
        name = name.replace(/\./i,"-");
        if (!categories[name]) {
            var container = $('<div class="red-ui-palette-category red-ui-sidebar-config-category" id="red-ui-sidebar-config-category-'+name+'"></div>').appendTo(parent);
            var header = $('<div class="red-ui-sidebar-config-tray-header red-ui-palette-header"><i class="fa fa-angle-down expanded"></i></div>').appendTo(container);
            let lockIcon
            if (label) {
                lockIcon = $('<span style="margin-right: 5px"><i class="fa fa-lock"/></span>').appendTo(header)
                lockIcon.toggle(!!isLocked)
                $('<span class="red-ui-palette-node-config-label"/>').text(label).appendTo(header);
            } else {
                $('<span class="red-ui-palette-node-config-label" data-i18n="sidebar.config.'+name+'">').appendTo(header);
            }
            $('<span class="red-ui-sidebar-node-config-filter-info"></span>').appendTo(header);
            category = $('<ul class="red-ui-palette-content red-ui-sidebar-node-config-list"></ul>').appendTo(container);
            category.on("click", function(e) {
                $(content).find(".red-ui-palette-node").removeClass("selected");
            });
            container.i18n();
            var icon = header.find("i");
            var result = {
                label: label,
                lockIcon,
                list: category,
                size: function() {
                    return result.list.find("li:not(.red-ui-palette-node-config-none)").length
                },
                open: function(snap) {
                    if (!icon.hasClass("expanded")) {
                        icon.addClass("expanded");
                        if (snap) {
                            result.list.show();
                        } else {
                            result.list.slideDown();
                        }
                    }
                },
                close: function(snap) {
                    if (icon.hasClass("expanded")) {
                        icon.removeClass("expanded");
                        if (snap) {
                            result.list.hide();
                        } else {
                            result.list.slideUp();
                        }
                    }
                },
                isOpen: function() {
                    return icon.hasClass("expanded");
                }
            };

            header.on('click', function(e) {
                if (result.isOpen()) {
                    result.close();
                } else {
                    result.open();
                }
            });
            categories[name] = result;
        } else {
            if (isLocked !== undefined && categories[name].lockIcon) {
                categories[name].lockIcon.toggle(!!isLocked)
            }
            if (categories[name].label !== label) {
                categories[name].list.parent().find('.red-ui-palette-node-config-label').text(label);
                categories[name].label = label;
            }
        }
        return categories[name];
    }

    function createConfigNodeList(id,nodes) {
        var category = getOrCreateCategory(id.replace(/\./i,"-"))
        var list = category.list;

        nodes.sort(function(A,B) {
            if (A.type < B.type) { return -1;}
            if (A.type > B.type) { return 1;}
            return 0;
        });
        if (showUnusedOnly) {
            var hiddenCount = nodes.length;
            nodes = nodes.filter(function(n) {
                return n._def.hasUsers!==false && n.users.length === 0;
            })
            hiddenCount = hiddenCount - nodes.length;
            if (hiddenCount > 0) {
                list.parent().find('.red-ui-sidebar-node-config-filter-info').text(RED._('sidebar.config.filtered',{count:hiddenCount})).show();
            } else {
                list.parent().find('.red-ui-sidebar-node-config-filter-info').hide();
            }
        } else {
            list.parent().find('.red-ui-sidebar-node-config-filter-info').hide();
        }
        list.empty();
        if (nodes.length === 0) {
            $('<li class="red-ui-palette-node-config-none" data-i18n="sidebar.config.none">NONE</li>').i18n().appendTo(list);
            category.close(true);
        } else {
            var currentType = "";
            nodes.forEach(function(node) {
                var labelText = RED.utils.getNodeLabel(node,node.id);
                if (node.type != currentType) {
                    $('<li class="red-ui-palette-node-config-type">'+node.type+'</li>').appendTo(list);
                    currentType = node.type;
                }
                if (node.changed) {
                    labelText += "!!"
                }
                var entry = $('<li class="red-ui-palette-node_id_'+node.id.replace(/\./g,"-")+'"></li>').appendTo(list);
                var nodeDiv = $('<div class="red-ui-palette-node-config red-ui-palette-node"></div>').appendTo(entry);
                entry.data('node',node.id);
                nodeDiv.data('node',node.id);
                var label = $('<div class="red-ui-palette-label"></div>').text(labelText).appendTo(nodeDiv);

                if (node.d) {
                    nodeDiv.addClass("red-ui-palette-node-config-disabled");
                    $('<i class="fa fa-ban"></i>').prependTo(label);
                }

                if (node._def.hasUsers !== false) {
                    var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container red-ui-palette-icon-container-right"}).appendTo(nodeDiv);
                    if (node.users.length === 0) {
                        iconContainer.text(0);
                    } else {
                        $('<a href="#"/>').on("click", function(e) {
                            e.stopPropagation();
                            e.preventDefault();
                            RED.search.show(node.id);
                        }).text(node.users.length).appendTo(iconContainer);
                    }
                    RED.popover.tooltip(iconContainer,RED._('editor.nodesUse',{count:node.users.length}));
                    if (node.users.length === 0) {
                        nodeDiv.addClass("red-ui-palette-node-config-unused");
                    }
                }

                if (!node.valid) {
                    nodeDiv.addClass("red-ui-palette-node-config-invalid")
                    const nodeDivAnnotations = $('<svg class="red-ui-palette-node-annotations red-ui-flow-node-error" width="10" height="10"></svg>').appendTo(nodeDiv)
                    const errorBadge = document.createElementNS("http://www.w3.org/2000/svg","path");
                    errorBadge.setAttribute("d","M 0,9 l 10,0 -5,-8 z");
                    nodeDivAnnotations.append($(errorBadge))
                    RED.popover.tooltip(nodeDivAnnotations, function () {
                        if (node.validationErrors && node.validationErrors.length > 0) {
                            return RED._("editor.errors.invalidProperties")+"<br>  - "+node.validationErrors.join("<br>  - ")
                        }
                    })
                }

                nodeDiv.on('click',function(e) {
                    e.stopPropagation();
                    RED.view.select(false);
                    if (e.metaKey) {
                        $(this).toggleClass("selected");
                    } else {
                        $(content).find(".red-ui-palette-node").removeClass("selected");
                        $(this).addClass("selected");
                    }
                    RED.sidebar.info.refresh(node);
                });
                nodeDiv.on('dblclick',function(e) {
                    e.stopPropagation();
                    RED.editor.editConfig("", node.type, node.id);
                });
                var userArray = node.users.map(function(n) { return n.id });
                nodeDiv.on('mouseover',function(e) {
                    RED.nodes.eachNode(function(node) {
                        if( userArray.indexOf(node.id) != -1) {
                            node.highlighted = true;
                            node.dirty = true;
                        }
                    });
                    RED.view.redraw();
                });
                nodeDiv.on('mouseout',function(e) {
                    RED.nodes.eachNode(function(node) {
                        if(node.highlighted) {
                            node.highlighted = false;
                            node.dirty = true;
                        }
                    });
                    RED.view.redraw();
                });
            });
            category.open(true);
        }
    }

    function refreshConfigNodeList() {
        var validList = {"global":true};

        getOrCreateCategory("global",globalCategories);

        RED.nodes.eachWorkspace(function(ws) {
            validList[ws.id.replace(/\./g,"-")] = true;
            getOrCreateCategory(ws.id,flowCategories,ws.label, ws.locked);
        })
        RED.nodes.eachSubflow(function(sf) {
            validList[sf.id.replace(/\./g,"-")] = true;
            getOrCreateCategory(sf.id,subflowCategories,sf.name);
        })
        $(".red-ui-sidebar-config-category").each(function() {
            var id = $(this).attr('id').substring("red-ui-sidebar-config-category-".length);
            if (!validList[id]) {
                $(this).remove();
                delete categories[id];
            }
        })
        var globalConfigNodes = [];
        var configList = {};
        RED.nodes.eachConfig(function(cn) {
            if (cn.z) {
                configList[cn.z.replace(/\./g,"-")] = configList[cn.z.replace(/\./g,"-")]||[];
                configList[cn.z.replace(/\./g,"-")].push(cn);
            } else if (!cn.z) {
                globalConfigNodes.push(cn);
            }
        });
        for (var id in validList) {
            if (validList.hasOwnProperty(id)) {
                createConfigNodeList(id,configList[id]||[]);
            }
        }
        createConfigNodeList('global',globalConfigNodes);
    }

    function init() {
        RED.sidebar.addTab({
            id: "config",
            label: RED._("sidebar.config.label"),
            name: RED._("sidebar.config.name"),
            content: content,
            toolbar: toolbar,
            iconClass: "fa fa-cog",
            action: "core:show-config-tab",
            onchange: function() { refreshConfigNodeList(); }
        });
        RED.actions.add("core:show-config-tab", function() {RED.sidebar.show('config')});
        RED.actions.add("core:select-all-config-nodes", function() {
            $(content).find(".red-ui-palette-node").addClass("selected");
        })
        RED.actions.add("core:delete-config-selection", function() {
            var selectedNodes = [];
            $(content).find(".red-ui-palette-node.selected").each(function() {
                selectedNodes.push($(this).parent().data('node'));
            });
            if (selectedNodes.length > 0) {
                var historyEvent = {
                    t:'delete',
                    nodes:[],
                    changes: {},
                    dirty: RED.nodes.dirty()
                }
                for (let i = 0; i < selectedNodes.length; i++) {
                    let node = RED.nodes.node(selectedNodes[i])
                    if (node.z) {
                        let ws = RED.nodes.workspace(node.z)
                        if (ws && ws.locked) {
                            return
                        }
                    }
                }
                selectedNodes.forEach(function(id) {
                    var node = RED.nodes.node(id);
                    try {
                        if (node._def.oneditdelete) {
                            node._def.oneditdelete.call(node);
                        }
                    } catch(err) {
                        console.log("oneditdelete",node.id,node.type,err.toString());
                    }
                    historyEvent.nodes.push(node);
                    for (var i=0;i<node.users.length;i++) {
                        var user = node.users[i];
                        historyEvent.changes[user.id] = {
                            changed: user.changed,
                            valid: user.valid
                        };
                        for (var d in user._def.defaults) {
                            if (user._def.defaults.hasOwnProperty(d) && user[d] == id) {
                                historyEvent.changes[user.id][d] = id
                                user[d] = "";
                                user.changed = true;
                                user.dirty = true;
                            }
                        }
                        RED.editor.validateNode(user);
                    }
                    RED.nodes.remove(id);
                })
                RED.nodes.dirty(true);
                RED.view.redraw(true);
                RED.history.push(historyEvent);
            }
        });


        RED.events.on("view:selection-changed",function() {
            $(content).find(".red-ui-palette-node").removeClass("selected");
        });

        $("#red-ui-sidebar-config-collapse-all").on("click", function(e) {
            e.preventDefault();
            for (var cat in categories) {
                if (categories.hasOwnProperty(cat)) {
                    categories[cat].close();
                }
            }
        });
        $("#red-ui-sidebar-config-expand-all").on("click", function(e) {
            e.preventDefault();
            for (var cat in categories) {
                if (categories.hasOwnProperty(cat)) {
                    if (categories[cat].size() > 0) {
                        categories[cat].open();
                    }
                }
            }
        });
        $('#red-ui-sidebar-config-filter-all').on("click",function(e) {
            e.preventDefault();
            if (showUnusedOnly) {
                $(this).addClass('selected');
                $('#red-ui-sidebar-config-filter-unused').removeClass('selected');
                showUnusedOnly = !showUnusedOnly;
                refreshConfigNodeList();
            }
        });
        $('#red-ui-sidebar-config-filter-unused').on("click",function(e) {
            e.preventDefault();
            if (!showUnusedOnly) {
                $(this).addClass('selected');
                $('#red-ui-sidebar-config-filter-all').removeClass('selected');
                showUnusedOnly = !showUnusedOnly;
                refreshConfigNodeList();
            }
        });

        RED.popover.tooltip($('#red-ui-sidebar-config-filter-all'), RED._("sidebar.config.showAllConfigNodes"));
        RED.popover.tooltip($('#red-ui-sidebar-config-filter-unused'), RED._("sidebar.config.showAllUnusedConfigNodes"));
        RED.popover.tooltip($('#red-ui-sidebar-config-collapse-all'), RED._("palette.actions.collapse-all"));
        RED.popover.tooltip($('#red-ui-sidebar-config-expand-all'), RED._("palette.actions.expand-all"));
    }

    function flashConfigNode(el) {
        if(flashingConfigNode && flashingConfigNode.length) {
            //cancel current flashing node before flashing new node
            clearInterval(flashingConfigNodeTimer);
            flashingConfigNodeTimer = null;
            flashingConfigNode.children("div").removeClass('highlighted');
            flashingConfigNode = null;
        }
        if(!el || !el.children("div").length) { return; }

        flashingConfigNodeTimer = setInterval(function(flashEndTime) {
            if (flashEndTime >= Date.now()) {
                const highlighted = el.children("div").hasClass("highlighted");
                el.children("div").toggleClass('highlighted', !highlighted)
            } else {
                clearInterval(flashingConfigNodeTimer);
                flashingConfigNodeTimer = null;
                flashingConfigNode = null;
                el.children("div").removeClass('highlighted');
            }
        }, 100, Date.now() + 2200);
        flashingConfigNode = el;
        el.children("div").addClass('highlighted');
    }

    function show(id) {
        if (typeof id === 'boolean') {
            if (id) {
                $('#red-ui-sidebar-config-filter-unused').trigger("click");
            } else {
                $('#red-ui-sidebar-config-filter-all').trigger("click");
            }
        }
        refreshConfigNodeList();
        if (typeof id === "string") {
            $('#red-ui-sidebar-config-filter-all').trigger("click");
            id = id.replace(/\./g,"-");
            setTimeout(function() {
                var node = $(".red-ui-palette-node_id_"+id);
                var y = node.position().top;
                var h = node.height();
                var scrollWindow = $(".red-ui-sidebar-node-config");
                var scrollHeight = scrollWindow.height();

                if (y+h > scrollHeight) {
                    scrollWindow.animate({scrollTop: '-='+(scrollHeight-(y+h)-30)},150);
                } else if (y<0) {
                    scrollWindow.animate({scrollTop: '+='+(y-10)},150);
                }
                flashConfigNode(node, id);
            },100);
        }
        RED.sidebar.show("config");
    }
    return {
        init:init,
        show:show,
        refresh:refreshConfigNodeList
    }
})();
;/**
 * 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.
 **/
RED.sidebar.context = (function() {

    var content;
    var sections;

    var flowAutoRefresh;
    var nodeAutoRefresh;
    var nodeSection;
    // var subflowSection;
    var flowSection;
    var globalSection;

    const expandedPaths = {}

    var currentNode;
    var currentFlow;

    function init() {

        content = $("<div>").css({"position":"relative","height":"100%"});
        content.className = "red-ui-sidebar-context"

        var footerToolbar = $('<div></div>');

        var stackContainer = $("<div>",{class:"red-ui-sidebar-context-stack"}).appendTo(content);
        sections = RED.stack.create({
            container: stackContainer
        });

        nodeSection = sections.add({
            title: RED._("sidebar.context.node"),
            collapsible: true
        });
        nodeSection.expand();
        nodeSection.content.css({height:"100%"});
        nodeSection.timestamp = $('<div class="red-ui-sidebar-context-updated">&nbsp;</div>').appendTo(nodeSection.content);
        var table = $('<table class="red-ui-info-table"></table>').appendTo(nodeSection.content);
        nodeSection.table = $('<tbody>').appendTo(table);
        var bg = $('<div style="float: right"></div>').appendTo(nodeSection.header);

        var nodeAutoRefreshSetting = RED.settings.get("editor.context.nodeRefresh",false);
        nodeAutoRefresh = $('<input type="checkbox">').prop("checked",nodeAutoRefreshSetting).appendTo(bg).toggleButton({
            baseClass: "red-ui-sidebar-header-button red-ui-button-small",
            enabledLabel: "",
            disabledLabel: ""
        }).on("change", function() {
            var value = $(this).prop("checked");
            RED.settings.set("editor.context.flowRefresh",value);
        });
        RED.popover.tooltip(nodeAutoRefresh.next(),RED._("sidebar.context.autoRefresh"));


        var manualRefreshNode = $('<button class="red-ui-button red-ui-button-small" style="margin-left: 5px"><i class="fa fa-refresh"></i></button>')
            .appendTo(bg)
            .on("click", function(evt) {
                evt.stopPropagation();
                evt.preventDefault();
                updateNode(currentNode, true);
            })
        RED.popover.tooltip(manualRefreshNode,RED._("sidebar.context.refrsh"));

        flowSection = sections.add({
            title: RED._("sidebar.context.flow"),
            collapsible: true
        });
        flowSection.expand();
        flowSection.content.css({height:"100%"});
        flowSection.timestamp = $('<div class="red-ui-sidebar-context-updated">&nbsp;</div>').appendTo(flowSection.content);
        var table = $('<table class="red-ui-info-table"></table>').appendTo(flowSection.content);
        flowSection.table = $('<tbody>').appendTo(table);
        bg = $('<div style="float: right"></div>').appendTo(flowSection.header);

        var flowAutoRefreshSetting = RED.settings.get("editor.context.flowRefresh",false);
        flowAutoRefresh = $('<input type="checkbox">').prop("checked",flowAutoRefreshSetting).appendTo(bg).toggleButton({
            baseClass: "red-ui-sidebar-header-button red-ui-button-small",
            enabledLabel: "",
            disabledLabel: ""
        }).on("change", function() {
            var value = $(this).prop("checked");
            RED.settings.set("editor.context.flowRefresh",value);
        });
        RED.popover.tooltip(flowAutoRefresh.next(),RED._("sidebar.context.autoRefresh"));

        var manualRefreshFlow = $('<button class="red-ui-button red-ui-button-small" style="margin-left: 5px"><i class="fa fa-refresh"></i></button>')
            .appendTo(bg)
            .on("click", function(evt) {
                evt.stopPropagation();
                evt.preventDefault();
                updateFlow(currentFlow, true);
            })
        RED.popover.tooltip(manualRefreshFlow,RED._("sidebar.context.refrsh"));

        globalSection = sections.add({
            title: RED._("sidebar.context.global"),
            collapsible: true
        });
        globalSection.expand();
        globalSection.content.css({height:"100%"});
        globalSection.timestamp = $('<div class="red-ui-sidebar-context-updated">&nbsp;</div>').appendTo(globalSection.content);
        var table = $('<table class="red-ui-info-table"></table>').appendTo(globalSection.content);
        globalSection.table = $('<tbody>').appendTo(table);

        bg = $('<div style="float: right"></div>').appendTo(globalSection.header);
        $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>')
            .appendTo(bg)
            .on("click", function(evt) {
                evt.stopPropagation();
                evt.preventDefault();
                updateEntry(globalSection,"context/global","global");
            })
        RED.popover.tooltip(bg,RED._("sidebar.context.refrsh"));

        RED.actions.add("core:show-context-tab",show);

        RED.sidebar.addTab({
            id: "context",
            label: RED._("sidebar.context.label"),
            name: RED._("sidebar.context.name"),
            iconClass: "fa fa-database",
            content: content,
            toolbar: footerToolbar,
            // pinned: true,
            enableOnEdit: true,
            action: "core:show-context-tab"
        });

        RED.events.on("view:selection-changed", function(event) {
            var selectedNode = event.nodes && event.nodes.length === 1 && event.nodes[0];
            updateNode(selectedNode);
        })

        RED.events.on("workspace:change", function(event) {
            updateFlow(RED.nodes.workspace(event.workspace));
        })

        $(globalSection.table).empty();
        $('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.refresh"></td></tr>').appendTo(globalSection.table).i18n();
        globalSection.timestamp.html("&nbsp;");
    }

    function updateNode(node,force) {
        currentNode = node;
        if (force || nodeAutoRefresh.prop("checked")) {
            if (node) {
                updateEntry(nodeSection,"context/node/"+node.id,node.id);
            } else {
                updateEntry(nodeSection)
            }
        } else {
            $(nodeSection.table).empty();
            if (node) {
                $('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.refresh"></td></tr>').appendTo(nodeSection.table).i18n();
            } else {
                $('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.none"></td></tr>').appendTo(nodeSection.table).i18n();
            }
            nodeSection.timestamp.html("&nbsp;");
        }
    }
    function updateFlow(flow, force) {
        currentFlow = flow;
        if (force || flowAutoRefresh.prop("checked")) {
            if (flow) {
                updateEntry(flowSection,"context/flow/"+flow.id,flow.id);
            } else {
                updateEntry(flowSection)
            }
        } else {
            $(flowSection.table).empty();
            $('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.refresh"></td></tr>').appendTo(flowSection.table).i18n();
            flowSection.timestamp.html("&nbsp;");
        }
    }

    function refreshEntry(section,baseUrl,id) {

        var contextStores = RED.settings.context.stores;
        var container = section.table;

        $.getJSON(baseUrl, function(data) {
            $(container).empty();
            var sortedData = {};
            for (var store in data) {
                if (data.hasOwnProperty(store)) {
                    for (var key in data[store]) {
                        if (data[store].hasOwnProperty(key)) {
                            if (!sortedData.hasOwnProperty(key)) {
                                sortedData[key] = [];
                            }
                            data[store][key].store = store;
                            sortedData[key].push(data[store][key])
                        }
                    }
                }
            }
            var keys = Object.keys(sortedData);
            keys.sort();
            var l = keys.length;
            for (var i = 0; i < l; i++) {
                sortedData[keys[i]].forEach(function(v) {
                    const k = keys[i];
                    let payload = v.msg;
                    let format = v.format;
                    const tools = $('<span class="button-group"></span>');
                    expandedPaths[id + "." + k] = expandedPaths[id + "." + k] || new Set()
                    const objectElementOptions = {
                        typeHint: format,
                        sourceId: id + "." + k,
                        tools,
                        path: k,
                        rootPath: k,
                        exposeApi: true,
                        ontoggle: function(path,state) {
                            path = path.substring(k.length+1)
                            if (state) {
                                expandedPaths[id+"."+k].add(path)
                            } else {
                                // if 'a' has been collapsed, we want to remove 'a.b' and 'a[0]...' from the set
                                // of collapsed paths
                                for (let expandedPath of expandedPaths[id+"."+k]) {
                                    if (expandedPath.startsWith(path+".") || expandedPath.startsWith(path+"[")) {
                                        expandedPaths[id+"."+k].delete(expandedPath)
                                    }
                                }
                                expandedPaths[id+"."+k].delete(path)
                            }
                        },
                        expandPaths: [ ...expandedPaths[id+"."+k] ].sort(),
                        expandLeafNodes: true
                    }
                    const propRow = $('<tr class="red-ui-help-info-row"><td class="red-ui-sidebar-context-property"></td><td></td></tr>').appendTo(container);
                    const obj = $(propRow.children()[0]);
                    obj.text(k);
                    const urlSafeK = encodeURIComponent(k)
                    const refreshItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>').appendTo(tools).on("click", function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                        $.getJSON(baseUrl+"/"+urlSafeK+"?store="+v.store, function(data) {
                            if (data.msg !== payload || data.format !== format) {
                                payload = data.msg;
                                format = data.format;
                                tools.detach();
                                $(propRow.children()[1]).empty();
                                RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
                                    ...objectElementOptions,
                                    typeHint: data.format,
                                }).appendTo(propRow.children()[1]);
                            }
                        })
                    });
                    RED.popover.tooltip(refreshItem,RED._("sidebar.context.refrsh"));
                    const deleteItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>').appendTo(tools).on("click", function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                        var popover = RED.popover.create({
                            trigger: 'modal',
                            target: propRow,
                            direction: "left",
                            content: function() {
                                const content = $('<div>');
                                $('<p data-i18n="sidebar.context.deleteConfirm"></p>').appendTo(content);
                                var row = $('<p>').appendTo(content);
                                var bg = $('<span class="button-group"></span>').appendTo(row);
                                $('<button class="red-ui-button" data-i18n="common.label.cancel"></button>').appendTo(bg).on("click", function(e) {
                                    e.preventDefault();
                                    popover.close();
                                });
                                bg = $('<span class="button-group"></span>').appendTo(row);
                                $('<button class="red-ui-button primary" data-i18n="common.label.delete"></button>').appendTo(bg).on("click", function(e) {
                                    e.preventDefault();
                                    popover.close();
                                    const urlSafeK = encodeURIComponent(k)
                                    $.ajax({
                                        url: baseUrl+"/"+urlSafeK+"?store="+v.store,
                                        type: "DELETE"
                                    }).done(function(data,textStatus,xhr) {
                                        $.getJSON(baseUrl+"/"+urlSafeK+"?store="+v.store, function(data) {
                                            if (data.format === 'undefined') {
                                                propRow.remove();
                                                if (container.children().length === 0) {
                                                    $('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.empty"></td></tr>').appendTo(container).i18n();
                                                }
                                                delete expandedPaths[id + "." + k]
                                            } else {
                                                payload = data.msg;
                                                format = data.format;
                                                tools.detach();
                                                $(propRow.children()[1]).empty();
                                                RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
                                                    ...objectElementOptions,
                                                    typeHint: data.format
                                                }).appendTo(propRow.children()[1]);
                                            }
                                        });
                                    }).fail(function(xhr,textStatus,err) {

                                    })
                                });
                                return content.i18n();
                            }
                        });
                        popover.open();

                    });
                    RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete"));
                    RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), objectElementOptions).appendTo(propRow.children()[1]);
                    if (contextStores.length > 1) {
                        $("<span>",{class:"red-ui-sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0]))
                    }
                });
            }
            if (l === 0) {
                $('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.empty"></td></tr>').appendTo(container).i18n();
            }
            $(section.timestamp).text(new Date().toLocaleString());
        });
    }
    function updateEntry(section,baseUrl,id) {
        var container = section.table;
        if (id) {
            refreshEntry(section,baseUrl,id);
        } else {
            $(container).empty();
            $('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.none"></td></tr>').appendTo(container).i18n();
        }
    }



    function show() {
        RED.sidebar.show("context");
    }
    return {
        init: init
    }
})();
;/**
 * 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.
 **/
RED.palette.editor = (function() {

    var disabled = false;
    let catalogues = []
    const loadedCatalogs = []
    var editorTabs;
    let filterInput;
    let searchInput;
    let nodeList;
    let packageList;
    let fullList = []
    let loadedList = [];
    let filteredList = [];
    let loadedIndex = {};

    var typesInUse = {};
    var nodeEntries = {};
    var eventTimers = {};
    var activeFilter = "";

    var semverre = /^(\d+)(\.(\d+))?(\.(\d+))?(-([0-9A-Za-z-]+))?(\.([0-9A-Za-z-.]+))?$/;
    var NUMBERS_ONLY = /^\d+$/;

    function SemVerPart(part) {
        this.number = 0;
        this.text = part;
        if ( NUMBERS_ONLY.test(part)){
            this.number = parseInt(part);
            this.type = "N";
        } else {
            this.type = part == undefined || part.length < 1 ? "E" : "T";
        }
    }

    SemVerPart.prototype.compare = function(other) {
        var types = this.type + other.type;
        switch ( types ) {
            case "EE": return 0;
            case "NT":
            case "TE":
            case "EN": return -1;
            case "NN": return this.number - other.number;
            case "TT": return this.text.localeCompare( other.text );
            case "ET":
            case "TN":
            case "NE": return 1;
        }
    };

    function SemVer(ver) {
        var groups = ver.match( semverre );
        this.parts = [ new SemVerPart( groups[1] ), new SemVerPart( groups[3] ), new SemVerPart( groups[5] ), new SemVerPart( groups[7] ), new SemVerPart( groups[9] ) ];
    }

    SemVer.prototype.compare = function(other) {
        var result = 0;
        for ( var i = 0, n = this.parts.length; result == 0 && i < n; i++ ) {
            result = this.parts[ i ].compare( other.parts[ i ] );
        }
        return result;
    };

    function semVerCompare(ver1, ver2) {
        var semver1 = new SemVer(ver1);
        var semver2 = new SemVer(ver2);
        var result = semver1.compare(semver2);
        return result;
    }

    function delayCallback(start,callback) {
        var delta = Date.now() - start;
        if (delta < 300) {
            delta = 300;
        } else {
            delta = 0;
        }
        setTimeout(function() {
            callback();
        },delta);
    }
    function changeNodeState(id,state,shade,callback) {
        shade.show();
        var start = Date.now();
        $.ajax({
            url:"nodes/"+id,
            type: "PUT",
            data: JSON.stringify({
                enabled: state
            }),
            contentType: "application/json; charset=utf-8"
        }).done(function(data,textStatus,xhr) {
            delayCallback(start,function() {
                shade.hide();
                callback();
            });
        }).fail(function(xhr,textStatus,err) {
            delayCallback(start,function() {
                shade.hide();
                callback(xhr);
            });
        })
    }
    function installNodeModule(id,version,url,callback) {
        var requestBody = {
            module: id
        };
        if (version) {
            requestBody.version = version;
        }
        if (url) {
            requestBody.url = url;
        }
        $.ajax({
            url:"nodes",
            type: "POST",
            data: JSON.stringify(requestBody),
            contentType: "application/json; charset=utf-8"
        }).done(function(data,textStatus,xhr) {
            callback();
        }).fail(function(xhr,textStatus,err) {
            callback(xhr,textStatus,err);
        });
    }
    function removeNodeModule(id,callback) {
        $.ajax({
            url:"nodes/"+id,
            type: "DELETE"
        }).done(function(data,textStatus,xhr) {
            callback();
        }).fail(function(xhr,textStatus,err) {
            callback(xhr);
        })
    }

    function refreshNodeModuleList() {
        for (var id in nodeEntries) {
            if (nodeEntries.hasOwnProperty(id)) {
                _refreshNodeModule(id);
            }
        }
    }

    function refreshNodeModule(module) {
        if (!eventTimers.hasOwnProperty(module)) {
            eventTimers[module] = setTimeout(function() {
                delete eventTimers[module];
                _refreshNodeModule(module);
            },100);
        }
    }

    function getContrastingBorder(rgbColor){
        var parts = /^rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)[,)]/.exec(rgbColor);
        if (parts) {
            var r = parseInt(parts[1]);
            var g = parseInt(parts[2]);
            var b = parseInt(parts[3]);
            var yiq = ((r*299)+(g*587)+(b*114))/1000;
            if (yiq > 160) {
                r = Math.floor(r*0.8);
                g = Math.floor(g*0.8);
                b = Math.floor(b*0.8);
                return "rgb("+r+","+g+","+b+")";
            }
        }
        return rgbColor;
    }

    function formatUpdatedAt(dateString) {
        var now = new Date();
        var d = new Date(dateString);
        var delta = (Date.now() - new Date(dateString).getTime())/1000;

        if (delta < 60) {
            return RED._('palette.editor.times.seconds');
        }
        delta = Math.floor(delta/60);
        if (delta < 10) {
            return RED._('palette.editor.times.minutes');
        }
        if (delta < 60) {
            return RED._('palette.editor.times.minutesV',{count:delta});
        }

        delta = Math.floor(delta/60);

        if (delta < 24) {
            return RED._('palette.editor.times.hoursV',{count:delta});
        }

        delta = Math.floor(delta/24);

        if (delta < 7) {
            return RED._('palette.editor.times.daysV',{count:delta})
        }
        var weeks = Math.floor(delta/7);
        var days = delta%7;

        if (weeks < 4) {
            return RED._('palette.editor.times.weeksV',{count:weeks})
        }

        var months = Math.floor(weeks/4);
        weeks = weeks%4;

        if (months < 12) {
            return RED._('palette.editor.times.monthsV',{count:months})
        }
        var years = Math.floor(months/12);
        months = months%12;

        if (months === 0) {
            return RED._('palette.editor.times.yearsV',{count:years})
        } else {
            return RED._('palette.editor.times.year'+(years>1?'s':'')+'MonthsV',{y:years,count:months})
        }
    }


    function _refreshNodeModule(module) {
        if (!nodeEntries.hasOwnProperty(module)) {
            nodeEntries[module] = {info:RED.nodes.registry.getModule(module)};
            var index = [module];
            for (var s in nodeEntries[module].info.sets) {
                if (nodeEntries[module].info.sets.hasOwnProperty(s)) {
                    index.push(s);
                    index = index.concat(nodeEntries[module].info.sets[s].types)
                }
            }
            nodeEntries[module].index = index.join(",").toLowerCase();
            nodeList.editableList('addItem', nodeEntries[module]);
        } else {
            var moduleInfo = nodeEntries[module].info;
            var nodeEntry = nodeEntries[module].elements;
            if (nodeEntry) {
                if (moduleInfo.plugin) {
                    nodeEntry.enableButton.hide();
                    nodeEntry.removeButton.show();

                    let pluginCount = 0;
                    for (let setName in moduleInfo.sets) {
                        if (moduleInfo.sets.hasOwnProperty(setName)) {
                            let set = moduleInfo.sets[setName];
                            if (set.plugins) {
                                pluginCount += set.plugins.length;
                            }
                        }
                    }
                    
                    nodeEntry.setCount.text(RED._('palette.editor.pluginCount',{count:pluginCount,label:pluginCount}));

                } else {
                    var activeTypeCount = 0;
                    var typeCount = 0;
                    var errorCount = 0;
                    nodeEntry.errorList.empty();
                    nodeEntries[module].totalUseCount = 0;
                    nodeEntries[module].setUseCount = {};

                    for (var setName in moduleInfo.sets) {
                        if (moduleInfo.sets.hasOwnProperty(setName)) {
                            var inUseCount = 0;
                            const set = moduleInfo.sets[setName];
                            const setElements = nodeEntry.sets[setName]

                            if (set.err) {
                                errorCount++;
                                var errMessage = set.err;
                                if (set.err.message) {
                                    errMessage = set.err.message;
                                } else if (set.err.code) {
                                    errMessage = set.err.code;
                                }
                                $("<li>").text(errMessage).appendTo(nodeEntry.errorList);
                            }
                            if (set.enabled) {
                                activeTypeCount += set.types.length;
                            }
                            typeCount += set.types.length;
                            for (var i=0;i<moduleInfo.sets[setName].types.length;i++) {
                                var t = moduleInfo.sets[setName].types[i];
                                inUseCount += (typesInUse[t]||0);
                                if (setElements && set.enabled) {
                                    var def = RED.nodes.getType(t);
                                    if (def && def.color) {
                                        setElements.swatches[t].css({background:RED.utils.getNodeColor(t,def)});
                                        setElements.swatches[t].css({border: "1px solid "+getContrastingBorder(setElements.swatches[t].css('backgroundColor'))})
                                    }
                                }
                            }
                            nodeEntries[module].setUseCount[setName] = inUseCount;
                            nodeEntries[module].totalUseCount += inUseCount;

                            if (setElements) {
                                if (inUseCount > 0) {
                                    setElements.enableButton.text(RED._('palette.editor.inuse'));
                                    setElements.enableButton.addClass('disabled');
                                } else {
                                    setElements.enableButton.removeClass('disabled');
                                    if (set.enabled) {
                                        setElements.enableButton.text(RED._('palette.editor.disable'));
                                    } else {
                                        setElements.enableButton.text(RED._('palette.editor.enable'));
                                    }
                                }
                                setElements.setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);
                            }
                        }
                    }

                    if (errorCount === 0) {
                        nodeEntry.errorRow.hide()
                    } else {
                        nodeEntry.errorRow.show();
                    }

                    var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount;
                    nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount}));

                    if (nodeEntries[module].totalUseCount > 0) {
                        nodeEntry.enableButton.text(RED._('palette.editor.inuse'));
                        nodeEntry.enableButton.addClass('disabled');
                        nodeEntry.removeButton.hide();
                    } else {
                        nodeEntry.enableButton.removeClass('disabled');
                        if (moduleInfo.local) {
                            nodeEntry.removeButton.css('display', 'inline-block');
                        }
                        if (activeTypeCount === 0) {
                            nodeEntry.enableButton.text(RED._('palette.editor.enableall'));
                        } else {
                            nodeEntry.enableButton.text(RED._('palette.editor.disableall'));
                        }
                        nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
                    }
                }
            }
            if (moduleInfo.pending_version) {
                nodeEntry.versionSpan.html(moduleInfo.version+' <i class="fa fa-long-arrow-right"></i> '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow)
                nodeEntry.updateButton.text(RED._('palette.editor.updated')).addClass('disabled').css('display', 'inline-block');
            } else if (loadedIndex.hasOwnProperty(module)) {
                if (updateAllowed &&
                    semVerCompare(loadedIndex[module].version,moduleInfo.version) > 0 &&
                    RED.utils.checkModuleAllowed(module,null,updateAllowList,updateDenyList)
                ) {
                    nodeEntry.updateButton.show();
                    nodeEntry.updateButton.text(RED._('palette.editor.update',{version:loadedIndex[module].version}));
                } else {
                    nodeEntry.updateButton.hide();
                }
            } else {
                nodeEntry.updateButton.hide();
            }
        }

    }

    function filterChange(val) {
        activeFilter = val.toLowerCase();
        var visible = nodeList.editableList('filter');
        var size = nodeList.editableList('length');
        if (val === "") {
            filterInput.searchBox('count');
        } else {
            filterInput.searchBox('count',visible+" / "+size);
        }
    }


    var catalogueCount;
    var catalogueLoadStatus = [];
    var catalogueLoadStart;
    var catalogueLoadErrors = false;

    var activeSort = sortModulesRelevance;

    function handleCatalogResponse(err,catalog,index,v) {
        const url = catalog.url
        catalogueLoadStatus.push(err||v);
        if (!err) {
            if (v.modules) {
                v.modules = v.modules.filter(function(m) {
                    if (RED.utils.checkModuleAllowed(m.id,m.version,installAllowList,installDenyList)) {
                        loadedIndex[m.id] = m;
                        m.index = [m.id];
                        if (m.keywords) {
                            m.index = m.index.concat(m.keywords);
                        }
                        if (m.types) {
                            m.index = m.index.concat(m.types);
                        }
                        if (m.updated_at) {
                            m.timestamp = new Date(m.updated_at).getTime();
                        } else {
                            m.timestamp = 0;
                        }
                        m.index = m.index.join(",").toLowerCase();
                        m.catalog = catalog;
                        m.catalogIndex = index;
                        return true;
                    }
                    return false;
                })
                loadedList = loadedList.concat(v.modules);
            }
        } else {
            catalogueLoadErrors = true;
        }
        if (catalogueCount > 1) {
            $(".red-ui-palette-module-shade-status").html(RED._('palette.editor.loading')+"<br>"+catalogueLoadStatus.length+"/"+catalogueCount);
        }
        if (catalogueLoadStatus.length === catalogueCount) {
            if (catalogueLoadErrors) {
                RED.notify(RED._('palette.editor.errors.catalogLoadFailed',{url: url}),"error",false,8000);
            }
            var delta = 250-(Date.now() - catalogueLoadStart);
            setTimeout(function() {
                $("#red-ui-palette-module-install-shade").hide();
            },Math.max(delta,0));

        }
    }

    function initInstallTab() {
        if (loadedList.length === 0) {
            fullList = [];
            loadedList = [];
            loadedIndex = {};
            packageList.editableList('empty');

            $(".red-ui-palette-module-shade-status").text(RED._('palette.editor.loading'));

            catalogueLoadStatus = [];
            catalogueLoadErrors = false;
            catalogueCount = catalogues.length;
            if (catalogues.length > 1) {
                $(".red-ui-palette-module-shade-status").html(RED._('palette.editor.loading')+"<br>0/"+catalogues.length);
            }
            $("#red-ui-palette-module-install-shade").show();
            catalogueLoadStart = Date.now();
            var handled = 0;
            loadedCatalogs.length = 0; // clear the loadedCatalogs array
            for (let index = 0; index < catalogues.length; index++) {
                const url = catalogues[index];
                $.getJSON(url, {_: new Date().getTime()},function(v) {
                    loadedCatalogs.push({ index: index, url: url, name: v.name, updated_at: v.updated_at, modules_count: (v.modules || []).length })
                    handleCatalogResponse(null,{ url: url, name: v.name},index,v);
                    refreshNodeModuleList();
                }).fail(function(jqxhr, textStatus, error) {
                    console.warn("Error loading catalog",url,":",error);
                    handleCatalogResponse(jqxhr,url,index);
                }).always(function() {
                    handled++;
                    if (handled === catalogueCount) {
                        //sort loadedCatalogs by e.index ascending
                        loadedCatalogs.sort((a, b) => a.index - b.index)
                        updateCatalogFilter(loadedCatalogs)
                    }
                })
            }
        }
    }

    /**
     * Refreshes the catalog filter dropdown and updates local variables
     * @param {[{url:String, name:String, updated_at:String, modules_count:Number}]} catalogEntries
     */
    function updateCatalogFilter(catalogEntries, maxRetry = 3) {
        // clean up existing filters
        const catalogSelection = $('#red-catalogue-filter-select')
        if (catalogSelection.length === 0) {
            // sidebar not yet loaded (red-catalogue-filter-select is not in dom)
            if (maxRetry > 0) {
               // console.log("updateCatalogFilter: sidebar not yet loaded, retrying in 100ms")
                // try again in 100ms
                setTimeout(() => {
                    updateCatalogFilter(catalogEntries, maxRetry - 1)
                }, 100);
                return;
            } 
            return; // give up
        }
        catalogSelection.off("change") // remove any existing event handlers
        catalogSelection.attr('disabled', 'disabled')
        catalogSelection.empty()
        catalogSelection.append($('<option>', { value: "loading", text: RED._('palette.editor.loading'), disabled: true, selected: true }));
        
        fullList = loadedList.slice()
        catalogSelection.empty() // clear the select list

        // loop through catalogTypes, and an option entry per catalog
        for (let index = 0; index < catalogEntries.length; index++) {
            const catalog = catalogEntries[index];
            catalogSelection.append(`<option value="${catalog.name}">${catalog.name}</option>`)
        }
        // select the 1st option in the select list
        catalogSelection.val(catalogSelection.find('option:first').val())
        
        // if there is only 1 catalog, hide the select
        if (catalogEntries.length > 1) {
            catalogSelection.prepend(`<option value="all">${RED._('palette.editor.allCatalogs')}</option>`)
            catalogSelection.val('all')
            catalogSelection.removeAttr('disabled') // permit the user to select a catalog
        }
        // refresh the searchInput counter and trigger a change
        filterByCatalog(catalogSelection.val())
        searchInput.searchBox('change');

        // hook up the change event handler
        catalogSelection.on("change", function() {
            const selectedCatalog = $(this).val();
            filterByCatalog(selectedCatalog);
            searchInput.searchBox('change');
        })
    }

    function filterByCatalog(selectedCatalog) {
        if (loadedCatalogs.length <= 1 || selectedCatalog === "all") {
            loadedList = fullList.slice();
        } else {
            loadedList = fullList.filter(function(m) {
                return (m.catalog.name === selectedCatalog);
            })
        }
        refreshFilteredItems();
        searchInput.searchBox('count',filteredList.length+" / "+loadedList.length);
    }

    function refreshFilteredItems() {
        packageList.editableList('empty');
        var currentFilter = searchInput.searchBox('value').trim();
        if (currentFilter === "" && loadedList.length > 20){
            packageList.editableList('addItem',{count:loadedList.length})
            return;
        }
        filteredList.sort(activeSort);
        for (var i=0;i<Math.min(10,filteredList.length);i++) {
            packageList.editableList('addItem',filteredList[i]);
        }
        if (filteredList.length === 0) {
            packageList.editableList('addItem',{});
        }
        if (filteredList.length > 10) {
            packageList.editableList('addItem',{start:10,more:filteredList.length-10})
        }
    }
    function sortModulesRelevance(A,B) {
        var currentFilter = searchInput.searchBox('value').trim();
        if (currentFilter === "") {
            return sortModulesAZ(A,B);
        }
        var i = A.info.index.indexOf(currentFilter) - B.info.index.indexOf(currentFilter);
        if (i === 0) {
            return sortModulesAZ(A,B);
        }
        return i;
    }
    function sortModulesAZ(A,B) {
        return A.info.id.localeCompare(B.info.id);
    }
    function sortModulesRecent(A,B) {
        return -1 * (A.info.timestamp-B.info.timestamp);
    }

    var installAllowList = ['*'];
    var installDenyList = [];
    var updateAllowed = true;
    var updateAllowList = ['*'];
    var updateDenyList = [];

    function init() {
        catalogues = RED.settings.theme('palette.catalogues')||['https://catalogue.nodered.org/catalogue.json']
        if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
            return;
        }
        var settingsAllowList = RED.settings.get("externalModules.palette.allowList")
        var settingsDenyList = RED.settings.get("externalModules.palette.denyList")
        if (settingsAllowList || settingsDenyList) {
            installAllowList = settingsAllowList;
            installDenyList = settingsDenyList
        }
        installAllowList = RED.utils.parseModuleList(installAllowList);
        installDenyList = RED.utils.parseModuleList(installDenyList);

        var settingsUpdateAllowList = RED.settings.get("externalModules.palette.allowUpdateList")
        var settingsUpdateDenyList = RED.settings.get("externalModules.palette.denyUpdateList")
        if (settingsUpdateAllowList || settingsUpdateDenyList) {
            updateAllowList = settingsUpdateAllowList;
            updateDenyList = settingsUpdateDenyList;
        }
        updateAllowList = RED.utils.parseModuleList(updateAllowList);
        updateDenyList = RED.utils.parseModuleList(updateDenyList);
        updateAllowed = RED.settings.get("externalModules.palette.allowUpdate",true);


        createSettingsPane();

        RED.userSettings.add({
            id:'palette',
            title: RED._("palette.editor.palette"),
            get: getSettingsPane,
            close: function() {
                settingsPane.detach();
            },
            focus: function() {
                editorTabs.resize();
                setTimeout(function() {
                    filterInput.trigger("focus");
                },200);
            }
        })

        RED.actions.add("core:manage-palette",function() {
                RED.userSettings.show('palette');
            });

        RED.events.on('registry:module-updated', function(ns) {
            refreshNodeModule(ns.module);
        });
        RED.events.on('registry:node-set-enabled', function(ns) {
            refreshNodeModule(ns.module);
        });
        RED.events.on('registry:node-set-disabled', function(ns) {
            refreshNodeModule(ns.module);
        });
        RED.events.on('registry:node-type-added', function(nodeType) {
            if (!/^subflow:/.test(nodeType)) {
                var ns = RED.nodes.registry.getNodeSetForType(nodeType);
                refreshNodeModule(ns.module);
            }
        });
        RED.events.on('registry:node-type-removed', function(nodeType) {
            if (!/^subflow:/.test(nodeType)) {
                var ns = RED.nodes.registry.getNodeSetForType(nodeType);
                refreshNodeModule(ns.module);
            }
        });
        RED.events.on('registry:node-set-added', function(ns) {
            refreshNodeModule(ns.module);
            for (var i=0;i<filteredList.length;i++) {
                if (filteredList[i].info.id === ns.module) {
                    var installButton = filteredList[i].elements.installButton;
                    installButton.addClass('disabled');
                    installButton.text(RED._('palette.editor.installed'));
                    break;
                }
            }
        });
        RED.events.on('registry:node-set-removed', function(ns) {
            var module = RED.nodes.registry.getModule(ns.module);
            if (!module) {
                var entry = nodeEntries[ns.module];
                if (entry) {
                    nodeList.editableList('removeItem', entry);
                    delete nodeEntries[ns.module];
                    for (var i=0;i<filteredList.length;i++) {
                        if (filteredList[i].info.id === ns.module) {
                            var installButton = filteredList[i].elements.installButton;
                            installButton.removeClass('disabled');
                            installButton.text(RED._('palette.editor.install'));
                            break;
                        }
                    }
                }
            }
        });
        RED.events.on('nodes:add', function(n) {
            if (!/^subflow:/.test(n.type)) {
                typesInUse[n.type] = (typesInUse[n.type]||0)+1;
                if (typesInUse[n.type] === 1) {
                    var ns = RED.nodes.registry.getNodeSetForType(n.type);
                    refreshNodeModule(ns.module);
                }
            }
        })
        RED.events.on('nodes:remove', function(n) {
            if (typesInUse.hasOwnProperty(n.type)) {
                typesInUse[n.type]--;
                if (typesInUse[n.type] === 0) {
                    delete typesInUse[n.type];
                    var ns = RED.nodes.registry.getNodeSetForType(n.type);
                    refreshNodeModule(ns.module);
                }
            }
        })

        RED.events.on("registry:plugin-module-added", function(module) {

            if (!nodeEntries.hasOwnProperty(module)) {
                nodeEntries[module] = {info:RED.plugins.getModule(module)};
                var index = [module];
                for (var s in nodeEntries[module].info.sets) {
                    if (nodeEntries[module].info.sets.hasOwnProperty(s)) {
                        index.push(s);
                        index = index.concat(nodeEntries[module].info.sets[s].types)
                    }
                }
                nodeEntries[module].index = index.join(",").toLowerCase();
                nodeList.editableList('addItem', nodeEntries[module]);
            } else {
                _refreshNodeModule(module);
            }

            for (var i=0;i<filteredList.length;i++) {
                if (filteredList[i].info.id === module) {
                    var installButton = filteredList[i].elements.installButton;
                    installButton.addClass('disabled');
                    installButton.text(RED._('palette.editor.installed'));
                    break;
                }
            }
        });
    }

    var settingsPane;

    function getSettingsPane() {
        initInstallTab();
        editorTabs.activateTab('nodes');
        return settingsPane;
    }

    function createSettingsPane() {
        settingsPane = $('<div id="red-ui-settings-tab-palette"></div>');
        var content = $('<div id="red-ui-palette-editor">'+
            '<ul id="red-ui-palette-editor-tabs"></ul>'+
        '</div>').appendTo(settingsPane);

        editorTabs = RED.tabs.create({
            element: settingsPane.find('#red-ui-palette-editor-tabs'),
            onchange:function(tab) {
                content.find(".red-ui-palette-editor-tab").hide();
                tab.content.show();
                if (filterInput) {
                    filterInput.searchBox('value',"");
                }
                if (searchInput) {
                    searchInput.searchBox('value',"");
                }
                if (tab.id === 'install') {
                    if (searchInput) {
                        searchInput.trigger("focus");
                    }
                } else {
                    if (filterInput) {
                        filterInput.trigger("focus");
                    }
                }
            },
            minimumActiveTabWidth: 110
        });

        createNodeTab(content);
        createInstallTab(content);
    }

    function createNodeTab(content) {
        var modulesTab = $('<div>',{class:"red-ui-palette-editor-tab"}).appendTo(content);

        editorTabs.addTab({
            id: 'nodes',
            label: RED._('palette.editor.tab-nodes'),
            content: modulesTab
        })

        var filterDiv = $('<div>',{class:"red-ui-palette-search"}).appendTo(modulesTab);
        filterInput = $('<input type="text" data-i18n="[placeholder]palette.filter"></input>')
            .appendTo(filterDiv)
            .searchBox({
                delay: 200,
                change: function() {
                    filterChange($(this).val());
                }
            });


        nodeList = $('<ol>',{id:"red-ui-palette-module-list"}).appendTo(modulesTab).editableList({
            class: "scrollable",
            addButton: false,
            scrollOnAdd: false,
            sort: function(A,B) {
                return A.info.name.localeCompare(B.info.name);
            },
            filter: function(data) {
                if (activeFilter === "" ) {
                    return true;
                }

                return (activeFilter==="")||(data.index.indexOf(activeFilter) > -1);
            },
            addItem: function(container,i,object) {
                var entry = object.info;
                if (entry) {
                    var headerRow = $('<div>',{class:"red-ui-palette-module-header"}).appendTo(container);
                    var titleRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-name"><i class="fa fa-cube"></i></div>').appendTo(headerRow);
                    $('<span>').text(entry.name).appendTo(titleRow);
                    var metaRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-version"><i class="fa fa-tag"></i></div>').appendTo(headerRow);
                    var versionSpan = $('<span>').text(entry.version).appendTo(metaRow);

                    var errorRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-errors"><i class="fa fa-warning"></i></div>').hide().appendTo(headerRow);
                    var errorList = $('<ul class="red-ui-palette-module-error-list"></ul>').appendTo(errorRow);
                    var buttonRow = $('<div>',{class:"red-ui-palette-module-meta"}).appendTo(headerRow);
                    var setButton = $('<a href="#" class="red-ui-button red-ui-button-small red-ui-palette-module-set-button"><i class="fa fa-angle-right red-ui-palette-module-node-chevron"></i> </a>').appendTo(buttonRow);
                    var setCount = $('<span>').appendTo(setButton);
                    var buttonGroup = $('<div>',{class:"red-ui-palette-module-button-group"}).appendTo(buttonRow);

                    var updateButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').text(RED._('palette.editor.update')).appendTo(buttonGroup);
                    updateButton.attr('id','up_'+Math.floor(Math.random()*1000000000));
                    updateButton.on("click", function(evt) {
                        evt.preventDefault();
                        if ($(this).hasClass('disabled')) {
                            return;
                        }
                        update(entry,loadedIndex[entry.name].version,loadedIndex[entry.name].pkg_url,container,function(err){});
                    })


                    var removeButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').text(RED._('palette.editor.remove')).appendTo(buttonGroup);
                    removeButton.attr('id','up_'+Math.floor(Math.random()*1000000000));
                    removeButton.on("click", function(evt) {
                        evt.preventDefault();
                        remove(entry,container,function(err){});
                    })
                    if (!entry.local) {
                        removeButton.hide();
                    }
                    var enableButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').text(RED._('palette.editor.disableall')).appendTo(buttonGroup);

                    var contentRow = $('<div>',{class:"red-ui-palette-module-content"}).appendTo(container);
                    var shade = $('<div class="red-ui-palette-module-shade hide"><img src="red/images/spin.svg" class="red-ui-palette-spinner"/></div>').appendTo(container);

                    object.elements = {
                        updateButton: updateButton,
                        removeButton: removeButton,
                        enableButton: enableButton,
                        errorRow: errorRow,
                        errorList: errorList,
                        setCount: setCount,
                        setButton: setButton,
                        container: container,
                        shade: shade,
                        versionSpan: versionSpan,
                        sets: {}
                    }
                    setButton.on("click", function(evt) {
                        evt.preventDefault();
                        if (container.hasClass('expanded')) {
                            container.removeClass('expanded');
                            contentRow.slideUp();
                            setTimeout(() => {
                                contentRow.empty()
                            }, 200)
                            object.elements.sets = {}
                        } else {
                            container.addClass('expanded');
                            populateSetList()
                            contentRow.slideDown();
                        }
                    })
                    const populateSetList = function () {
                        var setList = Object.keys(entry.sets)
                        setList.sort(function(A,B) {
                            return A.toLowerCase().localeCompare(B.toLowerCase());
                        });
                        setList.forEach(function(setName) {
                            var set = entry.sets[setName];
                            var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow);
                            var buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow);
                            var typeSwatches = {};
                            let enableButton;
                            if (set.types) {
                                set.types.forEach(function(t) {
                                    var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow);
                                    typeSwatches[t] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
                                    if (set.enabled) {
                                        var def = RED.nodes.getType(t);
                                        if (def && def.color) {
                                            typeSwatches[t].css({background:RED.utils.getNodeColor(t,def)});
                                            typeSwatches[t].css({border: "1px solid "+getContrastingBorder(typeSwatches[t].css('backgroundColor'))})
                                        }
                                    }
                                    $('<span>',{class:"red-ui-palette-module-type-node"}).text(t).appendTo(typeDiv);
                                })
                                enableButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').appendTo(buttonGroup);
                                enableButton.on("click", function(evt) {
                                    evt.preventDefault();
                                    if (object.setUseCount[setName] === 0) {
                                        var currentSet = RED.nodes.registry.getNodeSet(set.id);
                                        shade.show();
                                        var newState = !currentSet.enabled
                                        changeNodeState(set.id,newState,shade,function(xhr){
                                            if (xhr) {
                                                if (xhr.responseJSON) {
                                                    RED.notify(RED._('palette.editor.errors.'+(newState?'enable':'disable')+'Failed',{module: id,message:xhr.responseJSON.message}));
                                                }
                                            }
                                        });
                                    }
                                })

                                if (object.setUseCount[setName] > 0) {
                                    enableButton.text(RED._('palette.editor.inuse'));
                                    enableButton.addClass('disabled');
                                } else {
                                    enableButton.removeClass('disabled');
                                    if (set.enabled) {
                                        enableButton.text(RED._('palette.editor.disable'));
                                    } else {
                                        enableButton.text(RED._('palette.editor.enable'));
                                    }
                                }
                                setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);


                            }
                            if (set.plugins) {
                                set.plugins.forEach(function(p) {
                                    var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow);
                                    // typeSwatches[p.id] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
                                    $('<span><i class="fa fa-puzzle-piece" aria-hidden="true"></i>  </span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
                                    $('<span>',{class:"red-ui-palette-module-type-node"}).text(p.id).appendTo(typeDiv);
                                })
                            }

                            object.elements.sets[set.name] = {
                                setRow: setRow,
                                enableButton: enableButton,
                                swatches: typeSwatches
                            };
                        });
                    }
                    enableButton.on("click", function(evt) {
                        evt.preventDefault();
                        if (object.totalUseCount === 0) {
                            changeNodeState(entry.name,(container.hasClass('disabled')),shade,function(xhr){
                                if (xhr) {
                                    if (xhr.responseJSON) {
                                        RED.notify(RED._('palette.editor.errors.installFailed',{module: id,message:xhr.responseJSON.message}));
                                    }
                                }
                            });
                        }
                    })
                    refreshNodeModule(entry.name);
                } else {
                    $('<div>',{class:"red-ui-search-empty"}).text(RED._('search.empty')).appendTo(container);
                }
            }
        })
    }

    function createInstallTab(content) {
        const installTab = $('<div>',{class:"red-ui-palette-editor-tab", style: "display: none;"}).appendTo(content);
        editorTabs.addTab({
            id: 'install',
            label: RED._('palette.editor.tab-install'),
            content: installTab
        })

        const toolBar = $('<div>',{class:"red-ui-palette-editor-toolbar"}).appendTo(installTab);
        
        const searchDiv = $('<div>',{class:"red-ui-palette-search"}).appendTo(installTab);
        searchInput = $('<input type="text" data-i18n="[placeholder]palette.search"></input>')
            .appendTo(searchDiv)
            .searchBox({
                delay: 300,
                change: function() {
                    var searchTerm = $(this).val().trim().toLowerCase();
                    if (searchTerm.length > 0 || loadedList.length < 20) {
                        filteredList = loadedList.filter(function(m) {
                            return (m.index.indexOf(searchTerm) > -1);
                        }).map(function(f) { return {info:f}});
                        refreshFilteredItems();
                        searchInput.searchBox('count',filteredList.length+" / "+loadedList.length);
                    } else {
                        searchInput.searchBox('count',loadedList.length);
                        packageList.editableList('empty');
                        packageList.editableList('addItem',{count:loadedList.length});
                    }
                }
            });

        const catalogSelection = $('<select id="red-catalogue-filter-select">').appendTo(toolBar);
        catalogSelection.addClass('red-ui-palette-editor-catalogue-filter');
 
        const toolBarActions = $('<div>',{class:"red-ui-palette-editor-toolbar-actions"}).appendTo(toolBar);

        $('<span>').text(RED._("palette.editor.sort")+' ').appendTo(toolBarActions);
        const sortGroup = $('<span class="button-group"></span>').appendTo(toolBarActions);
        const sortRelevance = $('<a href="#" class="red-ui-palette-editor-install-sort-option red-ui-sidebar-header-button-toggle selected"><i class="fa fa-sort-amount-desc"></i></a>').appendTo(sortGroup);
        const sortAZ = $('<a href="#" class="red-ui-palette-editor-install-sort-option red-ui-sidebar-header-button-toggle"><i class="fa fa-sort-alpha-asc"></i></a>').appendTo(sortGroup);
        const sortRecent = $('<a href="#" class="red-ui-palette-editor-install-sort-option red-ui-sidebar-header-button-toggle"><i class="fa fa-calendar"></i></a>').appendTo(sortGroup);
        RED.popover.tooltip(sortRelevance,RED._("palette.editor.sortRelevance"));
        RED.popover.tooltip(sortAZ,RED._("palette.editor.sortAZ"));
        RED.popover.tooltip(sortRecent,RED._("palette.editor.sortRecent"));


        const sortOpts = [
            {button: sortRelevance, func: sortModulesRelevance},
            {button: sortAZ, func: sortModulesAZ},
            {button: sortRecent, func: sortModulesRecent}
        ]
        sortOpts.forEach(function(opt) {
            opt.button.on("click", function(e) {
                e.preventDefault();
                if ($(this).hasClass("selected")) {
                    return;
                }
                $(".red-ui-palette-editor-install-sort-option").removeClass("selected");
                $(this).addClass("selected");
                activeSort = opt.func;
                refreshFilteredItems();
            });
        });

        var refreshSpan = $('<span>').appendTo(toolBarActions);
        var refreshButton = $('<a href="#" class="red-ui-sidebar-header-button"><i class="fa fa-refresh"></i></a>').appendTo(refreshSpan);
        refreshButton.on("click", function(e) {
            e.preventDefault();
            loadedList = [];
            loadedIndex = {};
            initInstallTab();
        })
        RED.popover.tooltip(refreshButton,RED._("palette.editor.refresh"));

        packageList = $('<ol>').appendTo(installTab).editableList({
            class: "scrollable",
            addButton: false,
            scrollOnAdd: false,
            addItem: function(container,i,object) {
                if (object.count) {
                    $('<div>',{class:"red-ui-search-empty"}).text(RED._('palette.editor.moduleCount',{count:object.count})).appendTo(container);
                    return
                }
                if (object.more) {
                    container.addClass('red-ui-palette-module-more');
                    var moreRow = $('<div>',{class:"red-ui-palette-module-header palette-module"}).appendTo(container);
                    var moreLink = $('<a href="#"></a>').text(RED._('palette.editor.more',{count:object.more})).appendTo(moreRow);
                    moreLink.on("click", function(e) {
                        e.preventDefault();
                        packageList.editableList('removeItem',object);
                        for (var i=object.start;i<Math.min(object.start+10,object.start+object.more);i++) {
                            packageList.editableList('addItem',filteredList[i]);
                        }
                        if (object.more > 10) {
                            packageList.editableList('addItem',{start:object.start+10, more:object.more-10})
                        }
                    })
                    return;
                }
                if (object.info) {
                    var entry = object.info;
                    var headerRow = $('<div>',{class:"red-ui-palette-module-header"}).appendTo(container);
                    var titleRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-name"><i class="fa fa-cube"></i></div>').appendTo(headerRow);
                    $('<span>').text(entry.name||entry.id).appendTo(titleRow);
                    $('<a target="_blank" class="red-ui-palette-module-link"><i class="fa fa-external-link"></i></a>').attr('href',entry.url).appendTo(titleRow);
                    var descRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow);
                    $('<div>',{class:"red-ui-palette-module-description"}).text(entry.description).appendTo(descRow);
                    var metaRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow);
                    $('<span class="red-ui-palette-module-version"><i class="fa fa-tag"></i> '+entry.version+'</span>').appendTo(metaRow);
                    $('<span class="red-ui-palette-module-updated"><i class="fa fa-calendar"></i> '+formatUpdatedAt(entry.updated_at)+'</span>').appendTo(metaRow);
                    if (loadedCatalogs.length > 1) {
                        $('<span class="red-ui-palette-module-updated"><i class="fa fa-cubes"></i>' + (entry.catalog.name || entry.catalog.url) + '</span>').appendTo(metaRow);
                    }

                    var duplicateType = false;
                    if (entry.types && entry.types.length > 0) {

                        for (var i=0;i<entry.types.length;i++) {
                            var nodeset = RED.nodes.registry.getNodeSetForType(entry.types[i]);
                            if (nodeset) {
                                duplicateType = nodeset.module;
                                break;
                            }
                        }
                        // $('<div>',{class:"red-ui-palette-module-meta"}).text(entry.types.join(",")).appendTo(headerRow);
                    }

                    var buttonRow = $('<div>',{class:"red-ui-palette-module-meta"}).appendTo(headerRow);
                    var buttonGroup = $('<div>',{class:"red-ui-palette-module-button-group"}).appendTo(buttonRow);
                    var installButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').text(RED._('palette.editor.install')).appendTo(buttonGroup);
                    installButton.on("click", function(e) {
                        e.preventDefault();
                        if (!$(this).hasClass('disabled')) {
                            install(entry,container,function(xhr) {});
                        }
                    })
                    if (nodeEntries.hasOwnProperty(entry.id)) {
                        installButton.addClass('disabled');
                        installButton.text(RED._('palette.editor.installed'));
                    } else if (duplicateType) {
                        installButton.addClass('disabled');
                        installButton.text(RED._('palette.editor.conflict'));
                        RED.popover.create({
                            target:installButton,
                            content: RED._('palette.editor.conflictTip',{module:duplicateType}),
                            trigger:"hover",
                            direction:"bottom",
                            delay:{show:750,hide:50}
                        })
                    }

                    object.elements = {
                        installButton:installButton
                    }
                } else {
                    $('<div>',{class:"red-ui-search-empty"}).text(RED._('search.empty')).appendTo(container);
                }
            }
        });
        

        if (RED.settings.get('externalModules.palette.allowUpload', true) !== false) {
            var uploadSpan = $('<span class="button-group">').prependTo(toolBarActions);
            var uploadButton = $('<button type="button" class="red-ui-sidebar-header-button red-ui-palette-editor-upload-button"><label><i class="fa fa-upload"></i><form id="red-ui-palette-editor-upload-form" enctype="multipart/form-data"><input name="tarball" type="file" accept=".tgz"></label></button>').appendTo(uploadSpan);

            var uploadInput = uploadButton.find('input[type="file"]');
            uploadInput.on("change", function(evt) {
                if (this.files.length > 0) {
                    uploadFilenameLabel.text(this.files[0].name)
                    uploadToolbar.slideDown(200);
                }
            })

            var uploadToolbar = $('<div class="red-ui-palette-editor-upload"></div>').appendTo(installTab);
            var uploadForm = $('<div>').appendTo(uploadToolbar);
            var uploadFilename = $('<div class="placeholder-input"><i class="fa fa-upload"></i> </div>').appendTo(uploadForm);
            var uploadFilenameLabel = $('<span></span>').appendTo(uploadFilename);
            var uploadButtons = $('<div class="red-ui-palette-editor-upload-buttons"></div>').appendTo(uploadForm);
            $('<button class="editor-button"></button>').text(RED._("common.label.cancel")).appendTo(uploadButtons).on("click", function(evt) {
                evt.preventDefault();
                uploadToolbar.slideUp(200);
                uploadInput.val("");
            });
            $('<button class="editor-button primary"></button>').text(RED._("common.label.upload")).appendTo(uploadButtons).on("click", function(evt) {
                evt.preventDefault();

                var spinner = RED.utils.addSpinnerOverlay(uploadToolbar, true);
                var buttonRow = $('<div style="position: relative;bottom: calc(50% + 17px); padding-right: 10px;text-align: right;"></div>').appendTo(spinner);
                $('<button class="red-ui-button"></button>').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function(evt) {
                    evt.preventDefault();
                    RED.actions.invoke("core:show-event-log");
                });
                RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+uploadInput[0].files[0].name);

                var data = new FormData();
                data.append("tarball",uploadInput[0].files[0]);
                var filename = uploadInput[0].files[0].name;
                $.ajax({
                    url: 'nodes',
                    data: data,
                    cache: false,
                    contentType: false,
                    processData: false,
                    method: 'POST',
                }).always(function(data,textStatus,xhr) {
                    spinner.remove();
                    uploadInput.val("");
                    uploadToolbar.slideUp(200);
                }).fail(function(xhr,textStatus,err) {
                    var message = textStatus;
                    if (xhr.responseJSON) {
                        message = xhr.responseJSON.message;
                    }
                    var notification = RED.notify(RED._('palette.editor.errors.installFailed',{module: filename,message:message}),{
                        type: 'error',
                        modal: true,
                        fixed: true,
                        buttons: [
                            {
                                text: RED._("common.label.close"),
                                click: function() {
                                    notification.close();
                                }
                            },{
                                text: RED._("eventLog.view"),
                                click: function() {
                                    notification.close();
                                    RED.actions.invoke("core:show-event-log");
                                }
                            }
                        ]
                    });
                    uploadInput.val("");
                    uploadToolbar.slideUp(200);
                })
            })
            RED.popover.tooltip(uploadButton,RED._("palette.editor.upload"));
        }

        $('<div id="red-ui-palette-module-install-shade" class="red-ui-palette-module-shade hide"><div class="red-ui-palette-module-shade-status"></div><img src="red/images/spin.svg" class="red-ui-palette-spinner"/></div>').appendTo(installTab);
    }

    function update(entry,version,url,container,done) {
        if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
            done(new Error('Palette not editable'));
            return;
        }
        var notification = RED.notify(RED._("palette.editor.confirm.update.body",{module:entry.name}),{
            modal: true,
            fixed: true,
            buttons: [
                {
                    text: RED._("common.label.cancel"),
                    click: function() {
                        notification.close();
                    }
                },
                {
                    text: RED._("palette.editor.confirm.button.update"),
                    class: "primary red-ui-palette-module-install-confirm-button-update",
                    click: function() {
                        var spinner = RED.utils.addSpinnerOverlay(container, true);
                        var buttonRow = $('<div style="position: relative;bottom: calc(50% + 17px); padding-right: 10px;text-align: right;"></div>').appendTo(spinner);
                        $('<button class="red-ui-button"></button>').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function(evt) {
                            evt.preventDefault();
                            RED.actions.invoke("core:show-event-log");
                        });
                        RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.name+" "+version);
                        installNodeModule(entry.name,version,url,function(xhr) {
                            spinner.remove();
                            if (xhr) {
                                if (xhr.responseJSON) {
                                    var notification = RED.notify(RED._('palette.editor.errors.updateFailed',{module: entry.name,message:xhr.responseJSON.message}),{
                                        type: 'error',
                                        modal: true,
                                        fixed: true,
                                        buttons: [
                                            {
                                                text: RED._("common.label.close"),
                                                click: function() {
                                                    notification.close();
                                                }
                                            },{
                                                text: RED._("eventLog.view"),
                                                click: function() {
                                                    notification.close();
                                                    RED.actions.invoke("core:show-event-log");
                                                }
                                            }
                                        ]
                                    });
                                }
                            }
                            done(xhr);
                        });
                        notification.close();
                    }
                }
            ]
        })
    }
    function remove(entry,container,done) {
        if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
            done(new Error('Palette not editable'));
            return;
        }
        var notification = RED.notify(RED._("palette.editor.confirm.remove.body",{module:entry.name}),{
            modal: true,
            fixed: true,
            buttons: [
                {
                    text: RED._("common.label.cancel"),
                    click: function() {
                        notification.close();
                    }
                },
                {
                    text: RED._("palette.editor.confirm.button.remove"),
                    class: "primary red-ui-palette-module-install-confirm-button-remove",
                    click: function() {
                        var spinner = RED.utils.addSpinnerOverlay(container, true);
                        var buttonRow = $('<div style="position: relative;bottom: calc(50% + 17px); padding-right: 10px;text-align: right;"></div>').appendTo(spinner);
                        $('<button class="red-ui-button"></button>').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function(evt) {
                            evt.preventDefault();
                            RED.actions.invoke("core:show-event-log");
                        });
                        RED.eventLog.startEvent(RED._("palette.editor.confirm.button.remove")+" : "+entry.name);
                        removeNodeModule(entry.name, function(xhr) {
                            spinner.remove();
                            if (xhr) {
                                if (xhr.responseJSON) {
                                    var notification = RED.notify(RED._('palette.editor.errors.removeFailed',{module: entry.name,message:xhr.responseJSON.message}),{
                                        type: 'error',
                                        modal: true,
                                        fixed: true,
                                        buttons: [
                                            {
                                                text: RED._("common.label.close"),
                                                click: function() {
                                                    notification.close();
                                                }
                                            },{
                                                text: RED._("eventLog.view"),
                                                click: function() {
                                                    notification.close();
                                                    RED.actions.invoke("core:show-event-log");
                                                }
                                            }
                                        ]
                                    });                                
                                }
                            } else {
                                // dedicated list management for plugins
                                if (entry.plugin) {

                                    let e = nodeEntries[entry.name];
                                    if (e) {
                                        nodeList.editableList('removeItem', e);
                                        delete nodeEntries[entry.name];
                                    }

                                    // We assume that a plugin that implements onremove
                                    // cleans the editor accordingly of its left-overs.
                                    let found_onremove = true;

                                    let keys = Object.keys(entry.sets);
                                    keys.forEach((key) => {
                                        let set = entry.sets[key];
                                        for (let i=0; i<set.plugins?.length; i++) {
                                            let plgn = RED.plugins.getPlugin(set.plugins[i].id);
                                            if (plgn && plgn.onremove  && typeof plgn.onremove === 'function') {
                                                plgn.onremove();
                                            } else {
                                                if (plgn && plgn.onadd && typeof plgn.onadd === 'function') {
                                                    // if there's no 'onadd', there shouldn't be any left-overs
                                                    found_onremove = false;
                                                }
                                            }
                                        }
                                    });

                                    if (!found_onremove) {
                                        let removeNotify = RED.notify(RED._("palette.editor.confirm.removePlugin.body",{module:entry.name}),{
                                            modal: true,
                                            fixed: true,
                                            type: 'warning',
                                            buttons: [
                                                {
                                                    text: RED._("palette.editor.confirm.button.understood"),
                                                    class:"primary",
                                                    click: function(e) {
                                                        removeNotify.close();
                                                    }
                                                }
                                            ]
                                        });
                                    }
                                }        
                            }
                        })
                        notification.close();
                    }
                }
            ]
        })
    }
    function install(entry,container,done) {
        if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
            done(new Error('Palette not editable'));
            return;
        }
        var buttons = [
            {
                text: RED._("common.label.cancel"),
                click: function() {
                    notification.close();
                }
            }
        ];
        if (entry.url) {
            buttons.push({
                text: RED._("palette.editor.confirm.button.review"),
                class: "primary red-ui-palette-module-install-confirm-button-install",
                click: function() {
                    var url = entry.url||"";
                    window.open(url);
                }
            });
        }
        buttons.push({
            text: RED._("palette.editor.confirm.button.install"),
            class: "primary red-ui-palette-module-install-confirm-button-install",
            click: function() {
                var spinner = RED.utils.addSpinnerOverlay(container, true);

                var buttonRow = $('<div style="position: relative;bottom: calc(50% + 17px); padding-right: 10px;text-align: right;"></div>').appendTo(spinner);
                $('<button class="red-ui-button"></button>').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function(evt) {
                    evt.preventDefault();
                    RED.actions.invoke("core:show-event-log");
                });
                RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.id+" "+entry.version);
                installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr, textStatus,err) {
                    spinner.remove();
                     if (err && xhr.status === 504) {
                        var notification = RED.notify(RED._("palette.editor.errors.installTimeout"), {
                            modal: true,
                            fixed: true,
                            buttons: [
                                {
                                    text: RED._("common.label.close"),
                                    click: function() {
                                        notification.close();
                                    }
                                },{
                                    text: RED._("eventLog.view"),
                                    click: function() {
                                        notification.close();
                                        RED.actions.invoke("core:show-event-log");
                                    }
                                }
                            ]
                        })
                     } else if (xhr) {
                         if (xhr.responseJSON) {
                             var notification = RED.notify(RED._('palette.editor.errors.installFailed',{module: entry.id,message:xhr.responseJSON.message}),{
                                 type: 'error',
                                 modal: true,
                                 fixed: true,
                                 buttons: [
                                     {
                                         text: RED._("common.label.close"),
                                         click: function() {
                                             notification.close();
                                         }
                                     },{
                                         text: RED._("eventLog.view"),
                                         click: function() {
                                             notification.close();
                                             RED.actions.invoke("core:show-event-log");
                                         }
                                     }
                                 ]
                             });
                         }
                     }
                     done(xhr);
                });
                notification.close();
            }
        });

        var notification = RED.notify(RED._("palette.editor.confirm.install.body",{module:entry.id}),{
            modal: true,
            fixed: true,
            buttons: buttons
        })
    }

    return {
        init: init,
        install: install
    }
})();
;/**
 * 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));
            if (subflow){
                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<subflowNodes.length;i++) {
                isValid = subflowNodes[i].valid;
                hasChanged = subflowNodes[i].changed;
                if (isValid === undefined) {
                    isValid = validateNode(subflowNodes[i]);
                    hasChanged = subflowNodes[i].changed;
                }
                node.valid = node.valid && isValid;
                node.changed = node.changed || hasChanged;
            }
            var subflowInstances = RED.nodes.filterNodes({type:"subflow:"+node.id});
            var modifiedTabs = {};
            for (i=0;i<subflowInstances.length;i++) {
                subflowInstances[i].valid = node.valid;
                subflowInstances[i].changed = subflowInstances[i].changed || node.changed;
                subflowInstances[i].dirty = true;
                modifiedTabs[subflowInstances[i].z] = true;
            }
            Object.keys(modifiedTabs).forEach(function(id) {
                var subflow = RED.nodes.subflow(id);
                if (subflow) {
                    validateNode(subflow);
                }
            });
        }
        if (oldValue !== node.valid || oldChanged !== node.changed) {
            node.dirty = true;
            subflow = RED.nodes.subflow(node.z);
            if (subflow) {
                validateNode(subflow);
            }
        }
        return node.valid;
    }

    /**
     * Validate a node's properties for the given set of property definitions
     * @param node - the node being validated
     * @param definition - the node property definitions (either def.defaults or def.creds)
     * @param properties - the node property values to validate
     * @returns {array} an array of invalid properties
     */
    function validateNodeProperties(node, definition, properties) {
        var result = [];
        for (var prop in definition) {
            if (definition.hasOwnProperty(prop)) {
                var valid = validateNodeProperty(node, definition, prop, properties[prop]);
                if ((typeof valid) === "string") {
                    result.push(valid);
                } else if (Array.isArray(valid)) {
                    result = result.concat(valid)
                } else if(!valid) {
                    result.push(prop);
                }
            }
        }
        return result;
    }

    /**
     * Validate a individual node property
     * @param node - the node being validated
     * @param definition - the node property definitions (either def.defaults or def.creds)
     * @param property - the property name being validated
     * @param value - the property value being validated
     * @returns {boolean|string} whether the node proprty is valid. `true`: valid `false|String`: invalid
     */
    function validateNodeProperty(node,definition,property,value) {
        var valid = true;
        // Check for $(env-var) and consider it valid
        if (/^\$\([a-zA-Z_][a-zA-Z0-9_]*\)$/.test(value)) {
            return true;
        }
        // Check for ${env-var} and consider it valid
        if (/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/.test(value)) {
            return true;
        }
        var label = null;
        if (("label" in definition[property]) &&
            ((typeof definition[property].label) == "string")) {
            label = definition[property].label;
        }
        if ("required" in definition[property] && definition[property].required) {
            valid = value !== "";
            if (!valid && label) {
                return RED._("validator.errors.missing-required-prop", {
                    prop: label
                });
            }
        }
        if (valid && "validate" in definition[property]) {
            if (definition[property].hasOwnProperty("required") &&
                definition[property].required === false) {
                if (value === "") {
                    return true;
                }
            }
            try {
                var opt = {};
                if (label) {
                    opt.label = label;
                }
                valid = definition[property].validate.call(node,value, opt);
                // If the validator takes two arguments, it is a 3.x validator that
                // can return a String to mean 'invalid' and provide a reason
                if ((definition[property].validate.length === 2) &&
                    ((typeof valid) === "string") || Array.isArray(valid)) {
                    return valid;
                } else {
                    // Otherwise, a 2.x returns a truth-like/false-like value that
                    // we should cooerce to a boolean.
                    valid = !!valid
                }
            } catch(err) {
                console.log("Validation error:",node.type,node.id,"property: "+property,"value:",value,err);
                return RED._("validator.errors.validation-error", {
                    prop: property,
                    node: node.type,
                    id: node.id,
                    error: err.message
                });
            }
        } else if (valid) {
            if (definition[property].hasOwnProperty("required") && definition[property].required === false) {
                if (value === "") {
                    return true;
                }
            }
            // If the validator is not provided in node property => Check if the input has a validator
            if ("category" in node._def) {
                const isConfig = node._def.category === "config";
                const prefix = isConfig ? "node-config-input" : "node-input";
                const input = $("#"+prefix+"-"+property);
                const isTypedInput = input.length > 0 && input.next(".red-ui-typedInput-container").length > 0;
                if (isTypedInput) {
                    valid = input.typedInput("validate", { returnErrorMessage: true });
                    if (typeof valid === "string") {
                        return label ? label + ": " + valid : valid;
                    }
                }
            }
        }
        if (valid && definition[property].type && RED.nodes.getType(definition[property].type) && !("validate" in definition[property])) {
            if (!value || value == "_ADD_") {
                valid = definition[property].hasOwnProperty("required") && !definition[property].required;
                if (!valid && label) {
                    return RED._("validator.errors.missing-required-prop", {
                        prop: label
                    });
                }
            } else {
                var configNode = RED.nodes.node(value);
                if (configNode) {
                    if ((configNode.valid == null) || configNode.valid) {
                        return true;
                    }
                    if (label) {
                        return RED._("validator.errors.invalid-config", {
                            prop: label
                        });
                    }
                }
                else {
                    if (label) {
                        return RED._("validator.errors.missing-config", {
                            prop: label
                        });
                    }
                }
                return false;
            }
        }
        return valid;
    }

    function validateNodeEditor(node,prefix) {
        for (var prop in node._def.defaults) {
            if (node._def.defaults.hasOwnProperty(prop)) {
                validateNodeEditorProperty(node,node._def.defaults,prop,prefix);
            }
        }
        if (node._def.credentials) {
            for (prop in node._def.credentials) {
                if (node._def.credentials.hasOwnProperty(prop)) {
                    validateNodeEditorProperty(node,node._def.credentials,prop,prefix);
                }
            }
        }
    }

    function validateNodeEditorProperty(node,defaults,property,prefix) {
        var input = $("#"+prefix+"-"+property);
        if (input.length > 0) {
            var value = input.val();
            if (defaults[property].hasOwnProperty("format") && defaults[property].format !== "" && input[0].nodeName === "DIV") {
                value = input.text();
            } else if (input.attr("type") === "checkbox") {
                value = input.prop("checked");
            }
            var valid = validateNodeProperty(node, defaults, property,value);
            if (((typeof valid) === "string") || !valid) {
                input.addClass("input-error");
                input.next(".red-ui-typedInput-container").addClass("input-error");
                if ((typeof valid) === "string") {
                    var tooltip = input.data("tooltip");
                    if (tooltip) {
                        tooltip.setContent(valid);
                    }
                    else {
                        tooltip = RED.popover.tooltip(input, valid);
                        input.data("tooltip", tooltip);
                    }
                }
            } else {
                input.removeClass("input-error");
                input.next(".red-ui-typedInput-container").removeClass("input-error");
                var tooltip = input.data("tooltip");
                if (tooltip) {
                    input.data("tooltip", null);
                    tooltip.delete();
                }
            }
        }
    }

    /**
     * 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<removedLinks.length;l++) {
            RED.nodes.removeLink(removedLinks[l]);
        }
        return removedLinks;
    }

    /**
     * Create a config-node select box for this property
     * @param  {Object} node - the node being edited
     * @param {String} property - the name of the node property
     * @param {String} type - the type of the config-node
     * @param {"node-config-input"|"node-input"|"node-input-subflow-env"} prefix - the prefix to use in the input element ids
     * @param {Function} [filter] - a function to filter the list of config nodes
     * @param {Object} [env] - the environment variable object (only used for subflow env vars)
     */
    function prepareConfigNodeSelect(node, property, type, prefix, filter, env) {
        let nodeValue
        if (prefix === 'node-input-subflow-env') {
            nodeValue = env?.value
        } else {
            nodeValue = node[property]
        }

        const addBtnId = `${prefix}-btn-${property}-add`;
        const editBtnId = `${prefix}-btn-${property}-edit`;
        const selectId = prefix + '-' + property;
        const input = $(`#${selectId}`);
        if (input.length === 0) {
            return;
        }
        const attrStyle = input.attr('style');
        let newWidth;
        let m;
        if ((m = /(^|\s|;)width\s*:\s*([^;]+)/i.exec(attrStyle)) !== null) {
            newWidth = m[2].trim();
        } else {
            newWidth = "70%";
        }
        const outerWrap = $("<div></div>").css({
            width: newWidth,
            display: 'inline-flex'
        });
        const select = $('<select id="' + selectId + '"></select>').appendTo(outerWrap);
        input.replaceWith(outerWrap);
        // set the style attr directly - using width() on FF causes a value of 114%...
        select.css({
            'flex-grow': 1
        });

        updateConfigNodeSelect(property, type, nodeValue, prefix, filter);

        // create the edit button
        const editButton = $('<a id="' + editBtnId + '" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
            .css({ "margin-left": "10px" })
            .appendTo(outerWrap);

        RED.popover.tooltip(editButton, RED._('editor.editConfig', { type }));

        // create the add button
        const addButton = $('<a id="' + addBtnId + '" class="red-ui-button"><i class="fa fa-plus"></i></a>')
            .css({ "margin-left": "10px" })
            .appendTo(outerWrap);
        RED.popover.tooltip(addButton, RED._('editor.addNewConfig', { type }));

        const disableButton = function(button, disabled) {
            $(button).prop("disabled", !!disabled)
            $(button).toggleClass("disabled", !!disabled)
        };

        // add the click handler
        addButton.on("click", function (e) {
            if (addButton.prop("disabled")) { return }
            showEditConfigNodeDialog(property, type, "_ADD_", prefix, node);
            e.preventDefault();
        });
        editButton.on("click", function (e) {
            const selectedOpt = select.find(":selected")
            if (selectedOpt.data('env')) { return } // don't show the dialog for env vars items (MVP. Future enhancement: lookup the env, if present, show the associated edit dialog)
            if (editButton.prop("disabled")) { return }
            showEditConfigNodeDialog(property, type, selectedOpt.val(), prefix, node);
            e.preventDefault();
        });

        // dont permit the user to click the button if the selected option is an env var
        select.on("change", function () {
            const selectedOpt = select.find(":selected");
            const optionsLength = select.find("option").length;
            if (selectedOpt?.data('env')) {
                disableButton(addButton, true);
                disableButton(editButton, true);
            // disable the edit button if no options available or 'none' selected
            } else if (optionsLength === 1 || selectedOpt.val() === "_ADD_") {
                disableButton(addButton, false);
                disableButton(editButton, true);
            } else {
                disableButton(addButton, false);
                disableButton(editButton, false);
            }
        });

        // If the value is "", 'add new...' option if no config node available or 'none' option
        // Otherwise, it's a config node
        select.val(nodeValue || '_ADD_');
    }

    /**
     * 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 = $("<a>",{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,node);
            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) {
        $("#"+prefix+"-"+property).on("change keyup paste", 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 = $('<ul></ul>').appendTo(trayBody);
            var editorContent = $('<div></div>').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, node);
                        }
                        var editPaneContent = $('<div>', {class:"red-ui-tray-content"}).appendTo(editorContent).hide();
                        editPaneDefinition.create.call(editPaneDefinition,editPaneContent);
                        var editTab = {
                            id: id,
                            label: editPaneDefinition.label,
                            name: editPaneDefinition.name,
                            iconClass: editPaneDefinition.iconClass,
                            content: editPaneContent,
                            onchange: function() {
                                if (editPaneDefinition.show) {
                                    editPaneDefinition.show.call(editPaneDefinition)
                                }
                            }
                        }
                        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,definition.defaults[d].filter);
                                }
                            } 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<editStack.length;i++) {
            var node = editStack[i];
            label = node.type;
            if (node.type === 'group') {
                label = RED._("group.editGroup",{name:RED.utils.sanitize(node.name||node.id)});
            } else if (node.type === '_expression') {
                label = RED._("expressionEditor.title");
            } else if (node.type === '_js') {
                label = RED._("jsEditor.title");
            } else if (node.type === '_text') {
                label = RED._("textEditor.title");
            } else if (node.type === '_json') {
                label = RED._("jsonEditor.title");
            } else if (node.type === '_markdown') {
                label = RED._("markdownEditor.title");
            } else if (node.type === '_buffer') {
                label = RED._("bufferEditor.title");
            } else if (node.type === 'subflow') {
                label = RED._("subflow.editSubflow",{name:RED.utils.sanitize(node.name)})
            } else if (node.type.indexOf("subflow:")===0) {
                var subflow = RED.nodes.subflow(node.type.substring(8));
                label = RED._("subflow.editSubflowInstance",{name:RED.utils.sanitize(subflow.name)})
            } else if (node._def !== undefined) {
                if (typeof node._def.paletteLabel !== "undefined") {
                    try {
                        label = RED.utils.sanitize((typeof node._def.paletteLabel === "function" ? node._def.paletteLabel.call(node._def) : node._def.paletteLabel)||"");
                    } catch(err) {
                        console.log("Definition error: "+node.type+".paletteLabel",err);
                    }
                }
                if (i === editStack.length-1) {
                    if (RED.nodes.node(node.id)) {
                        label = RED._("editor.editNode",{type:RED.utils.sanitize(label)});
                    } else {
                        label = RED._("editor.addNewConfig",{type:RED.utils.sanitize(label)});
                    }
                }
            }
        }
        return label;
    }

    function isSameObj(env0, env1) {
        return (JSON.stringify(env0) === JSON.stringify(env1));
    }

    function buildEditForm(container,formId,type,ns,node) {
        var dialogForm = $('<form id="'+formId+'" class="form-horizontal" autocomplete="off"></form>').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<keys.length;i++) {
                var key = keys[i];
                if (key.indexOf(":") === -1) {
                    var prefix = "";
                    if (key.indexOf("[")===0) {
                        var parts = key.split("]");
                        prefix = parts[0]+"]";
                        key = parts[1];
                    }
                    keys[i] = prefix+ns+":"+key;
                }
            }
            $(this).attr("data-i18n",keys.join(";"));
        });

        // Add dummy fields to prevent 'Enter' submitting the form in some
        // cases, and also prevent browser auto-fill of password
        //  - the elements cannot be hidden otherwise Chrome will ignore them.
        //  - the elements need to have id's that imply password/username
        $('<span style="position: absolute; top: -2000px;"><input id="red-ui-trap-password" type="password"/></span>').prependTo(dialogForm);
        $('<span style="position: absolute; top: -2000px;"><input id="red-ui-trap-username"  type="text"/></span>').prependTo(dialogForm);
        $('<span style="position: absolute; top: -2000px;"><input id="red-ui-trap-user"  type="text"/></span>').prependTo(dialogForm);
        dialogForm.on("submit", function(e) { e.preventDefault();});
        dialogForm.find('input').attr("autocomplete","off");
        return dialogForm;
    }

    function handleEditSave(editing_node, editState) {
        var d;
        if (editing_node._def.oneditsave) {
            var oldValues = {};
            for (d in editing_node._def.defaults) {
                if (editing_node._def.defaults.hasOwnProperty(d)) {
                    if (typeof editing_node[d] === "string" || typeof editing_node[d] === "number") {
                        oldValues[d] = editing_node[d];
                    } else {
                        // Dont clone the group node `nodes` array
                        if (editing_node.type !== 'group' || d !== "nodes") {
                            oldValues[d] = $.extend(true,{},{v:editing_node[d]}).v;
                        }
                    }
                }
            }

            const oldCreds = {};
            if (editing_node._def.credentials) {
                for (const prop in editing_node._def.credentials) {
                    if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) {
                        if (editing_node._def.credentials[prop].type === 'password') {
                            oldCreds['has_' + prop] = editing_node.credentials['has_' + prop];
                        }
                        if (prop in editing_node.credentials) {
                            oldCreds[prop] = editing_node.credentials[prop];
                        }
                    }
                }
            }

            try {
                const rc = editing_node._def.oneditsave.call(editing_node);
                if (rc === true) {
                    editState.changed = true;
                } else if (typeof rc === 'object' && rc !== null ) {
                    if (rc.changed === true) {
                        editState.changed = true
                    }
                    if (Array.isArray(rc.history) && rc.history.length > 0) {
                        editState.history = rc.history
                    }
                }
            } catch(err) {
                console.warn("oneditsave",editing_node.id,editing_node.type,err.toString());
            }

            for (d in editing_node._def.defaults) {
                if (editing_node._def.defaults.hasOwnProperty(d)) {
                    if (oldValues[d] === null || typeof oldValues[d] === "string" || typeof oldValues[d] === "number") {
                        if (oldValues[d] !== editing_node[d]) {
                            editState.changes[d] = oldValues[d];
                            editState.changed = true;
                        }
                    } else if (editing_node.type !== 'group' || d !== "nodes") {
                        if (JSON.stringify(oldValues[d]) !== JSON.stringify(editing_node[d])) {
                            editState.changes[d] = oldValues[d];
                            editState.changed = true;
                        }
                    }
                }
            }

            if (editing_node._def.credentials) {
                for (const prop in editing_node._def.credentials) {
                    if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) {
                        if (oldCreds[prop] !== editing_node.credentials[prop]) {
                            if (editing_node.credentials[prop] === '__PWRD__') {
                                // The password may not exist in oldCreds
                                // The value '__PWRD__' means the password exists,
                                // so ignore this change
                                continue;
                            }
                            editState.changes.credentials = editState.changes.credentials || {};
                            editState.changes.credentials['has_' + prop] = oldCreds['has_' + prop];
                            editState.changes.credentials[prop] = oldCreds[prop];
                            editState.changed = true;
                        }
                    }
                }
            }
        }
    }

    function defaultConfigNodeSort(A,B) {
        // sort case insensitive so that `[env] node-name` items are at the top and
        // not mixed inbetween the the lower and upper case items
        return (A.__label__ || '').localeCompare((B.__label__ || ''), undefined, {sensitivity: 'base'})
    }

    function updateConfigNodeSelect(name,type,value,prefix,filter) {
        // if prefix is null, there is no config select to update
        if (prefix) {
            var button = $("#"+prefix+"-edit-"+name);
            if (button.length) {
                if (value) {
                    button.text(RED._("editor.configEdit"));
                } else {
                    button.text(RED._("editor.configAdd"));
                }
                $("#"+prefix+"-"+name).val(value);
            } else {
                let inclSubflowEnvvars = false
                var select = $("#"+prefix+"-"+name);
                var node_def = RED.nodes.getType(type);
                select.children().remove();

                var activeWorkspace = RED.nodes.workspace(RED.workspaces.active());
                if (!activeWorkspace) {
                    activeWorkspace = RED.nodes.subflow(RED.workspaces.active());
                    inclSubflowEnvvars = true
                }

                var configNodes = [];
                if (typeof filter !== 'function') {
                    filter = null;
                }
                RED.nodes.eachConfig(function(config) {
                    if (config.type == type && (!config.z || config.z === activeWorkspace.id)) {
                        if (!filter || filter.call(null,config)) {
                            var label = RED.utils.getNodeLabel(config,config.id);
                            config.__label__ = label+(config.d?" ["+RED._("workspace.disabled")+"]":"");
                            configNodes.push(config);
                        }
                    }
                });
 
                // as includeSubflowEnvvars is true, this is a subflow.
                // include any 'conf-types' env vars as a list of avaiable configs
                // in the config dropdown as `[env] node-name`
                if (inclSubflowEnvvars && activeWorkspace.env) {
                    const parentEnv = activeWorkspace.env.filter(env => env.ui?.type === 'conf-types' && env.type === type)
                    if (parentEnv && parentEnv.length > 0) {
                        const locale = RED.i18n.lang()
                        for (let i = 0; i < parentEnv.length; i++) {
                            const tenv = parentEnv[i]
                            const ui = tenv.ui || {}
                            const labels = ui.label || {}
                            const labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"] || tenv.name, locale)
                            const config = {
                                env: tenv,
                                id: '${' + tenv.name + '}',
                                type: type,
                                label: labelText,
                                __label__: `[env] ${labelText}`
                            }
                            configNodes.push(config)
                        }
                    }
                }

                var configSortFn = defaultConfigNodeSort;
                if (typeof node_def.sort == "function") {
                    configSortFn = node_def.sort;
                }
                try {
                    configNodes.sort(configSortFn);
                } catch(err) {
                    console.log("Definition error: "+node_def.type+".sort",err);
                }

                configNodes.forEach(function(cn) {
                    const option = $('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select);
                    if (cn.env) {
                        option.data('env', cn.env) // set a data attribute to indicate this is an env var (to inhibit the edit button)
                    }
                    delete cn.__label__;
                });

                var label = type;
                if (typeof node_def.paletteLabel !== "undefined") {
                    try {
                        label = RED.utils.sanitize((typeof node_def.paletteLabel === "function" ? node_def.paletteLabel.call(node_def) : node_def.paletteLabel)||type);
                    } catch(err) {
                        console.log("Definition error: "+type+".paletteLabel",err);
                    }
                }

                if (!configNodes.length) {
                    // Add 'add new...' option
                    select.append('<option value="_ADD_" selected>' + RED._("editor.addNewType", { type: label }) + '</option>');
                } else {
                    // Add 'none' option
                    select.append('<option value="_ADD_">' + RED._("editor.inputs.none") + '</option>');
                }

                window.setTimeout(function() { select.trigger("change");},50);
            }
        }
    }

    function getNodeCredentials(type, id, done) {
        var timeoutNotification;
        var intialTimeout = setTimeout(function() {
            timeoutNotification = RED.notify($('<p data-i18n="[prepend]editor.loadCredentials">  <img src="red/images/spin.svg"/></p>').i18n(),{fixed: true})
        },800);

        var dashedType = type.replace(/\s+/g, '-');
        var credentialsUrl = 'credentials/' + dashedType + "/" + id;

        $.ajax({
            url: credentialsUrl,
            dataType: 'json',
            success: function(data) {
                if (timeoutNotification) {
                    timeoutNotification.close();
                    timeoutNotification = null;
                }
                clearTimeout(intialTimeout);
                done(data);
            },
            error: function(jqXHR,status,error) {
                if (timeoutNotification) {
                    timeoutNotification.close();
                    timeoutNotification = null;
                }
                clearTimeout(intialTimeout);
                RED.notify(RED._("editor.errors.credentialLoadFailed"),"error")
                done(null);
            },
            timeout: 30000,
        });
    }

    function showEditDialog(node, defaultTab) {
        if (buildingEditDialog) { return }
        buildingEditDialog = true;
        if (node.z && RED.workspaces.isLocked(node.z)) { return }
        var editing_node = node;
        var removeInfoEditorOnClose = false;
        var skipInfoRefreshOnClose = false;
        var activeEditPanes = [];

        editStack.push(node);
        RED.view.state(RED.state.EDITING);
        var type = node.type;
        if (node.type.substring(0,8) == "subflow:") {
            type = "subflow";
        }

        var trayOptions = {
            title: getEditStackTitle(),
            buttons: [
                {
                    id: "node-dialog-delete",
                    class: 'leftButton',
                    text: RED._("common.label.delete"),
                    click: function() {
                        var startDirty = RED.nodes.dirty();
                        var removedNodes = [];
                        var removedLinks = [];
                        var removedEntities = RED.nodes.remove(editing_node.id);
                        removedNodes.push(editing_node);
                        removedNodes = removedNodes.concat(removedEntities.nodes);
                        removedLinks = removedLinks.concat(removedEntities.links);

                        var historyEvent = {
                            t:'delete',
                            nodes:removedNodes,
                            links:removedLinks,
                            changes: {},
                            dirty: startDirty
                        }

                        if (editing_node.g) {
                            const group = RED.nodes.group(editing_node.g);
                            // Don't use RED.group.removeFromGroup as that emits
                            // a change event on the node - but we're deleting it
                            const index = group?.nodes.indexOf(editing_node) ?? -1;
                            if (index > -1) {
                                group.nodes.splice(index, 1);
                                RED.group.markDirty(group);
                            }
                        }

                        RED.nodes.dirty(true);
                        RED.view.redraw(true);
                        RED.history.push(historyEvent);
                        RED.tray.close();
                    }
                },
                {
                    id: "node-dialog-cancel",
                    text: RED._("common.label.cancel"),
                    click: function() {
                        if (editing_node._def) {
                            if (editing_node._def.oneditcancel) {
                                try {
                                    editing_node._def.oneditcancel.call(editing_node);
                                } catch(err) {
                                    console.log("oneditcancel",editing_node.id,editing_node.type,err.toString());
                                }
                            }

                            for (var d in editing_node._def.defaults) {
                                if (editing_node._def.defaults.hasOwnProperty(d)) {
                                    var def = editing_node._def.defaults[d];
                                    if (def.type) {
                                        var configTypeDef = RED.nodes.getType(def.type);
                                        if (configTypeDef && configTypeDef.exclusive) {
                                            var input = $("#node-input-"+d).val()||"";
                                            if (input !== "" && !editing_node[d]) {
                                                // This node has an exclusive config node that
                                                // has just been added. As the user is cancelling
                                                // the edit, need to delete the just-added config
                                                // node so that it doesn't get orphaned.
                                                RED.nodes.remove(input);
                                            }
                                        }
                                    }
                                }

                            }
                        }
                        RED.tray.close();
                    }
                },
                {
                    id: "node-dialog-ok",
                    text: RED._("common.label.done"),
                    class: "primary",
                    click: function() {
                        var editState = {
                            changes: {},
                            changed: false,
                            outputMap: null
                        }
                        var wasDirty = RED.nodes.dirty();

                        handleEditSave(editing_node,editState)

                        activeEditPanes.forEach(function(pane) {
                            if (pane.apply) {
                                pane.apply.call(pane, editState);
                            }
                        })

                        var removedLinks = updateNodeProperties(editing_node, editState.outputMap);

                        if ($("#node-input-node-disabled").prop('checked')) {
                            if (node.d !== true) {
                                editState.changes.d = node.d;
                                editState.changed = true;
                                node.d = true;
                            }
                        } else {
                            if (node.d === true) {
                                editState.changes.d = node.d;
                                editState.changed = true;
                                delete node.d;
                            }
                        }

                        node.resize = true;

                        if (editState.changed) {
                            var wasChanged = editing_node.changed;
                            editing_node.changed = true;
                            RED.nodes.dirty(true);

                            var activeSubflow = RED.nodes.subflow(RED.workspaces.active());
                            var subflowInstances = null;
                            if (activeSubflow) {
                                subflowInstances = [];
                                RED.nodes.eachNode(function(n) {
                                    if (n.type == "subflow:"+RED.workspaces.active()) {
                                        subflowInstances.push({
                                            id:n.id,
                                            changed:n.changed
                                        });
                                        n.changed = true;
                                        n.dirty = true;
                                        updateNodeProperties(n);
                                    }
                                });
                            }
                            let historyEvent = {
                                t:'edit',
                                node:editing_node,
                                changes:editState.changes,
                                links:removedLinks,
                                dirty:wasDirty,
                                changed:wasChanged
                            };
                            if (editState.outputMap) {
                                historyEvent.outputMap = editState.outputMap;
                            }
                            if (subflowInstances) {
                                historyEvent.subflow = {
                                    instances:subflowInstances
                                }
                            }

                            if (editState.history) {
                                historyEvent = {
                                    t: 'multi',
                                    events: [ historyEvent, ...editState.history ],
                                    dirty: wasDirty
                                }
                            }

                            RED.history.push(historyEvent);
                        }
                        editing_node.dirty = true;
                        validateNode(editing_node);
                        RED.events.emit("editor:save",editing_node);
                        RED.events.emit("nodes:change",editing_node);
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                editTrayWidthCache[type] = dimensions.width;
                $(".red-ui-tray-content").height(dimensions.height - 50);
                var form = $(".red-ui-tray-content form").height(dimensions.height - 50 - 40);
                var size = {width:form.width(),height:form.height()};
                activeEditPanes.forEach(function(pane) {
                    if (pane.resize) {
                        pane.resize.call(pane, size);
                    }
                })
            },
            open: function(tray, done) {
                if (editing_node.hasOwnProperty('outputs')) {
                    editing_node.__outputs = editing_node.outputs;
                }

                var trayFooter = tray.find(".red-ui-tray-footer");
                var trayBody = tray.find('.red-ui-tray-body');
                trayBody.parent().css('overflow','hidden');

                var trayFooterLeft = $('<div class="red-ui-tray-footer-left"></div>').appendTo(trayFooter)

                var helpButton = $('<button type="button" class="red-ui-button"><i class="fa fa-book"></button>').on("click", function(evt) {
                    evt.preventDefault();
                    evt.stopPropagation();
                    RED.sidebar.help.show(editing_node.type);
                }).appendTo(trayFooterLeft);
                RED.popover.tooltip(helpButton, RED._("sidebar.help.showHelp"));

                $('<input id="node-input-node-disabled" type="checkbox">').prop("checked",!!node.d).appendTo(trayFooterLeft).toggleButton({
                    enabledIcon: "fa-circle-thin",
                    disabledIcon: "fa-ban",
                    invertState: true
                })

                var nodeEditPanes = ['editor-tab-properties'];
                if (/^subflow:/.test(node.type)) {
                    nodeEditPanes.push("editor-tab-envProperties");
                }
                if (!node._def.defaults || !node._def.defaults.hasOwnProperty('info'))  {
                    nodeEditPanes.push('editor-tab-description');
                    removeInfoEditorOnClose = true;
                    if(node.infoEditor) {
                        //As 'editor-tab-description' adds `node.infoEditor` store original & set a
                        //flag to NOT remove this property
                        node.infoEditor__orig = node.infoEditor;
                        delete node.infoEditor;
                        removeInfoEditorOnClose = false;
                    }
                }
                nodeEditPanes.push("editor-tab-appearance");

                prepareEditDialog(trayBody, nodeEditPanes,node,node._def,"node-input", defaultTab, function(_activeEditPanes) {
                    activeEditPanes = _activeEditPanes;
                    trayBody.i18n();
                    trayFooter.i18n();
                    buildingEditDialog = false;
                    done();
                });
            },
            close: function() {
                if (RED.view.state() != RED.state.IMPORT_DRAGGING) {
                    RED.view.state(RED.state.DEFAULT);
                }
                if (editing_node) {
                    if (editing_node.infoEditor__orig) {
                        editing_node.infoEditor = editing_node.infoEditor__orig;
                        delete editing_node.infoEditor__orig;
                    }
                    if (removeInfoEditorOnClose) {
                        delete editing_node.infoEditor;
                    }
                    if (!skipInfoRefreshOnClose) {
                        RED.sidebar.info.refresh(editing_node);
                    }
                }
                RED.workspaces.refresh();

                activeEditPanes.forEach(function(pane) {
                    if (pane.close) {
                        pane.close.call(pane);
                    }
                })

                RED.view.redraw(true);
                editStack.pop();
            },
            show: function() {
                if (editing_node) {
                    RED.sidebar.info.refresh(editing_node);
                    RED.sidebar.help.show(editing_node.type, false);
                    //ensure focused element is NOT body (for keyboard scope to operate correctly)
                    if (document.activeElement.tagName === 'BODY') {
                        $('#red-ui-editor-stack').trigger('focus')
                    }
                }
            }
        }
        if (editTrayWidthCache.hasOwnProperty(type)) {
            trayOptions.width = editTrayWidthCache[type];
        }

        if (type === 'subflow') {
            var id = editing_node.type.substring(8);
            trayOptions.buttons.unshift({
                class: 'leftButton',
                text: RED._("subflow.edit"),
                click: function() {
                    RED.workspaces.show(id);
                    skipInfoRefreshOnClose = true;
                    $("#node-dialog-ok").trigger("click");
                }
            });
        }

        RED.tray.show(trayOptions);
    }
    /**
     * name - name of the property that holds this config node
     * type - type of config node
     * id - id of config node to edit. _ADD_ for a new one
     * prefix - the input prefix of the parent property
     * editContext - the node that was being edited that triggered editing this node
     */
    function showEditConfigNodeDialog(name,type,id,prefix,editContext) {
        if (buildingEditDialog) { return }
        buildingEditDialog = true;
        var adding = (id == "_ADD_");
        var node_def = RED.nodes.getType(type);
        var editing_config_node = RED.nodes.node(id);
        var activeEditPanes = [];

        if (editing_config_node && editing_config_node.z && RED.workspaces.isLocked(editing_config_node.z)) { return }

        var configNodeScope = ""; // default to global
        var activeSubflow = RED.nodes.subflow(RED.workspaces.active());
        if (activeSubflow) {
            configNodeScope = activeSubflow.id;
        }
        if (editing_config_node == null) {
            editing_config_node = {
                id: RED.nodes.id(),
                _def: node_def,
                type: type,
                z: configNodeScope,
                users: []
            }
            for (var d in node_def.defaults) {
                if (node_def.defaults[d].value) {
                    editing_config_node[d] = JSON.parse(JSON.stringify(node_def.defaults[d].value));
                }
            }
            editing_config_node["_"] = node_def._;
        }
        editStack.push(editing_config_node);

        RED.view.state(RED.state.EDITING);
        var trayOptions = {
            title: getEditStackTitle(), //(adding?RED._("editor.addNewConfig", {type:type}):RED._("editor.editConfig", {type:type})),
            resize: function(dimensions) {
                $(".red-ui-tray-content").height(dimensions.height - 50);
                var form = $("#node-config-dialog-edit-form");
                var size = {width:form.width(),height:form.height()};
                activeEditPanes.forEach(function(pane) {
                    if (pane.resize) {
                        pane.resize.call(pane, size);
                    }
                })
            },
            open: function(tray, done) {
                var trayHeader = tray.find(".red-ui-tray-header");
                var trayBody = tray.find('.red-ui-tray-body');
                var trayFooter = tray.find(".red-ui-tray-footer");

                var trayFooterLeft = $('<div class="red-ui-tray-footer-left"></div>').appendTo(trayFooter)

                var helpButton = $('<button type="button" class="red-ui-button"><i class="fa fa-book"></button>').on("click", function(evt) {
                    evt.preventDefault();
                    evt.stopPropagation();
                    RED.sidebar.help.show(editing_config_node.type);
                }).appendTo(trayFooterLeft);
                RED.popover.tooltip(helpButton, RED._("sidebar.help.showHelp"));

                $('<input id="node-config-input-node-disabled" type="checkbox">').prop("checked",!!editing_config_node.d).appendTo(trayFooterLeft).toggleButton({
                    enabledIcon: "fa-circle-thin",
                    disabledIcon: "fa-ban",
                    invertState: true
                })

                if (node_def.hasUsers !== false) {
                    // $('<span><i class="fa fa-info-circle"></i> <span id="red-ui-editor-config-user-count"></span></span>').css("margin-left", "10px").appendTo(trayFooterLeft);
                    $('<button type="button" class="red-ui-button"><i class="fa fa-user"></i><span id="red-ui-editor-config-user-count"></span></button>').on('click', function() {
                        RED.sidebar.info.outliner.search('uses:'+editing_config_node.id)
                        RED.sidebar.info.show()
                    }).appendTo(trayFooterLeft);
                }
                trayFooter.append('<span class="red-ui-tray-footer-right"><span id="red-ui-editor-config-scope-warning" data-i18n="[title]editor.errors.scopeChange"><i class="fa fa-warning"></i></span><select id="red-ui-editor-config-scope"></select></span>');


                var nodeEditPanes = [ 'editor-tab-properties' ];
                if (!editing_config_node._def.defaults || !editing_config_node._def.defaults.hasOwnProperty('info'))  {
                    nodeEditPanes.push('editor-tab-description');
                }

                prepareEditDialog(trayBody, nodeEditPanes, editing_config_node, node_def, "node-config-input", null, function(_activeEditPanes) {
                    activeEditPanes = _activeEditPanes;
                    if (editing_config_node._def.exclusive) {
                        $("#red-ui-editor-config-scope").hide();
                    } else {
                        $("#red-ui-editor-config-scope").show();
                    }
                    $("#red-ui-editor-config-scope-warning").hide();

                    var nodeUserFlows = {};
                    editing_config_node.users.forEach(function(n) {
                        nodeUserFlows[n.z] = true;
                    });
                    var flowCount = Object.keys(nodeUserFlows).length;
                    var tabSelect = $("#red-ui-editor-config-scope").empty();
                    tabSelect.off("change");
                    tabSelect.append('<option value=""'+(!editing_config_node.z?" selected":"")+' data-i18n="sidebar.config.global"></option>');
                    tabSelect.append('<option disabled data-i18n="sidebar.config.flows"></option>');
                    RED.nodes.eachWorkspace(function(ws) {
                        var workspaceLabel = ws.label;
                        if (nodeUserFlows[ws.id]) {
                            workspaceLabel = "* "+workspaceLabel;
                        }
                        $('<option value="'+ws.id+'"'+(ws.id==editing_config_node.z?" selected":"")+'></option>').text(workspaceLabel).appendTo(tabSelect);
                    });
                    tabSelect.append('<option disabled data-i18n="sidebar.config.subflows"></option>');
                    RED.nodes.eachSubflow(function(ws) {
                        var workspaceLabel = ws.name;
                        if (nodeUserFlows[ws.id]) {
                            workspaceLabel = "* "+workspaceLabel;
                        }
                        $('<option value="'+ws.id+'"'+(ws.id==editing_config_node.z?" selected":"")+'></option>').text(workspaceLabel).appendTo(tabSelect);
                    });
                    if (flowCount > 0) {
                        tabSelect.on('change',function() {
                            var newScope = $(this).val();
                            if (newScope === '') {
                                // global scope - everyone can use it
                                $("#red-ui-editor-config-scope-warning").hide();
                            } else if (!nodeUserFlows[newScope] || flowCount > 1) {
                                // a user will loose access to it
                                $("#red-ui-editor-config-scope-warning").show();
                            } else {
                                $("#red-ui-editor-config-scope-warning").hide();
                            }
                        });
                    }
                    if (node_def.hasUsers !== false) {
                        $("#red-ui-editor-config-user-count").text(editing_config_node.users.length).parent().show();
                        RED.popover.tooltip($("#red-ui-editor-config-user-count").parent(), function() { return RED._('editor.nodesUse',{count:editing_config_node.users.length})});
                    }
                    trayBody.i18n();
                    trayFooter.i18n();
                    buildingEditDialog = false;
                    done();
                });
            },
            close: function() {
                RED.workspaces.refresh();

                activeEditPanes.forEach(function(pane) {
                    if (pane.close) {
                        pane.close.call(pane);
                    }
                })

                editStack.pop();
            },
            show: function() {
                if (editing_config_node) {
                    RED.sidebar.info.refresh(editing_config_node);
                    RED.sidebar.help.show(type, false);
                }
            }
        }
        trayOptions.buttons = [
            {
                id: "node-config-dialog-cancel",
                text: RED._("common.label.cancel"),
                click: function() {
                    var configType = type;
                    var configId = editing_config_node.id;
                    var configAdding = adding;
                    var configTypeDef = RED.nodes.getType(configType);
                    if (configTypeDef.oneditcancel) {
                        // TODO: what to pass as this to call
                        if (configTypeDef.oneditcancel) {
                            var cn = RED.nodes.node(configId);
                            if (cn) {
                                try {
                                    configTypeDef.oneditcancel.call(cn,false);
                                } catch(err) {
                                    console.log("oneditcancel",cn.id,cn.type,err.toString());
                                }
                            } else {
                                try {
                                    configTypeDef.oneditcancel.call({id:configId},true);
                                } catch(err) {
                                    console.log("oneditcancel",configId,configType,err.toString());
                                }
                            }
                        }
                    }
                    RED.tray.close();
                }
            },
            {
                id: "node-config-dialog-ok",
                text: adding ? RED._("editor.configAdd") : RED._("editor.configUpdate"),
                class: "primary",
                click: function() {
                    // TODO: Already defined
                    const configProperty = name;
                    const configType = type;
                    const configTypeDef = RED.nodes.getType(configType);

                    const wasChanged = editing_config_node.changed;
                    const editState = {
                        changes: {},
                        changed: false,
                        outputMap: null
                    };
                    
                    // Call `oneditsave` and search for changes
                    handleEditSave(editing_config_node, editState);

                    // Search for changes in the edit box (panes)
                    activeEditPanes.forEach(function (pane) {
                        if (pane.apply) {
                            pane.apply.call(pane, editState);
                        }
                    });

                    // TODO: Why?
                    editing_config_node.label = configTypeDef.label

                    // Check if disabled has changed
                    if ($("#node-config-input-node-disabled").prop('checked')) {
                        if (editing_config_node.d !== true) {
                            editState.changes.d = editing_config_node.d;
                            editState.changed = true;
                            editing_config_node.d = true;
                        }
                    } else {
                        if (editing_config_node.d === true) {
                            editState.changes.d = editing_config_node.d;
                            editState.changed = true;
                            delete editing_config_node.d;
                        }
                    }

                    // NOTE: must be undefined if no scope used
                    const scope = $("#red-ui-editor-config-scope").val() || undefined;

                    // Check if the scope has changed
                    if (editing_config_node.z !== scope) {
                        editState.changes.z = editing_config_node.z;
                        editState.changed = true;
                        editing_config_node.z = scope;
                    }

                    // Search for nodes that use this config node that are no longer
                    // in scope, so must be removed
                    const historyEvents = [];
                    if (scope) {
                        const newUsers = editing_config_node.users.filter(function (node) {
                            let keepNode = false;
                            let nodeModified = null;

                            for (const d in node._def.defaults) {
                                if (node._def.defaults.hasOwnProperty(d)) {
                                    if (node._def.defaults[d].type === editing_config_node.type) {
                                        if (node[d] === editing_config_node.id) {
                                            if (node.z === editing_config_node.z) {
                                                // The node is kept only if at least one property uses
                                                // this config node in the correct scope.
                                                keepNode = true;
                                            } else {
                                                if (!nodeModified) {
                                                    nodeModified = {
                                                        t: "edit",
                                                        node: node,
                                                        changes: { [d]: node[d] },
                                                        changed: node.changed,
                                                        dirty: node.dirty
                                                    };
                                                } else {
                                                    nodeModified.changes[d] = node[d];
                                                }

                                                // Remove the reference to the config node
                                                node[d] = "";
                                            }
                                        }
                                    }
                                }
                            }

                            // Add the node modified to the history
                            if (nodeModified) {
                                historyEvents.push(nodeModified);
                            }

                            // Mark as changed and revalidate this node
                            if (!keepNode) {
                                node.changed = true;
                                node.dirty = true;
                                validateNode(node);
                                RED.events.emit("nodes:change", node);
                            }

                            return keepNode;
                        });

                        // Check if users are changed
                        if (editing_config_node.users.length !== newUsers.length) {
                            editState.changes.users = editing_config_node.users;
                            editState.changed = true;
                            editing_config_node.users = newUsers;
                        }
                    }

                    if (editState.changed) {
                        // Set the congig node as changed
                        editing_config_node.changed = true;
                    }

                    // Now, validate the config node
                    validateNode(editing_config_node);

                    // And validate nodes using this config node too
                    const validatedNodes = new Set();
                    const userStack = editing_config_node.users.slice();

                    validatedNodes.add(editing_config_node.id);
                    while (userStack.length) {
                        const node = userStack.pop();
                        if (!validatedNodes.has(node.id)) {
                            validatedNodes.add(node.id);
                            if (node.users) {
                                userStack.push(...node.users);
                            }
                            validateNode(node);
                        }
                    }

                    let historyEvent = {
                        t: "edit",
                        node: editing_config_node,
                        changes: editState.changes,
                        changed: wasChanged,
                        dirty: RED.nodes.dirty()
                    };

                    if (historyEvents.length) {
                        // Need a multi events
                        historyEvent = {
                            t: "multi",
                            events: [historyEvent].concat(historyEvents),
                            dirty: historyEvent.dirty
                        };
                    }

                    if (!adding) {
                        // This event is triggered when the edit box is saved,
                        // regardless of whether there are any modifications.
                        RED.events.emit("editor:save", editing_config_node);
                    }

                    if (editState.changed) {
                        if (adding) {
                            RED.history.push({ t: "add", nodes: [editing_config_node.id], dirty: RED.nodes.dirty() });
                            // Add the new config node and trigger the `nodes:add` event
                            RED.nodes.add(editing_config_node);
                        } else {
                            RED.history.push(historyEvent);
                            RED.events.emit("nodes:change", editing_config_node);
                        }

                        RED.nodes.dirty(true);
                        RED.view.redraw(true);
                    }

                    RED.tray.close(function() {
                        var filter = null;
                        // when editing a config via subflow edit panel, the `configProperty` will not
                        // necessarily be a property of the editContext._def.defaults object
                        // Also, when editing via dashboard sidebar, editContext can be null
                        // so we need to guard both scenarios
                        if (editContext?._def) {
                            const isSubflow = (editContext._def.type === 'subflow' || /subflow:.*/.test(editContext._def.type))
                            if (editContext && !isSubflow && typeof editContext._def.defaults?.[configProperty]?.filter === 'function') {
                                filter = function(n) {
                                    return editContext._def.defaults[configProperty].filter.call(editContext,n);
                                }
                            }
                        }
                        updateConfigNodeSelect(configProperty,configType,editing_config_node.id,prefix,filter);
                    });
                }
            }
        ];

        if (!adding) {
            trayOptions.buttons.unshift({
                class: 'leftButton',
                text: RED._("editor.configDelete"), //'<i class="fa fa-trash"></i>',
                click: function() {
                    var configProperty = name;
                    var configId = editing_config_node.id;
                    var configType = type;
                    var configTypeDef = RED.nodes.getType(configType);

                    try {

                        if (configTypeDef.ondelete) {
                            // Deprecated: never documented but used by some early nodes
                            console.log("Deprecated API warning: config node type ",configType," has an ondelete function - should be oneditdelete");
                            configTypeDef.ondelete.call(editing_config_node);
                        }
                        if (configTypeDef.oneditdelete) {
                            configTypeDef.oneditdelete.call(editing_config_node);
                        }
                    } catch(err) {
                        console.log("oneditdelete",editing_config_node.id,editing_config_node.type,err.toString());
                    }

                    var historyEvent = {
                        t:'delete',
                        nodes:[editing_config_node],
                        changes: {},
                        dirty: RED.nodes.dirty()
                    }
                    for (var i=0;i<editing_config_node.users.length;i++) {
                        var user = editing_config_node.users[i];
                        historyEvent.changes[user.id] = {
                            changed: user.changed,
                            valid: user.valid
                        };
                        for (var d in user._def.defaults) {
                            if (user._def.defaults.hasOwnProperty(d) && user[d] == configId) {
                                historyEvent.changes[user.id][d] = configId
                                user[d] = "";
                                user.changed = true;
                                user.dirty = true;
                            }
                        }
                        validateNode(user);
                    }
                    RED.nodes.remove(configId);
                    RED.nodes.dirty(true);
                    RED.view.redraw(true);
                    RED.history.push(historyEvent);
                    RED.tray.close(function() {
                        var filter = null;
                        if (editContext && typeof editContext._def.defaults[configProperty]?.filter === 'function') {
                            filter = function(n) {
                                return editContext._def.defaults[configProperty].filter.call(editContext,n);
                            }
                        }
                        updateConfigNodeSelect(configProperty,configType,"",prefix,filter);
                    });
                }
            });
        }

        RED.tray.show(trayOptions);
    }

    function showEditSubflowDialog(subflow, defaultTab) {
        if (buildingEditDialog) { return }
        buildingEditDialog = true;
        var editing_node = subflow;
        var activeEditPanes = [];
        editStack.push(subflow);
        RED.view.state(RED.state.EDITING);
        var trayOptions = {
            title: getEditStackTitle(),
            buttons: [
                {
                    id: "node-dialog-cancel",
                    text: RED._("common.label.cancel"),
                    click: function() {
                        RED.tray.close();
                    }
                },
                {
                    id: "node-dialog-ok",
                    class: "primary",
                    text: RED._("common.label.done"),
                    click: function() {
                        var i;
                        var editState = {
                            changes: {},
                            changed: false,
                            outputMap: null
                        }
                        var wasDirty = RED.nodes.dirty();

                        activeEditPanes.forEach(function(pane) {
                            if (pane.apply) {
                                pane.apply.call(pane, editState);
                            }
                        })

                        var newName = $("#subflow-input-name").val();

                        if (newName != editing_node.name) {
                            editState.changes['name'] = editing_node.name;
                            editing_node.name = newName;
                            editState.changed = true;
                        }


                        var old_env = editing_node.env;
                        var new_env = RED.subflow.exportSubflowTemplateEnv($("#node-input-env-container").editableList("items"));

                        if (new_env && new_env.length > 0) {
                            new_env.forEach(function(prop) {
                                if (prop.type === "cred") {
                                    editing_node.credentials = editing_node.credentials || {_:{}};
                                    editing_node.credentials[prop.name] = prop.value;
                                    editing_node.credentials['has_'+prop.name] = (prop.value !== "");
                                    if (prop.value !== '__PWRD__') {
                                        editState.changed = true;
                                    }
                                    delete prop.value;
                                }
                            });
                        }
                        let envToRemove = new Set()
                        if (!isSameObj(old_env, new_env)) {
                            // Get a list of env properties that have been removed
                            // by comparing old_env and new_env
                            if (old_env) {
                                old_env.forEach(env => { envToRemove.add(env.name) })
                            }
                            if (new_env) {
                                new_env.forEach(env => {
                                    envToRemove.delete(env.name)
                                })
                            }
                            editState.changes.env = editing_node.env;
                            editing_node.env = new_env;
                            editState.changed = true;
                        }



                        if (editState.changed) {
                            let wasChanged = editing_node.changed;
                            editing_node.changed = true;
                            validateNode(editing_node);
                            let subflowInstances = [];
                            let instanceHistoryEvents = []
                            RED.nodes.eachNode(function(n) {
                                if (n.type == "subflow:"+editing_node.id) {
                                    subflowInstances.push({
                                        id:n.id,
                                        changed:n.changed
                                    })
                                    n._def.color = editing_node.color;
                                    n.changed = true;
                                    n.dirty = true;
                                    if (n.env) {
                                        const oldEnv = n.env
                                        const newEnv = []
                                        let envChanged = false
                                        n.env.forEach((env, index) => {
                                            if (envToRemove.has(env.name)) {
                                                envChanged = true
                                            } else {
                                                newEnv.push(env)
                                            }
                                        })
                                        if (envChanged) {
                                            instanceHistoryEvents.push({
                                                t: 'edit',
                                                node: n,
                                                changes: { env: oldEnv },
                                                dirty: n.dirty,
                                                changed: n.changed
                                            })
                                            n.env = newEnv
                                        }
                                    }
                                    updateNodeProperties(n);
                                    validateNode(n);
                                }
                            });
                            RED.events.emit("subflows:change",editing_node);
                            RED.nodes.dirty(true);
                            let historyEvent = {
                                t:'edit',
                                node:editing_node,
                                changes:editState.changes,
                                dirty:wasDirty,
                                changed:wasChanged,
                                subflow: {
                                    instances:subflowInstances
                                }
                            };
                            if (instanceHistoryEvents.length > 0) {
                                historyEvent = {
                                    t: 'multi',
                                    events: [ historyEvent, ...instanceHistoryEvents ],
                                    dirty: wasDirty
                                }
                            }
                            RED.history.push(historyEvent);
                        }
                        editing_node.dirty = true;
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                $(".red-ui-tray-content").height(dimensions.height - 50);

                var form = $(".red-ui-tray-content form").height(dimensions.height - 50 - 40);
                var size = {width:form.width(),height:form.height()};
                activeEditPanes.forEach(function(pane) {
                    if (pane.resize) {
                        pane.resize.call(pane, size);
                    }
                })
            },
            open: function(tray, done) {
                var trayFooter = tray.find(".red-ui-tray-footer");
                var trayFooterLeft = $("<div/>", {
                    class: "red-ui-tray-footer-left"
                }).appendTo(trayFooter)
                var trayBody = tray.find('.red-ui-tray-body');
                trayBody.parent().css('overflow','hidden');

                $('<span style="margin-left: 10px"><i class="fa fa-info-circle"></i> <i id="red-ui-editor-subflow-user-count"></i></span>').appendTo(trayFooterLeft);

                if (editing_node) {
                    RED.sidebar.info.refresh(editing_node);
                }

                var nodeEditPanes = [
                    'editor-tab-properties',
                    'editor-tab-subflow-module',
                    'editor-tab-description',
                    'editor-tab-appearance'
                ];

                prepareEditDialog(trayBody, nodeEditPanes, subflow, subflow._def, "node-input", defaultTab, function(_activeEditPanes) {
                    activeEditPanes = _activeEditPanes;
                    $("#subflow-input-name").val(subflow.name);
                    RED.text.bidi.prepareInput($("#subflow-input-name"));
                    trayBody.i18n();
                    trayFooter.i18n();
                    buildingEditDialog = false;
                    done();
                });
            },
            close: function() {
                if (RED.view.state() != RED.state.IMPORT_DRAGGING) {
                    RED.view.state(RED.state.DEFAULT);
                }
                RED.sidebar.info.refresh(editing_node);
                RED.workspaces.refresh();
                activeEditPanes.forEach(function(pane) {
                    if (pane.close) {
                        pane.close.call(pane);
                    }
                })
                editStack.pop();
                editing_node = null;
            },
            show: function() {
            }
        }
        RED.tray.show(trayOptions);
    }

    function showEditGroupDialog(group, defaultTab) {
        if (buildingEditDialog) { return }
        buildingEditDialog = true;
        if (group.z && RED.workspaces.isLocked(group.z)) { return }
        var editing_node = group;
        editStack.push(group);
        RED.view.state(RED.state.EDITING);
        var activeEditPanes = [];

        var trayOptions = {
            title: getEditStackTitle(),
            buttons: [
                {
                    id: "node-dialog-cancel",
                    text: RED._("common.label.cancel"),
                    click: function() {
                        RED.tray.close();
                    }
                },
                {
                    id: "node-dialog-ok",
                    class: "primary",
                    text: RED._("common.label.done"),
                    click: function() {
                        var editState = {
                            changes: {},
                            changed: false,
                            outputMap: null
                        }
                        var wasDirty = RED.nodes.dirty();

                        handleEditSave(editing_node,editState);

                        activeEditPanes.forEach(function(pane) {
                            if (pane.apply) {
                                pane.apply.call(pane, editState);
                            }
                        })

                        if (editState.changed) {
                            var wasChanged = editing_node.changed;
                            editing_node.changed = true;
                            RED.nodes.dirty(true);
                            var historyEvent = {
                                t:'edit',
                                node:editing_node,
                                changes:editState.changes,
                                dirty:wasDirty,
                                changed:wasChanged
                            };
                            RED.history.push(historyEvent);
                            RED.events.emit("groups:change",editing_node);
                        }
                        editing_node.dirty = true;
                        RED.tray.close();
                        RED.view.redraw(true);
                    }
                }
            ],
            resize: function(dimensions) {
                editTrayWidthCache['group'] = dimensions.width;
                $(".red-ui-tray-content").height(dimensions.height - 50);
                var form = $(".red-ui-tray-content form").height(dimensions.height - 50 - 40);
                var size = {width:form.width(),height:form.height()};
                activeEditPanes.forEach(function(pane) {
                    if (pane.resize) {
                        pane.resize.call(pane, size);
                    }
                })
            },
            open: function(tray, done) {
                var trayFooter = tray.find(".red-ui-tray-footer");
                var trayFooterLeft = $("<div/>", {
                    class: "red-ui-tray-footer-left"
                }).appendTo(trayFooter)
                var trayBody = tray.find('.red-ui-tray-body');
                trayBody.parent().css('overflow','hidden');

                var nodeEditPanes = [
                    'editor-tab-properties',
                    'editor-tab-envProperties',
                    'editor-tab-description'
                ];
                prepareEditDialog(trayBody, nodeEditPanes, group,group._def,"node-input", defaultTab, function(_activeEditPanes) {
                    activeEditPanes = _activeEditPanes;
                    trayBody.i18n();
                    buildingEditDialog = false;
                    done();
                });

            },
            close: function() {
                if (RED.view.state() != RED.state.IMPORT_DRAGGING) {
                    RED.view.state(RED.state.DEFAULT);
                }
                RED.sidebar.info.refresh(editing_node);
                activeEditPanes.forEach(function(pane) {
                    if (pane.close) {
                        pane.close.call(pane);
                    }
                })
                editStack.pop();
                editing_node = null;
            },
            show: function() {
            }
        }

        if (editTrayWidthCache.hasOwnProperty('group')) {
            trayOptions.width = editTrayWidthCache['group'];
        }
        RED.tray.show(trayOptions);
    }

    function showEditFlowDialog(workspace, defaultTab) {
        if (buildingEditDialog) { return }
        buildingEditDialog = true;
        var activeEditPanes = [];
        RED.view.state(RED.state.EDITING);
        var trayOptions = {
            title: RED._("workspace.editFlow",{name:RED.utils.sanitize(workspace.label)}),
            buttons: [
                {
                    id: "node-dialog-delete",
                    class: 'leftButton'+((RED.workspaces.count() === 1)?" disabled":""),
                    text: RED._("common.label.delete"), //'<i class="fa fa-trash"></i>',
                    click: function() {
                        RED.workspaces.delete(workspace);
                        RED.tray.close();
                    }
                },
                {
                    id: "node-dialog-cancel",
                    text: RED._("common.label.cancel"),
                    click: function() {
                        RED.tray.close();
                    }
                },
                {
                    id: "node-dialog-ok",
                    class: "primary",
                    text: RED._("common.label.done"),
                    click: function() {
                        var editState = {
                            changes: {},
                            changed: false,
                            outputMap: null
                        }
                        var wasDirty = RED.nodes.dirty();

                        activeEditPanes.forEach(function(pane) {
                            if (pane.apply) {
                                pane.apply.call(pane, editState);
                            }
                        })

                        var disabled = $("#node-input-disabled").prop("checked");
                        if (workspace.disabled !== disabled) {
                            editState.changes.disabled = workspace.disabled;
                            editState.changed = true;
                            workspace.disabled = disabled;

                            $("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!workspace.disabled);
                        }

                        var locked = $("#node-input-locked").prop("checked");
                        if (workspace.locked !== locked) {
                            editState.changes.locked = workspace.locked;
                            editState.changed = true;
                            workspace.locked = locked;
                            $("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-locked',!!workspace.locked);
                        }
                        if (editState.changed) {
                            var historyEvent = {
                                t: "edit",
                                changes: editState.changes,
                                node: workspace,
                                dirty: wasDirty
                            }
                            workspace.changed = true;
                            RED.history.push(historyEvent);
                            RED.nodes.dirty(true);
                            if (editState.changes.hasOwnProperty('disabled')) {
                                RED.nodes.eachNode(function(n) {
                                    if (n.z === workspace.id) {
                                        n.dirty = true;
                                    }
                                });
                                RED.view.redraw();
                            }
                            RED.workspaces.refresh();
                            RED.events.emit("flows:change",workspace);
                        }
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                $(".red-ui-tray-content").height(dimensions.height - 50);
                var form = $(".red-ui-tray-content form").height(dimensions.height - 50 - 40);
                var size = {width:form.width(),height:form.height()};
                activeEditPanes.forEach(function(pane) {
                    if (pane.resize) {
                        pane.resize.call(pane, size);
                    }
                })
            },
            open: function(tray, done) {
                var trayFooter = tray.find(".red-ui-tray-footer");
                var trayBody = tray.find('.red-ui-tray-body');
                trayBody.parent().css('overflow','hidden');
                var trayFooterLeft = $('<div class="red-ui-tray-footer-left"></div>').appendTo(trayFooter)
                var trayFooterRight = $('<div class="red-ui-tray-footer-right"></div>').appendTo(trayFooter)

                var nodeEditPanes = [
                    'editor-tab-flow-properties',
                    'editor-tab-envProperties'
                ];

                if (!workspace.hasOwnProperty("disabled")) {
                    workspace.disabled = false;
                }
                $('<input id="node-input-disabled" type="checkbox">').prop("checked",workspace.disabled).appendTo(trayFooterLeft).toggleButton({
                    enabledIcon: "fa-circle-thin",
                    disabledIcon: "fa-ban",
                    invertState: true
                })

                if (!workspace.hasOwnProperty("locked")) {
                    workspace.locked = false;
                }
                $('<input id="node-input-locked" type="checkbox">').prop("checked",workspace.locked).appendTo(trayFooterRight).toggleButton({
                    enabledLabel: RED._("common.label.unlocked"),
                    enabledIcon: "fa-unlock-alt",
                    disabledLabel: RED._("common.label.locked"),
                    disabledIcon: "fa-lock",
                    invertState: true
                })

                prepareEditDialog(trayBody, nodeEditPanes, workspace, {}, "node-input", defaultTab, function(_activeEditPanes) {
                    activeEditPanes = _activeEditPanes;
                    trayBody.i18n();
                    trayFooter.i18n();
                    buildingEditDialog = false;
                    done();
                });
            },
            close: function() {
                if (RED.view.state() != RED.state.IMPORT_DRAGGING) {
                    RED.view.state(RED.state.DEFAULT);
                }
                activeEditPanes.forEach(function(pane) {
                    if (pane.close) {
                        pane.close.call(pane);
                    }
                })
                var selection = RED.view.selection();
                if (!selection.nodes && !selection.links && workspace.id === RED.workspaces.active()) {
                    RED.sidebar.info.refresh(workspace);
                }
            }
        }
        RED.tray.show(trayOptions);
    }

    function showTypeEditor(type, options) {
        if (customEditTypes.hasOwnProperty(type)) {
            if (editStack.length > 0) {
                options.parent = editStack[editStack.length-1].id;
            }
            editStack.push({type:type});
            options.title = options.title || getEditStackTitle();
            options.onclose = function() {
                editStack.pop();
            }
            customEditTypes[type].show(options);
        } else {
            console.log("Unknown type editor:",type);
        }
    }

    /** Genrate a consistent but unique ID for saving and restoring the code editors view state */
    function generateViewStateId(source, thing, suffix) {
        try {
            thing = thing || {};
            const thingOptions = typeof thing.options === "object" ? thing.options : {};
            let stateId;
            if (thing.hasOwnProperty("stateId")) {
                stateId = thing.stateId
            } else if (thingOptions.hasOwnProperty("stateId")) {
                stateId = thing.stateId
            }
            if (stateId === false) { return false; }
            if (!stateId) {
                let id;
                const selection = RED.view.selection();
                if (source === "node" && thing.id) {
                    id = thing.id;
                } else if (selection.nodes && selection.nodes.length) {
                    id = selection.nodes[0].id;
                } else {
                    return false; //cant obtain Id.
                }
                //Use a string builder to build an ID
                const sb = [id];
                //get the index of the el - there may be more than one editor.
                const el = $(thing.element || thingOptions.element);
                if(el.length) {
                    sb.push(el.closest(".form-row").index());
                    sb.push(el.index());
                }
                if (source == "typedInput") {
                    sb.push(el.closest("li").index());//for when embeded in editable list
                    if (!suffix && thing.propertyType) { suffix = thing.propertyType }
                }
                stateId = sb.join("/");
            }
            if (stateId && suffix) { stateId += "/" + suffix; }
            return stateId;
        } catch (error) {
            return false;
        }
    }
    return {
        init: function() {
            if(window.ace) { window.ace.config.set('basePath', 'vendor/ace'); }
            RED.tray.init();
            RED.actions.add("core:confirm-edit-tray", function() {
                $(document.activeElement).blur();
                $("#node-dialog-ok").trigger("click");
                $("#node-config-dialog-ok").trigger("click");
            });
            RED.actions.add("core:cancel-edit-tray", function() {
                $(document.activeElement).blur();
                $("#node-dialog-cancel").trigger("click");
                $("#node-config-dialog-cancel").trigger("click");
            });
            RED.editor.codeEditor.init();
        },
        generateViewStateId: generateViewStateId,
        edit: showEditDialog,
        editConfig: showEditConfigNodeDialog,
        editFlow: showEditFlowDialog,
        editSubflow: showEditSubflowDialog,
        editGroup: showEditGroupDialog,
        editJavaScript: function(options) { showTypeEditor("_js",options) },
        editExpression: function(options) { showTypeEditor("_expression", options) },
        editJSON: function(options) { showTypeEditor("_json", options) },
        editMarkdown: function(options) { showTypeEditor("_markdown", options) },
        editText: function(options) {
            if (options.mode == "markdown") {
                showTypeEditor("_markdown", options)
            } else {
                showTypeEditor("_text", options)
            }
        },
        editBuffer: function(options) { showTypeEditor("_buffer", options) },
        getEditStack: function () { return [...editStack] },
        buildEditForm: buildEditForm,
        validateNode: validateNode,
        updateNodeProperties: updateNodeProperties,

        showIconPicker: function() { RED.editor.iconPicker.show.apply(null,arguments); },

        /**
         * Show a type editor.
         * @param {string} type - the type to display
         * @param {object} options - options for the editor
         * @function
         * @memberof RED.editor
         */
        showTypeEditor: showTypeEditor,

        /**
         * Register a type editor.
         * @param {string} type - the type name
         * @param {object} definition - the editor definition
         * @function
         * @memberof RED.editor
         */
        registerTypeEditor: function(type, definition) {
            customEditTypes[type] = definition;
        },

        /**
         * Create a editor ui component
         * @param {object} options - the editor options
         * @returs The code editor
         * @memberof RED.editor
         */
        createEditor: function(options) {
            return RED.editor.codeEditor.create(options);
        },
        get customEditTypes() {
            return customEditTypes;
        },

        registerEditPane: function(type, definition, filter) {
            if (filter) {
                filteredEditPanes[type] = filter
            }
            editPanes[type] = definition;
        },
        prepareConfigNodeSelect: prepareConfigNodeSelect,
    }
})();
;;(function() {

    RED.editor.registerEditPane("editor-tab-appearance", function(node) {
        return {
            label: RED._("editor-tab.appearance"),
            name: RED._("editor-tab.appearance"),
            iconClass: "fa fa-object-group",
            create: function(container) {
                this.content = container;
                buildAppearanceForm(this.content,node);

                if (node.type === 'subflow') {
                    this.defaultIcon = "node-red/subflow.svg";
                } else {
                    var iconPath = RED.utils.getDefaultNodeIcon(node._def,node);
                    this.defaultIcon = iconPath.module+"/"+iconPath.file;
                    if (node.icon && node.icon !== this.defaultIcon) {
                        this.isDefaultIcon = false;
                    } else {
                        this.isDefaultIcon = true;
                    }
                }
            },
            resize: function(size) {

            },
            close: function() {

            },
            show: function() {
                refreshLabelForm(this.content, node);
            },
            apply: function(editState) {
                if (updateLabels(node, editState.changes, editState.outputMap)) {
                    editState.changed = true;
                }
                if (!node._def.defaults || !node._def.defaults.hasOwnProperty("icon")) {
                    var icon = $("#red-ui-editor-node-icon").val()||"";
                    if (!this.isDefaultIcon) {
                        if ((node.icon && icon !== node.icon) || (!node.icon && icon !== "")) {
                            editState.changes.icon = node.icon;
                            node.icon = icon;
                            editState.changed = true;
                        }
                    } else {
                        if (icon !== "" && icon !== this.defaultIcon) {
                            editState.changes.icon = node.icon;
                            node.icon = icon;
                            editState.changed = true;
                        } else {
                            var iconPath = RED.utils.getDefaultNodeIcon(node._def, node);
                            var currentDefaultIcon = iconPath.module+"/"+iconPath.file;
                            if (this.defaultIcon !== currentDefaultIcon) {
                                editState.changes.icon = node.icon;
                                node.icon = currentDefaultIcon;
                                editState.changed = true;
                            }
                        }
                    }
                }
                if (node.type === "subflow") {
                    var newCategory = $("#subflow-appearance-input-category").val().trim();
                    if (newCategory === "_custom_") {
                        newCategory = $("#subflow-appearance-input-custom-category").val().trim();
                        if (newCategory === "") {
                            newCategory = node.category;
                        }
                    }
                    if (newCategory === 'subflows') {
                        newCategory = '';
                    }
                    if (newCategory != node.category) {
                        editState.changes['category'] = node.category;
                        node.category = newCategory;
                        editState.changed = true;
                    }

                    var oldColor = node.color;
                    var newColor =  $("#red-ui-editor-node-color").val();
                    if (oldColor !== newColor) {
                        editState.changes.color = node.color;
                        node.color = newColor;
                        editState.changed = true;
                        RED.utils.clearNodeColorCache();
                        if (node.type === "subflow") {
                            var nodeDefinition = RED.nodes.getType(
                                "subflow:" + node.id
                            );
                            nodeDefinition["color"] = newColor;
                        }
                    }



                }
                var showLabel = node._def.hasOwnProperty("showLabel")?node._def.showLabel:true;

                if (!$("#node-input-show-label").prop('checked')) {
                    // Not checked - hide label

                    if (showLabel) {
                        // Default to show label
                        if (node.l !== false) {
                            editState.changes.l = node.l;
                            editState.changed = true;
                        }
                        node.l = false;
                    } else {
                        // Node has showLabel:false (eg link nodes)
                        if (node.hasOwnProperty('l') && node.l) {
                            editState.changes.l = node.l;
                            editState.changed = true;
                        }
                        delete node.l;
                    }
                } else {
                    // Checked - show label
                    if (showLabel) {
                        // Default to show label
                        if (node.hasOwnProperty('l') && !node.l) {
                            editState.changes.l = node.l;
                            editState.changed = true;
                        }
                        delete node.l;
                    } else {
                        if (!node.l) {
                            editState.changes.l = node.l;
                            editState.changed = true;
                        }
                        node.l = true;
                    }
                }
            }
        };
    });

    function buildAppearanceForm(container,node) {
        var dialogForm = $('<form class="dialog-form form-horizontal" autocomplete="off"></form>').appendTo(container);

        var i,row;

        if (node.type === "subflow") {
            var categoryRow = $("<div/>", {
                class: "form-row"
            }).appendTo(dialogForm);
            $("<label/>", {
                for: "subflow-appearance-input-category",
                "data-i18n": "editor:subflow.category"
            }).appendTo(categoryRow);
            var categorySelector = $("<select/>", {
                id: "subflow-appearance-input-category"
            }).css({
                width: "250px"
            }).appendTo(categoryRow);
            $("<input/>", {
                type: "text",
                id: "subflow-appearance-input-custom-category"
            }).css({
                display: "none",
                "margin-left": "10px",
                width: "calc(100% - 250px)"
            }).appendTo(categoryRow);

            var categories = RED.palette.getCategories();
            categories.sort(function(A,B) {
                return A.label.localeCompare(B.label);
            });
            categories.forEach(function(cat) {
                categorySelector.append($("<option/>").val(cat.id).text(cat.label));
            });
            categorySelector.append($("<option/>").attr('disabled',true).text("---"));
            categorySelector.append($("<option/>").val("_custom_").text(RED._("palette.addCategory")));

            $("#subflow-appearance-input-category").on("change", function() {
                var val = $(this).val();
                if (val === "_custom_") {
                    $("#subflow-appearance-input-category").width(120);
                    $("#subflow-appearance-input-custom-category").show();
                } else {
                    $("#subflow-appearance-input-category").width(250);
                    $("#subflow-appearance-input-custom-category").hide();
                }
            });

            $("#subflow-appearance-input-category").val(node.category||"subflows");
            var userCount = 0;
            var subflowType = "subflow:"+node.id;

            // RED.nodes.eachNode(function(n) {
            //     if (n.type === subflowType) {
            //         userCount++;
            //     }
            // });
            $("#red-ui-editor-subflow-user-count")
                .text(RED._("subflow.subflowInstances", {count:node.instances.length})).show();
        }

        $('<div class="form-row">'+
            '<label for="node-input-show-label" data-i18n="editor.label"></label>'+
            '<span style="margin-right: 2px;"/>'+
            '<input type="checkbox" id="node-input-show-label"/>'+
        '</div>').appendTo(dialogForm);

        $("#node-input-show-label").toggleButton({
            enabledLabel: RED._("editor.show"),
            disabledLabel: RED._("editor.hide")
        });

        if (!node.hasOwnProperty("l")) {
            // Show label unless def.showLabel set to false
            node.l =  node._def.hasOwnProperty("showLabel")?node._def.showLabel:true;
        }
        $("#node-input-show-label").prop("checked",node.l).trigger("change");

        if (node.type === "subflow") {
            // subflow template can select its color
            var color = node.color ? node.color : "#DDAA99";
            var colorRow = $("<div/>", {
                class: "form-row"
            }).appendTo(dialogForm);
            $("<label/>").text(RED._("editor.color")).appendTo(colorRow);

            var recommendedColors = [
                "#DDAA99",
                "#3FADB5", "#87A980", "#A6BBCF",
                "#AAAA66", "#C0C0C0", "#C0DEED",
                "#C7E9C0", "#D7D7A0", "#D8BFD8",
                "#DAC4B4", "#DEB887", "#DEBD5C",
                "#E2D96E", "#E6E0F8", "#E7E7AE",
                "#E9967A", "#F3B567", "#FDD0A2",
                "#FDF0C2", "#FFAAAA", "#FFCC66",
                "#FFF0F0", "#FFFFFF"
            ];

            RED.editor.colorPicker.create({
                id: "red-ui-editor-node-color",
                value: color,
                defaultValue: "#DDAA99",
                palette: recommendedColors,
                sortPalette: function (a, b) {return a.l - b.l;}
            }).appendTo(colorRow);

            $("#red-ui-editor-node-color").on('change', function(ev) {
                // Horribly out of scope...
                var colour = $(this).val();
                nodeDiv.css('backgroundColor',colour);
                var borderColor = RED.utils.getDarkerColor(colour);
                if (borderColor !== colour) {
                    nodeDiv.css('border-color',borderColor);
                }
            });
        }


        // If a node has icon property in defaults, the icon of the node cannot be modified. (e.g, ui_button node in dashboard)
        if ((!node._def.defaults || !node._def.defaults.hasOwnProperty("icon"))) {
            var iconRow = $('<div class="form-row"></div>').appendTo(dialogForm);
            $('<label data-i18n="editor.settingIcon">').appendTo(iconRow);

            var iconButton = $('<button type="button" class="red-ui-button red-ui-editor-node-appearance-button">').appendTo(iconRow);
            $('<i class="fa fa-caret-down"></i>').appendTo(iconButton);
            var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(iconButton);
            var colour = RED.utils.getNodeColor(node.type, node._def);
            var icon_url = RED.utils.getNodeIcon(node._def,node);
            nodeDiv.css('backgroundColor',colour);
            var borderColor = RED.utils.getDarkerColor(colour);
            if (borderColor !== colour) {
                nodeDiv.css('border-color',borderColor);
            }

            var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
            RED.utils.createIconElement(icon_url, iconContainer, true);

            iconButton.on("click", function(e) {
                e.preventDefault();
                var iconPath;
                var icon = $("#red-ui-editor-node-icon").val()||"";
                if (icon) {
                    iconPath = RED.utils.separateIconPath(icon);
                } else {
                    iconPath = RED.utils.getDefaultNodeIcon(node._def, node);
                }
                var backgroundColor = RED.utils.getNodeColor(node.type, node._def);
                if (node.type === "subflow") {
                    backgroundColor = $("#red-ui-editor-node-color").val();
                }
                RED.editor.iconPicker.show(iconButton,backgroundColor,iconPath,false,function(newIcon) {
                    $("#red-ui-editor-node-icon").val(newIcon||"");
                    var icon_url = RED.utils.getNodeIcon(node._def,{type:node.type,icon:newIcon});
                    RED.utils.createIconElement(icon_url, iconContainer, true);
                });
            });

            RED.popover.tooltip(iconButton, function() {
                return $("#red-ui-editor-node-icon").val() || RED._("editor.default");
            });
            $('<input type="hidden" id="red-ui-editor-node-icon">').val(node.icon).appendTo(iconRow);
        }


        $('<div class="form-row"><span data-i18n="editor.portLabels"></span></div>').appendTo(dialogForm);

        var inputCount = node.inputs || node._def.inputs || 0;
        var outputCount = node.outputs || node._def.outputs || 0;
        if (node.type === 'subflow') {
            inputCount = node.in.length;
            outputCount = node.out.length;
        }

        var inputLabels = node.inputLabels || [];
        var outputLabels = node.outputLabels || [];

        var inputPlaceholder = node._def.inputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");
        var outputPlaceholder = node._def.outputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");

        $('<div class="form-row"><span style="margin-left: 50px;" data-i18n="editor.labelInputs"></span><div id="red-ui-editor-node-label-form-inputs"></div></div>').appendTo(dialogForm);
        var inputsDiv = $("#red-ui-editor-node-label-form-inputs");
        if (inputCount > 0) {
            for (i=0;i<inputCount;i++) {
                buildLabelRow("input",i,inputLabels[i],inputPlaceholder).appendTo(inputsDiv);
            }
        } else {
            buildLabelRow().appendTo(inputsDiv);
        }
        $('<div class="form-row"><span style="margin-left: 50px;" data-i18n="editor.labelOutputs"></span><div id="red-ui-editor-node-label-form-outputs"></div></div>').appendTo(dialogForm);
        var outputsDiv = $("#red-ui-editor-node-label-form-outputs");
        if (outputCount > 0) {
            for (i=0;i<outputCount;i++) {
                buildLabelRow("output",i,outputLabels[i],outputPlaceholder).appendTo(outputsDiv);
            }
        } else {
            buildLabelRow().appendTo(outputsDiv);
        }
    }

    function refreshLabelForm(container,node) {

        var inputPlaceholder = node._def.inputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");
        var outputPlaceholder = node._def.outputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");

        var inputsDiv = $("#red-ui-editor-node-label-form-inputs");
        var outputsDiv = $("#red-ui-editor-node-label-form-outputs");

        var inputCount;
        var formInputs = $("#node-input-inputs").val();
        if (formInputs === undefined) {
            if (node.type === 'subflow') {
                inputCount = node.in.length;
            } else {
                inputCount = node.inputs || node._def.inputs || 0;
            }
        } else {
            inputCount = Math.min(1,Math.max(0,parseInt(formInputs)));
            if (isNaN(inputCount)) {
                inputCount = 0;
            }
        }

        var children = inputsDiv.children();
        var childCount = children.length;
        if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) {
            childCount--;
        }

        if (childCount < inputCount) {
            if (childCount === 0) {
                // remove the 'none' placeholder
                $(children[0]).remove();
            }
            for (i = childCount;i<inputCount;i++) {
                buildLabelRow("input",i,"",inputPlaceholder).appendTo(inputsDiv);
            }
        } else if (childCount > inputCount) {
            for (i=inputCount;i<childCount;i++) {
                $(children[i]).remove();
            }
            if (inputCount === 0) {
                buildLabelRow().appendTo(inputsDiv);
            }
        }

        var outputCount;
        var i;
        var formOutputs = $("#node-input-outputs").val();

        if (formOutputs === undefined) {
            if (node.type === 'subflow') {
                outputCount = node.out.length;
            } else {
                inputCount = node.outputs || node._def.outputs || 0;
            }
        } else if (isNaN(formOutputs)) {
            var outputMap = JSON.parse(formOutputs);
            var keys = Object.keys(outputMap);
            children = outputsDiv.children();
            childCount = children.length;
            if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) {
                childCount--;
            }

            outputCount = 0;
            var rows = [];
            keys.forEach(function(p) {
                var row = $("#red-ui-editor-node-label-form-output-"+p).parent();
                if (row.length === 0 && outputMap[p] !== -1) {
                    if (childCount === 0) {
                        $(children[0]).remove();
                        childCount = -1;
                    }
                    row = buildLabelRow("output",p,"",outputPlaceholder);
                } else {
                    row.detach();
                }
                if (outputMap[p] !== -1) {
                    outputCount++;
                    rows.push({i:parseInt(outputMap[p]),r:row});
                }
            });
            rows.sort(function(A,B) {
                return A.i-B.i;
            });
            rows.forEach(function(r,i) {
                r.r.find("label").text((i+1)+".");
                r.r.appendTo(outputsDiv);
            });
            if (rows.length === 0) {
                buildLabelRow("output",i,"").appendTo(outputsDiv);
            } else {

            }
        } else {
            outputCount = Math.max(0,parseInt(formOutputs));
        }
        children = outputsDiv.children();
        childCount = children.length;
        if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) {
            childCount--;
        }
        if (childCount < outputCount) {
            if (childCount === 0) {
                // remove the 'none' placeholder
                $(children[0]).remove();
            }
            for (i = childCount;i<outputCount;i++) {
                buildLabelRow("output",i,"").appendTo(outputsDiv);
            }
        } else if (childCount > outputCount) {
            for (i=outputCount;i<childCount;i++) {
                $(children[i]).remove();
            }
            if (outputCount === 0) {
                buildLabelRow().appendTo(outputsDiv);
            }
        }
    }

    function buildLabelRow(type, index, value, placeHolder) {
        var result = $('<div>',{class:"red-ui-editor-node-label-form-row"});
        if (type === undefined) {
            $('<span>').text(RED._("editor.noDefaultLabel")).appendTo(result);
            result.addClass("red-ui-editor-node-label-form-none");
        } else {
            result.addClass("");
            var id = "red-ui-editor-node-label-form-"+type+"-"+index;
            $('<label>',{for:id}).text((index+1)+".").appendTo(result);
            var input = $('<input>',{type:"text",id:id, placeholder: placeHolder}).val(value).appendTo(result);
            var clear = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-times"></i></button>').appendTo(result);
            clear.on("click", function(evt) {
                evt.preventDefault();
                input.val("");
            });
        }
        return result;
    }

    function updateLabels(node, changes, outputMap) {
        var inputLabels = $("#red-ui-editor-node-label-form-inputs").children().find("input");
        var outputLabels = $("#red-ui-editor-node-label-form-outputs").children().find("input");

        var hasNonBlankLabel = false;
        var changed = false;
        var newValue = inputLabels.map(function() {
            var v = $(this).val();
            hasNonBlankLabel = hasNonBlankLabel || v!== "";
            return v;
        }).toArray().slice(0,node.inputs);
        if ((node.inputLabels === undefined && hasNonBlankLabel) ||
            (node.inputLabels !== undefined && JSON.stringify(newValue) !== JSON.stringify(node.inputLabels))) {
            changes.inputLabels = node.inputLabels;
            node.inputLabels = newValue;
            changed = true;
        }
        hasNonBlankLabel = false;
        newValue = new Array(node.outputs);
        outputLabels.each(function() {
            var index = $(this).attr('id').substring("red-ui-editor-node-label-form-output-".length);
            if (outputMap && outputMap.hasOwnProperty(index)) {
                index = parseInt(outputMap[index]);
                if (index === -1) {
                    return;
                }
            }
            var v = $(this).val();
            hasNonBlankLabel = hasNonBlankLabel || v!== "";

            // mark changed output port labels as dirty
            if (node.type === "subflow" && (!node.outputLabels || node.outputLabels[index] !== v)) {
                node.out[index].dirty = true;
            }

            newValue[index] = v;
        });

        if ((node.outputLabels === undefined && hasNonBlankLabel) ||
            (node.outputLabels !== undefined && JSON.stringify(newValue) !== JSON.stringify(node.outputLabels))) {
            changes.outputLabels = node.outputLabels;
            node.outputLabels = newValue;
            changed = true;

            // trigger redraw of dirty port labels
            if (node.type === "subflow") {
                RED.view.redraw();
            }

        }
        return changed;
    }


})();
;;(function() {

    RED.editor.registerEditPane("editor-tab-description", function(node) {
        return {
            label: RED._("editor-tab.description"),
            name: RED._("editor-tab.description"),
            iconClass: "fa fa-file-text-o",

            create: function(container) {
                this.editor = buildDescriptionForm(container,node);
            },
            resize: function(size) {
                this.editor.resize();
            },
            close: function() {
                this.editor.destroy();
                this.editor = null;
            },
            show: function() {
                this.editor.focus();
            },
            apply: function(editState) {
                var oldInfo = node.info;
                var newInfo = this.editor.getValue();
                if (!!oldInfo) {
                    // Has existing info property
                    if (newInfo.trim() === "") {
                        // New value is blank - remove the property
                        editState.changed = true;
                        editState.changes.info = oldInfo;
                        delete node.info;
                    } else if (newInfo !== oldInfo) {
                        // New value is different
                        editState.changed = true;
                        editState.changes.info = oldInfo;
                        node.info = newInfo;
                    }
                } else {
                    // No existing info
                    if (newInfo.trim() !== "") {
                        // New value is not blank
                        editState.changed = true;
                        editState.changes.info = undefined;
                        node.info = newInfo;
                    }
                }
            }
        }
    });

    function buildDescriptionForm(container,node) {
        var dialogForm = $('<form class="dialog-form form-horizontal" autocomplete="off"></form>').appendTo(container);
        var toolbarRow = $('<div></div>').appendTo(dialogForm);
        var row = $('<div class="form-row node-text-editor-row" style="position:relative; padding-top: 4px; height: 100%"></div>').appendTo(dialogForm);
        var editorId = "node-info-input-info-editor-"+Math.floor(1000*Math.random());
        $('<div style="height: 100%" class="node-text-editor" id="'+editorId+'" ></div>').appendTo(row);
        var nodeInfoEditor = RED.editor.createEditor({
            id: editorId,
            mode: 'ace/mode/markdown',
            stateId: RED.editor.generateViewStateId("node", node, "nodeinfo"),
            value: node.info || ""
        });
        node.infoEditor = nodeInfoEditor;
        return nodeInfoEditor;
    }

})();
;;(function() {

    RED.editor.registerEditPane("editor-tab-envProperties", function(node) {
        return {
            label: RED._("editor-tab.envProperties"),
            name: RED._("editor-tab.envProperties"),
            iconClass: "fa fa-list",
            create: function(container) {
                var form = $('<form class="dialog-form form-horizontal"></form>').appendTo(container);
                var listContainer = $('<div class="form-row node-input-env-container-row"></div>').appendTo(form);
                this.list = $('<ol></ol>').appendTo(listContainer);
                RED.editor.envVarList.create(this.list, node);
            },
            resize: function(size) {
                this.list.editableList('height',size.height);
            },
            close: function() {

            },
            apply: function(editState) {
                var old_env = node.env;
                var new_env = [];

                if (/^subflow:/.test(node.type)) {
                    // Get the list of environment variables from the node properties
                    new_env = RED.subflow.exportSubflowInstanceEnv(node);
                }

                if (old_env && old_env.length) {
                    old_env.forEach(function (prop) {
                        if (prop.type === "conf-type" && prop.value) {
                            const stillInUse = new_env?.some((p) => p.type === "conf-type" && p.name === prop.name && p.value === prop.value);
                            if (!stillInUse) {
                                // Remove the node from the config node users
                                // Only for empty value or modified
                                const configNode = RED.nodes.node(prop.value);
                                if (configNode) {
                                    if (configNode.users.indexOf(node) !== -1) {
                                        configNode.users.splice(configNode.users.indexOf(node), 1);
                                        RED.events.emit('nodes:change', configNode)
                                    }
                                }
                            }
                        }
                    });
                }

                // Get the values from the Properties table tab
                var items = this.list.editableList('items');
                items.each(function (i,el) {
                    var data = el.data('data');
                    var item;
                    if (data.nameField && data.valueField) {
                        item = {
                            name: data.nameField.val(),
                            value: data.valueField.typedInput("value"),
                            type: data.valueField.typedInput("type")
                        }
                        if (item.name.trim() !== "") {
                            new_env.push(item);
                        }
                    }
                });

                if (new_env && new_env.length > 0) {
                    new_env.forEach(function(prop) {
                        if (prop.type === "cred") {
                            node.credentials = node.credentials || {_:{}};
                            node.credentials[prop.name] = prop.value;
                            node.credentials['has_'+prop.name] = (prop.value !== "");
                            if (prop.value !== '__PWRD__') {
                                editState.changed = true;
                            }
                            delete prop.value;
                        } else if (prop.type === "conf-type" && prop.value) {  
                            const configNode = RED.nodes.node(prop.value);
                            if (configNode) {
                                if (configNode.users.indexOf(node) === -1) {
                                    // Add the node to the config node users
                                    configNode.users.push(node);
                                    RED.events.emit('nodes:change', configNode);
                                }
                            }
                        }
                    });
                }
                if (!old_env && new_env.length === 0) {
                    delete node.env;
                } else if (!isSameObj(old_env, new_env)) {
                    editState.changes.env = node.env;
                    if (new_env.length === 0) {
                        delete node.env;
                    } else {
                        node.env = new_env;
                    }
                    editState.changed = true;
                }
            }
        }
    });
    function isSameObj(env0, env1) {
        return (JSON.stringify(env0) === JSON.stringify(env1));
    }
})();
;;(function() {

    RED.editor.registerEditPane("editor-tab-flow-properties", function(node) {
        return {
            label: RED._("editor-tab.properties"),
            name: RED._("editor-tab.properties"),
            iconClass: "fa fa-cog",
            create: function(container) {
                var dialogForm = $('<form id="dialog-form" class="form-horizontal"></form>').appendTo(container);
                $('<div class="form-row">'+
                  '<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>'+
                  '<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">'+
                  '</div>').appendTo(dialogForm);

                var row = $('<div class="form-row node-text-editor-row">'+
                            '<label for="node-input-info" data-i18n="editor:workspace.info" style="width:300px;"></label>'+
                            '<div style="min-height:150px;" class="node-text-editor" id="node-input-info"></div>'+
                            '</div>').appendTo(dialogForm);
                this.tabflowEditor = RED.editor.createEditor({
                    id: 'node-input-info',
                    mode: 'ace/mode/markdown',
                    value: ""
                });

                $('<input type="text" style="display: none;" />').prependTo(dialogForm);
                dialogForm.on("submit", function(e) { e.preventDefault();});

                $("#node-input-name").val(node.label);
                RED.text.bidi.prepareInput($("#node-input-name"));
                this.tabflowEditor.getSession().setValue(node.info || "", -1);
            },
            resize: function(size) {
                $("#node-input-info").css("height", (size.height-70)+"px");
                this.tabflowEditor.resize();
            },
            close: function() {
                this.tabflowEditor.destroy();
            },
            apply: function(editState) {
                var label = $( "#node-input-name" ).val();

                if (node.label != label) {
                    editState.changes.label = node.label;
                    editState.changed = true;
                    node.label = label;
                }

                var info = this.tabflowEditor.getValue();
                if (node.info !== info) {
                    editState.changes.info = node.info;
                    editState.changed = true;
                    node.info = info;
                }
                $("#red-ui-tab-"+(node.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!node.disabled);
            }
        }
    });
})();
;;(function() {

    RED.editor.registerEditPane("editor-tab-properties", function(node) {
        return {
            label: RED._("editor-tab.properties"),
            name: RED._("editor-tab.properties"),
            iconClass: "fa fa-cog",
            create: function(container) {

                var nodeType = node.type;
                if (node.type === "subflow") {
                    nodeType = "subflow-template";
                } else if (node.type.substring(0,8) == "subflow:") {
                    nodeType = "subflow";
                }

                var i18nNamespace;
                if (node._def.set.module === "node-red") {
                    i18nNamespace = "node-red";
                } else {
                    i18nNamespace = node._def.set.id;
                }

                var formStyle = "dialog-form";
                this.inputClass = "node-input";
                if (node._def.category === "config" && nodeType !== "group") {
                    this.inputClass = "node-config-input";
                    formStyle = "node-config-dialog-edit-form";
                }
                RED.editor.buildEditForm(container,formStyle,nodeType,i18nNamespace,node);
            },
            resize: function(size) {
                if (node && node._def.oneditresize) {
                    try {
                        node._def.oneditresize.call(node,size);
                    } catch(err) {
                        console.log("oneditresize",node.id,node.type,err.toString());
                    }
                }
            },
            close: function() {

            },
            apply: function(editState) {
                var newValue;
                var d;
                // If the node is a subflow, the node's properties (exepts name) are saved by `envProperties`
                if (node._def.defaults) {
                    for (d in node._def.defaults) {
                        if (node._def.defaults.hasOwnProperty(d)) {
                            var input = $("#"+this.inputClass+"-"+d);
                            if (input.attr('type') === "checkbox") {
                                newValue = input.prop('checked');
                            } else if (input.prop("nodeName") === "select" && input.attr("multiple") === "multiple") {
                                // An empty select-multiple box returns null.
                                // Need to treat that as an empty array.
                                newValue = input.val();
                                if (newValue == null) {
                                    newValue = [];
                                }
                            } else if ("format" in node._def.defaults[d] && node._def.defaults[d].format !== "" && input[0].nodeName === "DIV") {
                                newValue = input.text();
                            } else {
                                newValue = input.val();
                            }
                            if (newValue != null) {
                                if (d === "outputs") {
                                    if  (newValue.trim() === "") {
                                        continue;
                                    }
                                    if (isNaN(newValue)) {
                                        editState.outputMap = JSON.parse(newValue);
                                        var outputCount = 0;
                                        var outputsChanged = false;
                                        var keys = Object.keys(editState.outputMap);
                                        keys.forEach(function(p) {
                                            if (isNaN(p)) {
                                                // New output;
                                                outputCount ++;
                                                delete editState.outputMap[p];
                                            } else {
                                                editState.outputMap[p] = editState.outputMap[p]+"";
                                                if (editState.outputMap[p] !== "-1") {
                                                    outputCount++;
                                                    if (editState.outputMap[p] !== p) {
                                                        // Output moved
                                                        outputsChanged = true;
                                                    } else {
                                                        delete editState.outputMap[p];
                                                    }
                                                } else {
                                                    // Output removed
                                                    outputsChanged = true;
                                                }
                                            }
                                        });

                                        newValue = outputCount;
                                        if (outputsChanged) {
                                            editState.changed = true;
                                        }
                                    } else {
                                        newValue = parseInt(newValue);
                                    }
                                }
                                if (node._def.defaults[d].type) {
                                    if (newValue == "_ADD_") {
                                        newValue = "";
                                    }
                                }
                                if (!isEqual(node[d], newValue)) {
                                    if (node._def.defaults[d].type) {
                                        // Change to a related config node
                                        var configNode = RED.nodes.node(node[d]);
                                        if (configNode) {
                                            var users = configNode.users;
                                            users.splice(users.indexOf(node),1);
                                            RED.events.emit("nodes:change",configNode);
                                        }
                                        configNode = RED.nodes.node(newValue);
                                        if (configNode) {
                                            configNode.users.push(node);
                                            RED.events.emit("nodes:change",configNode);
                                        }
                                    }
                                    editState.changes[d] = node[d];
                                    node[d] = newValue;
                                    editState.changed = true;
                                }
                            }
                        }
                    }
                }
                if (node._def.credentials) {
                    const credDefinition = node._def.credentials;
                    const credChanges = updateNodeCredentials(node, credDefinition, this.inputClass);

                    if (Object.keys(credChanges).length) {
                        editState.changed = true;
                        editState.changes.credentials = {
                            ...(editState.changes.credentials || {}),
                            ...credChanges
                        };
                    }
                }
            }
        }
    });

    /**
     * Compares `newValue` with `originalValue` for equality.  
     * @param {*} originalValue Original value
     * @param {*} newValue New value
     * @returns {boolean} true if originalValue equals newValue, otherwise false
     */
     function isEqual(originalValue, newValue) {
        try {
            if(originalValue == newValue) {
                return true; 
            }
            return JSON.stringify(originalValue) === JSON.stringify(newValue);
        } catch (err) {
            return false;
        }
    }

    /**
     * Update the node credentials from the edit form
     * @param node - the node containing the credentials
     * @param credDefinition - definition of the credentials
     * @param prefix - prefix of the input fields
     * @return {object} an object containing the modified properties
     */
    function updateNodeCredentials(node, credDefinition, prefix) {
        const changes = {};

        if (!node.credentials) {
            node.credentials = {_:{}};
        } else if (!node.credentials._) {
            node.credentials._ = {};
        }

        for (var cred in credDefinition) {
            if (credDefinition.hasOwnProperty(cred)) {
                var input = $("#" + prefix + '-' + cred);
                if (input.length > 0) {
                    var value = input.val();
                    if (credDefinition[cred].type == 'password') {
                        if (value === '__PWRD__') {
                            // A cred value exists - no changes
                        } else if (value === '' && node.credentials['has_' + cred] === false) {
                            // Empty cred value exists - no changes
                        } else if (value === node.credentials[cred]) {
                            // A cred value exists locally in the editor - no changes
                            // Like the user sets a value, saves the config,
                            // reopens the config and save the config again
                        } else {
                            changes['has_' + cred] = node.credentials['has_' + cred];
                            changes[cred] = node.credentials[cred];
                            node.credentials[cred] = value;
                        }

                        node.credentials['has_' + cred] = (value !== '');
                    } else {
                        // Since these creds are loaded by the editor,
                        // values can be directly compared
                        if (value !== node.credentials[cred]) {
                            changes[cred] = node.credentials[cred];
                            node.credentials[cred] = value;
                        }
                    }
                }
            }
        }

        return changes;
    }
})();
;(function() {
    var _subflowModulePaneTemplate = '<form class="dialog-form form-horizontal" autocomplete="off">'+
        '<div class="form-row">'+
            '<label for="subflow-input-module-module" data-i18n="[append]editor:subflow.module"><i class="fa fa-cube"></i> </label>'+
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-module" data-i18n="[placeholder]common.label.name">'+
        '</div>'+
        '<div class="form-row">'+
            '<label for="subflow-input-module-type" data-i18n="[append]editor:subflow.type"> </label>'+
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-type">'+
        '</div>'+
        '<div class="form-row">'+
            '<label for="subflow-input-module-version" data-i18n="[append]editor:subflow.version"></label>'+
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-version" data-i18n="[placeholder]editor:subflow.versionPlaceholder">'+
        '</div>'+
        '<div class="form-row">'+
            '<label for="subflow-input-module-desc" data-i18n="[append]editor:subflow.desc"></label>'+
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-desc">'+
        '</div>'+
        '<div class="form-row">'+
            '<label for="subflow-input-module-license" data-i18n="[append]editor:subflow.license"></label>'+
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-license">'+
        '</div>'+
        '<div class="form-row">'+
            '<label for="subflow-input-module-author" data-i18n="[append]editor:subflow.author"></label>'+
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-author" data-i18n="[placeholder]editor:subflow.authorPlaceholder">'+
        '</div>'+
        '<div class="form-row">'+
            '<label for="subflow-input-module-keywords" data-i18n="[append]editor:subflow.keys"></label>'+
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-keywords" data-i18n="[placeholder]editor:subflow.keysPlaceholder">'+
        '</div>'+
    '</form>';

    RED.editor.registerEditPane("editor-tab-subflow-module", function(node) {
        return {
            label: RED._("editor-tab.module"),
            name: RED._("editor-tab.module"),
            iconClass: "fa fa-cube",
            create: function(container) {
                buildModuleForm(container, node);
            },
            resize: function(size) {
            },
            close: function() {

            },
            apply: function(editState) {
                var newMeta = exportSubflowModuleProperties(node);
                if (!isSameObj(node.meta,newMeta)) {
                    editState.changes.meta = node.meta;
                    node.meta = newMeta;
                    editState.changed = true;
                }
            }
        }
    });

    function isSameObj(env0, env1) {
        return (JSON.stringify(env0) === JSON.stringify(env1));
    }

    function setupInputValidation(input,validator) {
        var errorTip;
        var validateTimeout;

        var validateFunction = function() {
            if (validateTimeout) {
                return;
            }
            validateTimeout = setTimeout(function() {
                var error = validator(input.val());
                // if (!error && errorTip) {
                //     errorTip.close();
                //     errorTip = null;
                // } else if (error && !errorTip) {
                //     errorTip = RED.popover.create({
                //         tooltip: true,
                //         target:input,
                //         size: "small",
                //         direction: "bottom",
                //         content: error,
                //     }).open();
                // }
                input.toggleClass("input-error",!!error);
                validateTimeout = null;
            })
        }
        input.on("change keyup paste", validateFunction);
    }

    function buildModuleForm(container, node) {
        $(_subflowModulePaneTemplate).appendTo(container);
        var moduleProps = node.meta || {};
        [
            'module',
            'type',
            'version',
            'author',
            'desc',
            'keywords',
            'license'
        ].forEach(function(property) {
            $("#subflow-input-module-"+property).val(moduleProps[property]||"")
        })
        $("#subflow-input-module-type").attr("placeholder",node.id);

        setupInputValidation($("#subflow-input-module-module"), function(newValue) {
            newValue = newValue.trim();
            var isValid = newValue.length < 215;
            isValid = isValid && !/^[._]/.test(newValue);
            isValid = isValid && !/[A-Z]/.test(newValue);
            if (newValue !== encodeURIComponent(newValue)) {
                var m = /^@([^\/]+)\/([^\/]+)$/.exec(newValue);
                if (m) {
                    isValid = isValid && (m[1] === encodeURIComponent(m[1]) && m[2] === encodeURIComponent(m[2]))
                } else {
                    isValid = false;
                }
            }
            return isValid?"":"Invalid module name"
        })
        setupInputValidation($("#subflow-input-module-version"), function(newValue) {
            newValue = newValue.trim();
            var isValid = newValue === "" ||
                          /^(\d|[1-9]\d*)\.(\d|[1-9]\d*)\.(\d|[1-9]\d*)(-(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/.test(newValue);
            return isValid?"":"Invalid version number"
        })

        var licenses = ["none", "Apache-2.0", "BSD-3-Clause", "BSD-2-Clause", "GPL-2.0", "GPL-3.0", "MIT", "MPL-2.0", "CDDL-1.0", "EPL-2.0"];
        var typedLicenses = {
            types: licenses.map(function(l) {
                return {
                    value: l,
                    label: l === "none" ? RED._("editor:subflow.licenseNone") : l,
                    hasValue: false
                };
            })
        }
        typedLicenses.types.push({
            value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
        })
        if (!moduleProps.license) {
            typedLicenses.default = "none";
        } else if (licenses.indexOf(moduleProps.license) > -1) {
            typedLicenses.default = moduleProps.license;
        } else {
            typedLicenses.default = "_custom_";
        }
        $("#subflow-input-module-license").typedInput(typedLicenses)
    }
    function exportSubflowModuleProperties(node) {
        var value;
        var moduleProps = {};
        [
            'module',
            'type',
            'version',
            'author',
            'desc',
            'keywords'
        ].forEach(function(property) {
            value = $("#subflow-input-module-"+property).val().trim();
            if (value) {
                moduleProps[property] = value;
            }
        })
        var selectedLicenseType = $("#subflow-input-module-license").typedInput("type");

        if (selectedLicenseType === '_custom_') {
            value = $("#subflow-input-module-license").val();
            if (value) {
                moduleProps.license = value;
            }
        } else if (selectedLicenseType !== "none") {
            moduleProps.license = selectedLicenseType;
        }
        return moduleProps;
    }

})();
;/**
 * 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.
 **/
(function() {

    var template = '<script type="text/x-red" data-template-name="_buffer"><div id="red-ui-editor-type-buffer-panels"><div id="red-ui-editor-type-buffer-panel-str" class="red-ui-panel"><div class="form-row" style="margin-bottom: 3px; text-align: right;"><button class="red-ui-editor-type-buffer-type red-ui-button red-ui-button-small"><i class="fa fa-exclamation-circle"></i> <span id="red-ui-editor-type-buffer-type-string" data-i18n="bufferEditor.modeString"></span><span id="red-ui-editor-type-buffer-type-array" data-i18n="bufferEditor.modeArray"></span></button></div><div class="form-row node-text-editor-row"><div class="node-text-editor" id="red-ui-editor-type-buffer-str"></div></div></div><div id="red-ui-editor-type-buffer-panel-bin" class="red-ui-panel"><div class="form-row node-text-editor-row" style="margin-top: 10px; margin-bottom:0;"><div class="node-text-editor" id="red-ui-editor-type-buffer-bin"></div></div></div></div></script>';

    function stringToUTF8Array(str) {
        var data = [];
        var i=0, l = str.length;
        for (i=0; i<l; i++) {
            var char = str.charCodeAt(i);
            if (char < 0x80) {
                data.push(char);
            } else if (char < 0x800) {
                data.push(0xc0 | (char >> 6));
                data.push(0x80 | (char & 0x3f));
            } else if (char < 0xd800 || char >= 0xe000) {
                data.push(0xe0 | (char >> 12));
                data.push(0x80 | ((char>>6) & 0x3f));
                data.push(0x80 | (char & 0x3f));
            } else {
                i++;
                char = 0x10000 + (((char & 0x3ff)<<10) | (str.charAt(i) & 0x3ff));
                data.push(0xf0 | (char >>18));
                data.push(0x80 | ((char>>12) & 0x3f));
                data.push(0x80 | ((char>>6) & 0x3f));
                data.push(0x80 | (char & 0x3f));
            }
        }
        return data;
    }


    var definition = {
        show: function(options) {
            var value = options.value;
            var onCancel = options.cancel;
            var onComplete = options.complete;
            var type = "_buffer"
            if ($("script[data-template-name='"+type+"']").length === 0) {
                $(template).appendTo("#red-ui-editor-node-configs");
            }
            RED.view.state(RED.state.EDITING);
            var bufferStringEditor = [];
            var bufferBinValue;

            var panels;

            var trayOptions = {
                title: options.title,
                focusElement: options.focusElement,
                width: "inherit",
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            if (onCancel) { onCancel(); }
                            RED.tray.close();
                        }
                    },
                    {
                        id: "node-dialog-ok",
                        text: RED._("common.label.done"),
                        class: "primary",
                        click: function() {
                            bufferStringEditor.saveView();
                            if (onComplete) { onComplete(JSON.stringify(bufferBinValue),null,bufferStringEditor); }
                            RED.tray.close();
                        }
                    }
                ],
                resize: function(dimensions) {
                    var height = $("#dialog-form").height();
                    if (panels) {
                        panels.resize(height);
                    }
                },
                open: function(tray) {
                    var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor');
                    bufferStringEditor = RED.editor.createEditor({
                        id: 'red-ui-editor-type-buffer-str',
                        value: value||"",
                        stateId: RED.editor.generateViewStateId("buffer", options, ""),
                        focus: true,
                        mode:"ace/mode/text"
                    });

                    bufferBinEditor = RED.editor.createEditor({
                        id: 'red-ui-editor-type-buffer-bin',
                        value: "",
                        stateId: false,
                        focus: false,
                        mode:"ace/mode/text",
                        readOnly: true
                    });

                    var changeTimer;
                    var buildBuffer = function(data) {
                        var valid = true;
                        var isString = typeof data === 'string';
                        var binBuffer = [];
                        if (isString) {
                            bufferBinValue = stringToUTF8Array(data);
                        } else {
                            bufferBinValue = data;
                        }
                        var i=0,l=bufferBinValue.length;
                        var c = 0;
                        for(i=0;i<l;i++) {
                            var d = parseInt(Number(bufferBinValue[i]));
                            if (!isString && (isNaN(d) || d < 0 || d > 255)) {
                                valid = false;
                                break;
                            }
                            if (i>0) {
                                if (i%8 === 0) {
                                    if (i%16 === 0) {
                                        binBuffer.push("\n");
                                    } else {
                                        binBuffer.push("  ");
                                    }
                                } else {
                                    binBuffer.push(" ");
                                }
                            }
                            binBuffer.push((d<16?"0":"")+d.toString(16).toUpperCase());
                        }
                        if (valid) {
                            $("#red-ui-editor-type-buffer-type-string").toggle(isString);
                            $("#red-ui-editor-type-buffer-type-array").toggle(!isString);
                            bufferBinEditor.setValue(binBuffer.join(""),1);
                        }
                        return valid;
                    }
                    var bufferStringUpdate = function() {
                        var value = bufferStringEditor.getValue();
                        var isValidArray = false;
                        if (/^[\s]*\[[\s\S]*\][\s]*$/.test(value)) {
                            isValidArray = true;
                            try {
                                var data = JSON.parse(value);
                                isValidArray = buildBuffer(data);
                            } catch(err) {
                                isValidArray = false;
                            }
                        }
                        if (!isValidArray) {
                            buildBuffer(value);
                        }

                    }
                    bufferStringEditor.getSession().on('change', function() {
                        clearTimeout(changeTimer);
                        changeTimer = setTimeout(bufferStringUpdate,200);
                    });

                    bufferStringUpdate();

                    dialogForm.i18n();

                    panels = RED.panels.create({
                        id:"red-ui-editor-type-buffer-panels",
                        resize: function(p1Height,p2Height) {
                            var p1 = $("#red-ui-editor-type-buffer-panel-str");
                            p1Height -= $(p1.children()[0]).outerHeight(true);
                            var editorRow = $(p1.children()[1]);
                            p1Height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
                            $("#red-ui-editor-type-buffer-str").css("height",(p1Height-5)+"px");
                            bufferStringEditor.resize();

                            var p2 = $("#red-ui-editor-type-buffer-panel-bin");
                            editorRow = $(p2.children()[0]);
                            p2Height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
                            $("#red-ui-editor-type-buffer-bin").css("height",(p2Height-5)+"px");
                            bufferBinEditor.resize();
                        }
                    });

                    $(".red-ui-editor-type-buffer-type").on("click", function(e) {
                        e.preventDefault();
                        RED.sidebar.help.set(RED._("bufferEditor.modeDesc"));
                    })


                },
                close: function() {
                    if (options.onclose) {
                        options.onclose();
                    }
                    bufferStringEditor.destroy();
                    bufferBinEditor.destroy();
                },
                show: function() {}
            }
            RED.tray.show(trayOptions);
        }
    }
    RED.editor.registerTypeEditor("_buffer", definition);

})();
;/**
 * 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.codeEditor
 */
 RED.editor.codeEditor = (function() {

    const MONACO = "monaco";
    const ACE = "ace";
    const defaultEditor = MONACO;
    const DEFAULT_SETTINGS = { lib: defaultEditor, options: {} };
    var selectedCodeEditor = null;
    var initialised = false;

    function init() {
        var codeEditorSettings = RED.editor.codeEditor.settings;
        var editorChoice = codeEditorSettings.lib === MONACO ? MONACO : ACE;
        try {
            var browser = RED.utils.getBrowserInfo();
            selectedCodeEditor = RED.editor.codeEditor[editorChoice];
            //fall back to default code editor if there are any issues
            if (!selectedCodeEditor || (editorChoice === MONACO && (browser.ie || !window.monaco))) {
                selectedCodeEditor = RED.editor.codeEditor[defaultEditor];
            }
            initialised = selectedCodeEditor.init();
        } catch (error) {
            selectedCodeEditor = null;
            console.warn("Problem initialising '" + editorChoice + "' code editor", error);
        }
        if(!initialised) {
            selectedCodeEditor = RED.editor.codeEditor[defaultEditor];
            initialised = selectedCodeEditor.init();
        }

        $('<div id="red-ui-image-drop-target"><div data-i18n="[append]workspace.dropImageHere"><i class="fa fa-download"></i><br></div></div>').appendTo('#red-ui-editor');
        $("#red-ui-image-drop-target").hide();
    }

    function create(options) {
        //TODO: (quandry - for consideration)
        // Below, I had to create a hidden element if options.id || options.element is not in the DOM
        // I have seen 1 node calling  `this.editor = RED.editor.createEditor()` with an
        // invalid (non existing html element selector) (e.g. node-red-contrib-components does this)
        // This causes monaco to throw an error when attempting to hook up its events to the dom  & the rest of the 'oneditperapre'
        // code is thus skipped.
        // In ACE mode, creating an ACE editor (with an invalid ID) allows the editor to be created (but obviously there is no UI)
        // Because one (or more) contrib nodes have left this bad code in place, how would we handle this?
        // For compatibility, I have decided to create a hidden element so that at least an editor is created & errors do not occur.
        // IMO, we should warn and exit as it is a coding error by the contrib author.

        if (!options) {
            console.warn("createEditor() options are missing");
            options = {};
        }

        var editor = null;
        if (this.editor.type === MONACO) {
            // compatibility (see above note)
            if (!options.element && !options.id) {
                options.id = 'node-backwards-compatability-dummy-editor';
            }
            options.element = options.element || $("#" + options.id)[0];
            if (!options.element) {
                console.warn("createEditor() options.element or options.id is not valid", options);
                $("#dialog-form").append('<div id="' + options.id + '" style="display: none;" />');
            }
            editor = this.editor.create(options);
        } else {
            editor = this.editor.create(options);//fallback to ACE
        }
        if (options.mode === "ace/mode/markdown") {
            RED.editor.customEditTypes['_markdown'].postInit(editor, options);
        }
        return editor;
    }

    return {
        init: init,
        /**
         * Get editor settings object
         * @memberof RED.editor.codeEditor
         */
        get settings() {
          return RED.settings.get('codeEditor') || DEFAULT_SETTINGS;
        },
        /**
         * Get user selected code editor
         * @return {string} Returns
         * @memberof RED.editor.codeEditor
         */
        get editor() {
            return selectedCodeEditor;
        },
        /**
         * Create a editor ui component
         * @param {object} options - the editor options
         * @memberof RED.editor.codeEditor
         */
        create: create
    }
})();
;RED.editor.colorPicker = RED.colorPicker = (function() {

    function create(options) {
        var color = options.value;
        var id = options.id;
        var colorPalette = options.palette || [];
        var width = options.cellWidth || 30;
        var height = options.cellHeight || 30;
        var margin = options.cellMargin || 2;
        var perRow = options.cellPerRow || 6;

        var container = $("<div>",{style:"display:inline-block"});
        var colorHiddenInput = $("<input/>", { id: id, type: "hidden", value: color }).appendTo(container);
        var opacityHiddenInput = $("<input/>", { id: id+"-opacity", type: "hidden", value: options.hasOwnProperty('opacity')?options.opacity:"1" }).appendTo(container);

        var colorButton = $('<button type="button" class="red-ui-button red-ui-editor-node-appearance-button">').appendTo(container);
        $('<i class="fa fa-caret-down"></i>').appendTo(colorButton);

        var colorDispContainer = $('<div>',{class:"red-ui-search-result-node"}).appendTo(colorButton);
        $('<div>',{class:"red-ui-color-picker-cell-none"}).appendTo(colorDispContainer);
        var colorDisp = $('<div>',{class:"red-ui-color-picker-swatch"}).appendTo(colorDispContainer);


        var refreshDisplay = function(color) {
            if (color === "none") {
                colorDisp.addClass('red-ui-color-picker-cell-none').css({
                    "background-color": "",
                    opacity: 1
                });
                colorDispContainer.css({
                    "border-color":""
                })
            } else {
                var opacity = parseFloat(opacityHiddenInput.val())
                colorDisp.removeClass('red-ui-color-picker-cell-none').css({
                    "background-color": color,
                    "opacity": opacity
                });
                var border = RED.utils.getDarkerColor(color);
                if (border[0] === '#') {
                    border += Math.round(255*Math.floor(opacity*100)/100).toString(16);
                } else {
                    border = "";
                }

                colorDispContainer.css({
                    "border-color": border
                })
            }
            if (options.hasOwnProperty('opacity')) {
                $(".red-ui-color-picker-opacity-slider-overlay").css({
                    "background-image": "linear-gradient(90deg, transparent 0%, "+color+" 100%)"
                })
            }


        }

        colorButton.on("click", function (e) {
            var numColors = colorPalette.length;

            var picker = $("<div/>", {
                class: "red-ui-color-picker"
            }).css({
                width: ((width+margin+margin)*perRow)+"px",
                height: Math.ceil(numColors/perRow)*(height+margin+margin)+"+px"
            });
            var count = 0;
            var row = null;
            row = $("<div/>").appendTo(picker);

            var colorInput = $('<input>',{
                type:"text",
                value:colorHiddenInput.val()
            }).appendTo(row);
            var focusTarget = colorInput;
            colorInput.on("change", function (e) {
                var color = colorInput.val();
                if (options.defaultValue && !color.match(/^([a-z]+|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3})$/)) {
                    color = options.defaultValue;
                }
                colorHiddenInput.val(color).trigger('change');
                refreshDisplay(color);
            });
            // if (options.hasOwnProperty('opacity')) {
            //     var sliderContainer = $("<div>",{class:"red-ui-color-picker-opacity-slider"
            // }

            if (options.none) {
                row = $("<div/>").appendTo(picker);
                var button = $("<button/>", {
                    class:"red-ui-color-picker-cell red-ui-color-picker-cell-none"
                }).css({
                    width: width+"px",
                    height: height+"px",
                    margin: margin+"px"
                }).appendTo(row);
                button.on("click",  function (e) {
                    e.preventDefault();
                    colorInput.val("none");
                    colorInput.trigger("change");
                });
            }


            colorPalette.forEach(function (col) {
                if ((count % perRow) == 0) {
                    row = $("<div/>").appendTo(picker);
                }
                var button = $("<button/>", {
                    class:"red-ui-color-picker-cell"
                }).css({
                    width: width+"px",
                    height: height+"px",
                    margin: margin+"px",
                    backgroundColor: col,
                    "border-color": RED.utils.getDarkerColor(col)
                }).appendTo(row);
                button.on("click",  function (e) {
                    e.preventDefault();
                    // colorPanel.hide();
                    colorInput.val(col);
                    colorInput.trigger("change");
                });
                count++;
            });
            if (options.none || options.hasOwnProperty('opacity')) {
                row = $("<div/>").appendTo(picker);
                // if (options.none) {
                //     var button = $("<button/>", {
                //         class:"red-ui-color-picker-cell red-ui-color-picker-cell-none"
                //     }).css({
                //         width: width+"px",
                //         height: height+"px",
                //         margin: margin+"px"
                //     }).appendTo(row);
                //     button.on("click",  function (e) {
                //         e.preventDefault();
                //         colorPanel.hide();
                //         selector.val("none");
                //         selector.trigger("change");
                //     });
                // }
                if (options.hasOwnProperty('opacity')) {
                    var sliderContainer = $("<div>",{class:"red-ui-color-picker-opacity-slider"}).appendTo(row);
                    sliderContainer.on("mousedown", function(evt) {
                        if (evt.target === sliderHandle[0]) {
                            return;
                        }
                        var v = evt.offsetX/sliderContainer.width();
                        sliderHandle.css({
                            left: ( v*(sliderContainer.width() - sliderHandle.outerWidth()))+"px"
                        });
                        v = Math.floor(100*v)
                        opacityHiddenInput.val(v/100)
                        opacityLabel.text(v+"%");
                        refreshDisplay(colorHiddenInput.val());
                    })
                     $("<div>",{class:"red-ui-color-picker-opacity-slider-overlay"}).appendTo(sliderContainer);
                    var sliderHandle = $("<div>",{class:"red-ui-color-picker-opacity-slider-handle red-ui-button red-ui-button-small"}).appendTo(sliderContainer).draggable({
                        containment: "parent",
                        axis: "x",
                        drag: function( event, ui ) {
                            var v = Math.max(0,ui.position.left/($(this).parent().width()-$(this).outerWidth()));
                            // Odd bug that if it is loaded with a non-0 value, the first time
                            // it is dragged it ranges -1 to 99. But every other time, its 0 to 100.
                            // The Math.max above makes the -1 disappear. The follow hack ensures
                            // it always maxes out at a 100, at the cost of not allowing 99% exactly.
                            v = Math.floor(100*v)
                            if ( v === 99 ) {
                                v = 100;
                            }
                            // console.log("uip",ui.position.left);
                            opacityHiddenInput.val(v/100)
                            opacityLabel.text(v+"%");
                            refreshDisplay(colorHiddenInput.val());
                        }
                    });
                    var opacityLabel = $('<small></small>').appendTo(row);
                    setTimeout(function() {
                        sliderHandle.css({
                            left: (parseFloat(opacityHiddenInput.val())*(sliderContainer.width() - sliderHandle.outerWidth()))+"px"
                        })
                        opacityLabel.text(Math.floor(opacityHiddenInput.val()*100)+"%");
                    },50);
                }
            }

            var colorPanel = RED.popover.panel(picker);
            setTimeout(function() {
                refreshDisplay(colorHiddenInput.val())
            },50);
            colorPanel.show({
                target: colorButton,
                onclose: function() {
                    colorButton.focus();
                }
            })
            if (focusTarget) {
                focusTarget.focus();
            }
        });
        setTimeout(function() {
            refreshDisplay(colorHiddenInput.val())
        },50);
        return container;
    }

    return {
        create: create
    }
})();
;RED.editor.envVarList = (function() {

    var currentLocale = 'en-US';
    const DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
    const DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES = ['str','num','bool','json','bin','env','conf-types'];
    const DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata'];

    /**
     * Create env var edit interface
     * @param container - container
     * @param node - subflow node
     */
    function buildPropertiesList(envContainer, node) {
        if(RED.editor.envVarList.debug) { console.log('envVarList: buildPropertiesList', envContainer, node) }
        const isTemplateNode = (node.type === "subflow");

        envContainer
            .css({
                'min-height':'150px',
                'min-width':'450px'
            })
            .editableList({
                header: isTemplateNode?$('<div><div><div></div><div data-i18n="common.label.name"></div><div data-i18n="editor-tab.defaultValue"></div><div></div></div></div>'):undefined,
                addItem: function(container, i, opt) {
                    // If this is an instance node, these are properties unique to
                    // this instance - ie opt.parent will not be defined.

                    if (isTemplateNode) {
                        container.addClass("red-ui-editor-subflow-env-editable")
                    }

                    var envRow = $('<div/>').appendTo(container);
                    var nameField = null;
                    var valueField = null;

                    nameField = $('<input/>', {
                        class: "node-input-env-name",
                        type: "text",
                        placeholder: RED._("common.label.name")
                    }).attr("autocomplete","disable").appendTo(envRow).val(opt.name);
                    valueField = $('<input/>',{
                        style: "width:100%",
                        class: "node-input-env-value",
                        type: "text",
                    }).attr("autocomplete","disable").appendTo(envRow);
                    var types = (opt.ui && opt.ui.opts && opt.ui.opts.types);
                    if (!types) {
                        types = isTemplateNode ? DEFAULT_ENV_TYPE_LIST : DEFAULT_ENV_TYPE_LIST_INC_CRED;
                    }
                    valueField.typedInput({default:'str',types:types});
                    valueField.typedInput('type', opt.type);
                    if (opt.type === "cred") {
                        if (opt.value) {
                            valueField.typedInput('value', opt.value);
                        } else if (node.credentials && node.credentials[opt.name]) {
                            valueField.typedInput('value', node.credentials[opt.name]);
                        } else if (node.credentials && node.credentials['has_'+opt.name]) {
                            valueField.typedInput('value', "__PWRD__");
                        } else {
                            valueField.typedInput('value', "");
                        }
                    } else {
                        valueField.typedInput('value', opt.value);
                    }


                    opt.nameField = nameField;
                    opt.valueField = valueField;

                    var actionButton = $('<a/>',{href:"#",class:"red-ui-editableList-item-remove red-ui-button red-ui-button-small"}).appendTo(envRow);
                    $('<i/>',{class:"fa "+(opt.parent?"fa-reply":"fa-remove")}).appendTo(actionButton);
                    var removeTip = RED.popover.tooltip(actionButton,RED._("subflow.env.remove"));
                    actionButton.on("click", function(evt) {
                        evt.preventDefault();
                        removeTip.close();
                        container.parent().addClass("red-ui-editableList-item-deleting")
                        container.fadeOut(300, function() {
                            envContainer.editableList('removeItem',opt);
                        });
                    });

                    if (isTemplateNode) {
                        // Add the UI customisation row
                        // if `opt.ui` does not exist, then apply defaults. If these
                        // defaults do not change then they will get stripped off
                        // before saving.
                        if (opt.type === 'conf-types') {
                            opt.ui = opt.ui || {
                                icon: "fa fa-cog",
                                type: "conf-types",
                                opts: {opts:[]}
                            }
                            opt.ui.type = "conf-types";
                        } else if (opt.type === 'cred') {
                            opt.ui = opt.ui || {
                                icon: "",
                                type: "cred"
                            }
                            opt.ui.type = "cred";
                        } else {
                            opt.ui = opt.ui || {
                                icon: "",
                                type: "input",
                                opts: {types:DEFAULT_ENV_TYPE_LIST}
                            }
                        }
                        opt.ui.label = opt.ui.label || {};
                        opt.ui.type = opt.ui.type || "input";
                        if ((opt.ui.type === "cred") &&
                            opt.ui.opts &&
                            opt.ui.opts.types) {
                            opt.ui.type = "input";
                        }

                        var uiRow = $('<div/>').appendTo(container).hide();
                        // save current info for reverting on cancel
                        // var copy = $.extend(true, {}, ui);

                         $('<a href="#"><i class="fa fa-angle-right"></a>').prependTo(envRow).on("click", function (evt) {
                            evt.preventDefault();
                            if ($(this).hasClass('expanded')) {
                                uiRow.slideUp();
                                $(this).removeClass('expanded');
                            } else {
                                uiRow.slideDown();
                                $(this).addClass('expanded');
                            }
                        });

                        buildEnvEditRow(uiRow, opt, nameField, valueField);
                        nameField.trigger('change');
                    }
                },
                sortable: true,
                removable: false
            });
        var parentEnv = {};
        var envList = [];
        if (/^subflow:/.test(node.type)) {
            var subflowDef = RED.nodes.subflow(node.type.substring(8));
            if (subflowDef.env) {
                subflowDef.env.forEach(function(env) {
                    var item = {
                        name:env.name,
                        parent: {
                            type: env.type,
                            value: env.value,
                            ui: env.ui
                        }
                    }
                    envList.push(item);
                    parentEnv[env.name] = item;
                })
            }
        }

        if (node.env) {
            for (var i = 0; i < node.env.length; i++) {
                var env = node.env[i];
                if (parentEnv.hasOwnProperty(env.name)) {
                    parentEnv[env.name].type = env.type;
                    parentEnv[env.name].value = env.value;
                } else {
                    envList.push({
                        name: env.name,
                        type: env.type,
                        value: env.value,
                        ui: env.ui
                    });
                }
            }
        }
        envList.forEach(function(env) {
            if (env.parent && env.parent.ui && env.parent.ui.type === 'hide') {
                return;
            }
            if (!isTemplateNode && env.parent) {
                return;
            }
            envContainer.editableList('addItem', JSON.parse(JSON.stringify(env)));
        });
    }


    /**
     * Create UI edit interface for environment variable
     * @param container - container
     * @param env - env var definition
     * @param nameField - name field of env var
     * @param valueField - value field of env var
     */
     function buildEnvEditRow(container, opt, nameField, valueField) {
        const ui = opt.ui
        if(RED.editor.envVarList.debug) { console.log('envVarList: buildEnvEditRow', container, ui, nameField, valueField) }
         container.addClass("red-ui-editor-subflow-env-ui-row")
         var topRow = $('<div></div>').appendTo(container);
         $('<div></div>').appendTo(topRow);
         $('<div>').text(RED._("editor.icon")).appendTo(topRow);
         $('<div>').text(RED._("editor.label")).appendTo(topRow);
         $('<div class="red-env-ui-input-type-col">').text(RED._("editor.inputType")).appendTo(topRow);

         var row = $('<div></div>').appendTo(container);
         $('<div><i class="red-ui-editableList-item-handle fa fa-bars"></i></div>').appendTo(row);
         var typeOptions = {
            'input': {types:DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES},
            'select': {opts:[]},
            'spinner': {},
            'cred': {}
         };
         if (ui.opts) {
             typeOptions[ui.type] = ui.opts;
         } else {
             // Pick up the default values if not otherwise provided
             ui.opts = typeOptions[ui.type];
         }
         var iconCell = $('<div></div>').appendTo(row);

         var iconButton = $('<a href="#"></a>').appendTo(iconCell);
         iconButton.on("click", function(evt) {
             evt.preventDefault();
             var icon = ui.icon || "";
             var iconPath = (icon ? RED.utils.separateIconPath(icon) : {});
             RED.editor.iconPicker.show(iconButton, null, iconPath, true, function (newIcon) {
                 iconButton.empty();
                 var path = newIcon || "";
                 var newPath = RED.utils.separateIconPath(path);
                 if (newPath) {
                     $('<i class="fa"></i>').addClass(newPath.file).appendTo(iconButton);
                 }
                 ui.icon = path;
             });
         })

         if (ui.icon) {
             var newPath = RED.utils.separateIconPath(ui.icon);
             $('<i class="fa '+newPath.file+'"></i>').appendTo(iconButton);
         }

         var labelCell = $('<div></div>').appendTo(row);

         var label = ui.label && ui.label[currentLocale] || "";
         var labelInput = $('<input type="text">').val(label).appendTo(labelCell);
         ui.labelField = labelInput;
         labelInput.on('change', function(evt) {
             ui.label = ui.label || {};
             var val = $(this).val().trim();
             if (val === "") {
                 delete ui.label[currentLocale];
             } else {
                 ui.label[currentLocale] = val;
             }
         })
         var labelIcon = $('<span class="red-ui-editor-subflow-env-lang-icon"><i class="fa fa-language"></i></span>').appendTo(labelCell);
         RED.popover.tooltip(labelIcon,function() {
             var langs = Object.keys(ui.label);
             var content = $("<div>");
             if (langs.indexOf(currentLocale) === -1) {
                 langs.push(currentLocale);
                 langs.sort();
             }
             langs.forEach(function(l) {
                 var row = $('<div>').appendTo(content);
                 $('<span>').css({display:"inline-block",width:"120px"}).text(RED._("languages."+l)+(l===currentLocale?"*":"")).appendTo(row);
                 $('<span>').text(ui.label[l]||"").appendTo(row);
             });
             return content;
         })

         nameField.on('change',function(evt) {
            labelInput.attr("placeholder",$(this).val())
        });

        var inputCell = $('<div class="red-env-ui-input-type-col"></div>').appendTo(row);
        var uiInputTypeInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
        if (ui.type === "input") {
            uiInputTypeInput.val(ui.opts.types.join(","));
        }
        var checkbox;
        var selectBox;

        // the options presented in the UI section for an "input" type selection
        uiInputTypeInput.typedInput({
            types: [
                {
                    value:"input",
                    label:RED._("editor.inputs.input"), icon:"fa fa-i-cursor",showLabel:false,multiple:true,options:[
                        {value:"str",label:RED._("editor.types.str"),icon:"red/images/typedInput/az.svg"},
                        {value:"num",label:RED._("editor.types.num"),icon:"red/images/typedInput/09.svg"},
                        {value:"bool",label:RED._("editor.types.bool"),icon:"red/images/typedInput/bool.svg"},
                        {value:"json",label:RED._("editor.types.json"),icon:"red/images/typedInput/json.svg"},
                        {value: "bin",label: RED._("editor.types.bin"),icon: "red/images/typedInput/bin.svg"},
                        {value: "env",label: RED._("editor.types.env"),icon: "red/images/typedInput/env.svg"},
                        {value: "cred",label: RED._("editor.types.cred"),icon: "fa fa-lock"}
                    ],
                    default: DEFAULT_ENV_TYPE_LIST,
                    valueLabel: function(container,value) {
                        container.css("padding",0);
                        var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type"></div>').appendTo(container);

                        var input = $('<div class="placeholder-input">').appendTo(innerContainer);
                        $('<span><i class="fa fa-i-cursor"></i></span>').appendTo(input);
                        if (value.length) {
                            value.forEach(function(v) {
                                if (!/^fa /.test(v.icon)) {
                                    $('<img>',{src:v.icon,style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input);
                                } else {
                                    var s = $('<span>',{style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input);
                                    $("<i>",{class: v.icon}).appendTo(s);
                                }
                            })
                        } else {
                            $('<span class="red-ui-editor-subflow-env-input-type-placeholder"></span>').text(RED._("editor.selectType")).appendTo(input);
                        }
                    }
                },
                {
                    value: "cred",
                    label: RED._("typedInput.type.cred"), icon:"fa fa-lock", showLabel: false,
                    valueLabel: function(container,value) {
                        container.css("padding",0);
                        var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type">').css({
                            "border-top-right-radius": "4px",
                            "border-bottom-right-radius": "4px"
                        }).appendTo(container);
                        $('<div class="placeholder-input">').html("&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;").appendTo(innerContainer);
                    }
                },
                {
                    value:"select",
                    label:RED._("editor.inputs.select"), icon:"fa fa-tasks",showLabel:false,
                    valueLabel: function(container,value) {
                        container.css("padding","0");

                        selectBox = $('<select></select>').appendTo(container);
                        if (ui.opts && Array.isArray(ui.opts.opts)) {
                            ui.opts.opts.forEach(function(o) {
                                var label = lookupLabel(o.l, o.l["en-US"]||o.v, currentLocale);
                                // $('<option>').val((o.t||'str')+":"+o.v).text(label).appendTo(selectBox);
                                $('<option>').val(o.v).text(label).appendTo(selectBox);
                            })
                        }
                        selectBox.on('change', function(evt) {
                            var v = selectBox.val();
                            // var parts = v.split(":");
                            // var t = parts.shift();
                            // v = parts.join(":");
                            //
                            // valueField.typedInput("type",'str')
                            valueField.typedInput("value",v)
                        });
                        selectBox.val(valueField.typedInput("value"));
                        // selectBox.val(valueField.typedInput('type')+":"+valueField.typedInput("value"));
                    },
                    expand: {
                        icon: "fa-caret-down",
                        minWidth: 400,
                        content: function(container) {
                            var content = $('<div class="red-ui-editor-subflow-ui-edit-panel">').appendTo(container);
                            var optList = $('<ol>').appendTo(content).editableList({
                                header:$("<div><div>"+RED._("editor.select.label")+"</div><div>"+RED._("editor.select.value")+"</div></div>"),
                                addItem: function(row,index,itemData) {
                                    var labelDiv = $('<div>').appendTo(row);
                                    var label = lookupLabel(itemData.l, "", currentLocale);
                                    itemData.label = $('<input type="text">').val(label).appendTo(labelDiv);
                                    itemData.label.on('keydown', function(evt) {
                                        if (evt.keyCode === 13) {
                                            itemData.input.focus();
                                            evt.preventDefault();
                                        }
                                    });
                                    var labelIcon = $('<span class="red-ui-editor-subflow-env-lang-icon"><i class="fa fa-language"></i></span>').appendTo(labelDiv);
                                    RED.popover.tooltip(labelIcon,function() {
                                        return currentLocale;
                                    })
                                    itemData.input = $('<input type="text">').val(itemData.v).appendTo(row);

                                    // Problem using a TI here:
                                    //  - this is in a popout panel
                                    //  - clicking the expand button in the TI will close the parent edit tray
                                    //    and open the type editor.
                                    //  - but it leaves the popout panel over the top.
                                    //  - there is no way to get back to the popout panel after closing the type editor
                                    //.typedInput({default:itemData.t||'str', types:DEFAULT_ENV_TYPE_LIST});
                                    itemData.input.on('keydown', function(evt) {
                                        if (evt.keyCode === 13) {
                                            // Enter or Tab
                                            var index = optList.editableList('indexOf',itemData);
                                            var length = optList.editableList('length');
                                            if (index + 1 === length) {
                                                var newItem = {};
                                                optList.editableList('addItem',newItem);
                                                setTimeout(function() {
                                                    if (newItem.label) {
                                                        newItem.label.focus();
                                                    }
                                                },100)
                                            } else {
                                                var nextItem = optList.editableList('getItemAt',index+1);
                                                if (nextItem.label) {
                                                    nextItem.label.focus()
                                                }
                                            }
                                            evt.preventDefault();
                                        }
                                    });
                                },
                                sortable: true,
                                removable: true,
                                height: 160
                            })
                            if (ui.opts.opts.length > 0) {
                                ui.opts.opts.forEach(function(o) {
                                    optList.editableList('addItem',$.extend(true,{},o))
                                })
                            } else {
                                optList.editableList('addItem',{})
                            }
                            return {
                                onclose: function() {
                                    var items = optList.editableList('items');
                                    var vals = [];
                                    items.each(function (i,el) {
                                        var data = el.data('data');
                                        var l = data.label.val().trim();
                                        var v = data.input.val();
                                        // var t = data.input.typedInput('type');
                                        // var v = data.input.typedInput('value');
                                        if (l.length > 0) {
                                            data.l = data.l || {};
                                            data.l[currentLocale] = l;
                                        }
                                        data.v = v;

                                        if (l.length > 0 || v.length > 0) {
                                            var val = {l:data.l,v:data.v};
                                            // if (t !== 'str') {
                                            //     val.t = t;
                                            // }
                                            vals.push(val);
                                        }
                                    });
                                    ui.opts.opts = vals;
                                    uiInputTypeInput.typedInput('value',Date.now())
                                }
                            }
                        }
                    }
                },
                {
                    value:"checkbox",
                    label:RED._("editor.inputs.checkbox"), icon:"fa fa-check-square-o",showLabel:false,
                    valueLabel: function(container,value) {
                        container.css("padding",0);
                        checkbox = $('<input type="checkbox">').appendTo(container);
                        checkbox.on('change', function(evt) {
                            valueField.typedInput('value',$(this).prop('checked')?"true":"false");
                        })
                        checkbox.prop('checked',valueField.typedInput('value')==="true");
                    }
                },
                {
                    value:"spinner",
                    label:RED._("editor.inputs.spinner"), icon:"fa fa-sort-numeric-asc", showLabel:false,
                    valueLabel: function(container,value) {
                        container.css("padding",0);
                        var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type"></div>').appendTo(container);

                        var input = $('<div class="placeholder-input">').appendTo(innerContainer);
                        $('<span><i class="fa fa-sort-numeric-asc"></i></span>').appendTo(input);

                        var min = ui.opts && ui.opts.min;
                        var max = ui.opts && ui.opts.max;
                        var label = "";
                        if (min !== undefined && max !== undefined) {
                            label = Math.min(min,max)+" - "+Math.max(min,max);
                        } else if (min !== undefined) {
                            label = "> "+min;
                        } else if (max !== undefined) {
                            label = "< "+max;
                        }
                        $('<span>').css("margin-left","15px").text(label).appendTo(input);
                    },
                    expand: {
                        icon: "fa-caret-down",
                        content: function(container) {
                            var content = $('<div class="red-ui-editor-subflow-ui-edit-panel">').appendTo(container);
                            content.css("padding","8px 5px")
                            var min = ui.opts.min;
                            var max = ui.opts.max;
                            var minInput = $('<input type="number" style="margin-bottom:0; width:60px">');
                            minInput.val(min);
                            var maxInput = $('<input type="number" style="margin-bottom:0; width:60px">');
                            maxInput.val(max);
                            $('<div class="form-row" style="margin-bottom:3px"><label>'+RED._("editor.spinner.min")+'</label></div>').append(minInput).appendTo(content);
                            $('<div class="form-row" style="margin-bottom:0"><label>'+RED._("editor.spinner.max")+'</label></div>').append(maxInput).appendTo(content);
                            return {
                                onclose: function() {
                                    var min = minInput.val().trim();
                                    var max = maxInput.val().trim();
                                    if (min !== "") {
                                        ui.opts.min = parseInt(min);
                                    } else {
                                        delete ui.opts.min;
                                    }
                                    if (max !== "") {
                                        ui.opts.max = parseInt(max);
                                    } else {
                                        delete ui.opts.max;
                                    }
                                    uiInputTypeInput.typedInput('value',Date.now())
                                }
                            }
                        }
                    }
                },
                'conf-types',
                {
                    value:"none",
                    label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false
                },
                {
                    value:"hide",
                    label:RED._("editor.inputs.hidden"), icon:"fa fa-ban",hasValue:false
                }
            ],
            default: 'none'
        }).on("typedinputtypechange", function(evt,type) {
            ui.type = $(this).typedInput("type");
            ui.opts = typeOptions[ui.type];
            if (ui.type === 'input') {
                // In the case of 'input' type, the typedInput uses the multiple-option
                // mode. Its value needs to be set to a comma-separately list of the
                // selected options.
                uiInputTypeInput.typedInput('value',ui.opts.types.join(","))
            } else if (ui.type === 'conf-types') {
                // In the case of 'conf-types' type, the typedInput will be populated
                // with a list of all config nodes types installed.
                // Restore the value to the last selected type
                uiInputTypeInput.typedInput('value', opt.type)
            } else {
                // No other type cares about `value`, but doing this will
                // force a refresh of the label now that `ui.opts` has
                // been updated.
                uiInputTypeInput.typedInput('value',Date.now())
            }

            if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:typedinputtypechange. ui.type = ' + ui.type) }
            switch (ui.type) {
                case 'input':
                    valueField.typedInput('types',ui.opts.types);
                    break;
                case 'select':
                    valueField.typedInput('types',['str']);
                    break;
                case 'checkbox':
                    valueField.typedInput('types',['bool']);
                    break;
                case 'spinner':
                    valueField.typedInput('types',['num']);
                    break;
                case 'cred':
                    valueField.typedInput('types',['cred']);
                    break;
                default:
                    valueField.typedInput('types', DEFAULT_ENV_TYPE_LIST);
            }
            if (ui.type === 'checkbox') {
                valueField.typedInput('type','bool');
            } else if (ui.type === 'spinner') {
                valueField.typedInput('type','num');
            }
            if (ui.type !== 'checkbox') {
                checkbox = null;
            }

        }).on("change", function(evt,type) {
            const selectedType = $(this).typedInput('type') // the UI typedInput type
            if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:change. selectedType = ' + selectedType) }
            if (selectedType === 'conf-types') {
                const selectedConfigType = $(this).typedInput('value') || opt.type
                let activeWorkspace = RED.nodes.workspace(RED.workspaces.active());
                if (!activeWorkspace) {
                    activeWorkspace = RED.nodes.subflow(RED.workspaces.active());
                }

                // get a list of all config nodes matching the selectedValue
                const configNodes = [];
                RED.nodes.eachConfig(function(config) {
                    if (config.type == selectedConfigType && (!config.z || config.z === activeWorkspace.id)) {
                        const modulePath = config._def?.set?.id || ''
                        let label = RED.utils.getNodeLabel(config, config.id) || config.id;
                        label += config.d ? ' ['+RED._('workspace.disabled')+']' : '';
                        const _config = {
                            _type: selectedConfigType,
                            value: config.id,
                            label: label,
                            title: modulePath ? modulePath + ' - ' + label : label,
                            enabled: config.d !== true,
                            disabled: config.d === true,
                        }
                        configNodes.push(_config);
                    }
                });
                const tiTypes = {
                    value: selectedConfigType,
                    label: "config",
                    icon: "fa fa-cog",
                    options: configNodes,
                }
                valueField.typedInput('types', [tiTypes]);
                valueField.typedInput('type', selectedConfigType);
                valueField.typedInput('value', opt.value);


            } else if (ui.type === 'input') {
                var types = uiInputTypeInput.typedInput('value');
                ui.opts.types = (types === "") ? ["str"] : types.split(",");
                valueField.typedInput('types',ui.opts.types);
            }
        });
        valueField.on("change", function(evt) {
            if (checkbox) {
                checkbox.prop('checked',$(this).typedInput('value')==="true")
            }
        })
        // Set the input to the right type. This will trigger the 'typedinputtypechange'
        // event handler (just above ^^) to update the value if needed
        uiInputTypeInput.typedInput('type',ui.type)
    }

    function setLocale(l, list) {
        currentLocale = l;
        if (list) {
            var items = list.editableList("items");
            items.each(function (i, item) {
                var entry = $(this).data('data');
                var labelField = entry.ui.labelField;
                labelField.val(lookupLabel(entry.ui.label, "", currentLocale));
                if (labelField.timeout) {
                    clearTimeout(labelField.timeout);
                    delete labelField.timeout;
                }
                labelField.addClass("input-updated");
                labelField.timeout = setTimeout(function() {
                    delete labelField.timeout
                    labelField.removeClass("input-updated");
                },3000);
            });
        }
    }

    /**
     * Lookup text for specific locale
     * @param labels - dict of labels
     * @param defaultLabel - fallback label if not found
     * @param locale - target locale
     * @returns {string} text for specified locale
     */
    function lookupLabel(labels, defaultLabel, locale) {
        if (labels) {
            if (labels[locale]) {
                return labels[locale];
            }
            if (locale) {
                var lang = locale.substring(0, 2);
                if (labels[lang]) {
                    return labels[lang];
                }
            }
        }
        return defaultLabel;
    }

    return {
        create: buildPropertiesList,
        setLocale: setLocale,
        lookupLabel: lookupLabel,
        DEFAULT_ENV_TYPE_LIST: DEFAULT_ENV_TYPE_LIST,
        DEFAULT_ENV_TYPE_LIST_INC_CRED: DEFAULT_ENV_TYPE_LIST_INC_CRED
    }
})();
;/**
 * 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.
 **/
(function() {


    var template = '<script type="text/x-red" data-template-name="_expression">'+
    '<div id="red-ui-editor-type-expression-panels">'+
        '<div id="red-ui-editor-type-expression-panel-expr" class="red-ui-panel">'+
            '<div class="form-row" style="margin-bottom: 3px; text-align: right;"><button class="red-ui-editor-type-expression-legacy red-ui-button red-ui-button-small"><i class="fa fa-exclamation-circle"></i> <span data-i18n="expressionEditor.compatMode"></span></button><button id="red-ui-editor-type-expression-reformat" class="red-ui-button red-ui-button-small"><span data-i18n="expressionEditor.format"></span></button></div>'+
            '<div class="form-row node-text-editor-row"><div class="node-text-editor" id="red-ui-editor-type-expression"></div></div>'+
        '</div>'+
        '<div id="red-ui-editor-type-expression-panel-info" class="red-ui-panel">'+
            '<div class="form-row">'+
                '<ul id="red-ui-editor-type-expression-tabs"></ul>'+
                '<div id="red-ui-editor-type-expression-tab-help" class="red-ui-editor-type-expression-tab-content hide">'+
                    '<div>'+
                        '<select id="red-ui-editor-type-expression-func"></select>'+
                        '<button id="red-ui-editor-type-expression-func-insert" class="red-ui-button" data-i18n="expressionEditor.insert"></button>'+
                    '</div>'+
                    '<div id="red-ui-editor-type-expression-help"></div>'+
                '</div>'+
                '<div id="red-ui-editor-type-expression-tab-test" class="red-ui-editor-type-expression-tab-content hide">'+
                    '<div>'+
                        '<span style="display: inline-block; width: calc(50% - 5px);"><span data-i18n="expressionEditor.data"></span><button style="float: right; margin-right: 5px;" id="node-input-example-reformat" class="red-ui-button red-ui-button-small"><span data-i18n="jsonEditor.format"></span></button></span>'+
                        '<span style="display: inline-block; margin-left: 10px; width: calc(50% - 5px);" data-i18n="expressionEditor.result"></span>'+
                    '</div>'+
                    '<div style="display: inline-block; width: calc(50% - 5px);" class="node-text-editor" id="red-ui-editor-type-expression-test-data"></div>'+
                    '<div style="display: inline-block; margin-left: 10px;  width:calc(50% - 5px);" class="node-text-editor" id="red-ui-editor-type-expression-test-result"></div>'+
                '</div>'+
            '</div>'+
        '</div>'+
    '</div>'+
    '</script>';
    var expressionTestCache = {};

    var definition = {
        show: function(options) {
            var expressionTestCacheId = options.parent||"_";
            var value = options.value;
            var onCancel = options.cancel;
            var onComplete = options.complete;
            var type = "_expression"
            if ($("script[data-template-name='"+type+"']").length === 0) {
                $(template).appendTo("#red-ui-editor-node-configs");
            }
            RED.view.state(RED.state.EDITING);
            var expressionEditor;
            var testDataEditor;
            var testResultEditor
            var panels;

            var trayOptions = {
                title: options.title,
                focusElement: options.focusElement,
                width: "inherit",
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            if(onCancel) { onCancel(); }
                            RED.tray.close();
                        }
                    },
                    {
                        id: "node-dialog-ok",
                        text: RED._("common.label.done"),
                        class: "primary",
                        click: function() {
                            $("#red-ui-editor-type-expression-help").text("");
                            expressionEditor.saveView();
                            if (onComplete) { onComplete(expressionEditor.getValue(),expressionEditor.getCursorPosition(),expressionEditor); }
                            RED.tray.close();
                        }
                    }
                ],
                resize: function(dimensions) {
                    var height = $("#dialog-form").height();
                    if (panels) {
                        panels.resize(height);
                    }

                },
                open: function(tray) {
                    var trayBody = tray.find('.red-ui-tray-body');
                    trayBody.addClass("red-ui-editor-type-expression")
                    var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form','_expression','editor');
                    var funcSelect = $("#red-ui-editor-type-expression-func");
                    Object.keys(jsonata.functions).forEach(function(f) {
                        funcSelect.append($("<option></option>").val(f).text(f));
                    })
                    funcSelect.on("change", function(e) {
                        var f = $(this).val();
                        var args = RED._('jsonata:'+f+".args",{defaultValue:''});
                        var title = "<h5>"+f+"("+args+")</h5>";
                        var body = RED.utils.renderMarkdown(RED._('jsonata:'+f+'.desc',{defaultValue:''}));
                        $("#red-ui-editor-type-expression-help").html(title+"<p>"+body+"</p>");

                    })
                    expressionEditor = RED.editor.createEditor({
                        id: 'red-ui-editor-type-expression',
                        value: "",
                        mode:"ace/mode/jsonata",
                        stateId: options.stateId,
                        focus: true,
                        options: {
                            enableBasicAutocompletion:true,
                            enableSnippets:true,
                            enableLiveAutocompletion: true
                        }
                    });
                    var currentToken = null;
                    var currentTokenPos = -1;
                    var currentFunctionMarker = null;

                    expressionEditor.getSession().setValue(value||"",-1);
                    //ace only (monaco has jsonata tokeniser)
                    if(expressionEditor.type == "ace") {
                        expressionEditor.on("changeSelection", function() {
                            var c = expressionEditor.getCursorPosition();
                            var token = expressionEditor.getSession().getTokenAt(c.row,c.column);
                            if (token !== currentToken || (token && /paren/.test(token.type) && c.column !== currentTokenPos)) {
                                currentToken = token;
                                var r,p;
                                var scopedFunction = null;
                                if (token && token.type === 'keyword') {
                                    r = c.row;
                                    scopedFunction = token;
                                } else {
                                    var depth = 0;
                                    var next = false;
                                    if (token) {
                                        if (token.type === 'paren.rparen') {
                                            // If this is a block of parens ')))', set
                                            // depth to offset against the cursor position
                                            // within the block
                                            currentTokenPos = c.column;
                                            depth = c.column - (token.start + token.value.length);
                                        }
                                        r = c.row;
                                        p = token.index;
                                    } else {
                                        r = c.row-1;
                                        p = -1;
                                    }
                                    while ( scopedFunction === null && r > -1) {
                                        var rowTokens = expressionEditor.getSession().getTokens(r);
                                        if (p === -1) {
                                            p = rowTokens.length-1;
                                        }
                                        while (p > -1) {
                                            var type = rowTokens[p].type;
                                            if (next) {
                                                if (type === 'keyword') {
                                                    scopedFunction = rowTokens[p];
                                                    // console.log("HIT",scopedFunction);
                                                    break;
                                                }
                                                next = false;
                                            }
                                            if (type === 'paren.lparen') {
                                                depth-=rowTokens[p].value.length;
                                            } else if (type === 'paren.rparen') {
                                                depth+=rowTokens[p].value.length;
                                            }
                                            if (depth < 0) {
                                                next = true;
                                                depth = 0;
                                            }
                                            // console.log(r,p,depth,next,rowTokens[p]);
                                            p--;
                                        }
                                        if (!scopedFunction) {
                                            r--;
                                        }
                                    }
                                }
                                expressionEditor.session.removeMarker(currentFunctionMarker);
                                if (scopedFunction) {
                                //console.log(token,.map(function(t) { return t.type}));
                                    funcSelect.val(scopedFunction.value).trigger("change");
                                }
                            }
                        });
                    }
                    dialogForm.i18n();
                    $("#red-ui-editor-type-expression-func-insert").on("click", function(e) {
                        e.preventDefault();
                        var pos = expressionEditor.getCursorPosition();
                        var f = funcSelect.val();
                        var snippet = jsonata.getFunctionSnippet(f);
                        expressionEditor.insertSnippet(snippet);
                        expressionEditor.focus();
                    });
                    $("#red-ui-editor-type-expression-reformat").on("click", function(evt) {
                        evt.preventDefault();
                        var v = expressionEditor.getValue()||"";
                        try {
                            v = jsonata.format(v);
                        } catch(err) {
                            // TODO: do an optimistic auto-format
                        }
                        expressionEditor.getSession().setValue(v||"",-1);
                    });
                    funcSelect.change();

                    var tabs = RED.tabs.create({
                        element: $("#red-ui-editor-type-expression-tabs"),
                        onchange:function(tab) {
                            $(".red-ui-editor-type-expression-tab-content").hide();
                            tab.content.show();
                            trayOptions.resize();
                        }
                    })

                    tabs.addTab({
                        id: 'expression-help',
                        label: RED._('expressionEditor.functionReference'),
                        content: $("#red-ui-editor-type-expression-tab-help")
                    });
                    tabs.addTab({
                        id: 'expression-tests',
                        label: RED._('expressionEditor.test'),
                        content: $("#red-ui-editor-type-expression-tab-test")
                    });
                    testDataEditor = RED.editor.createEditor({
                        id: 'red-ui-editor-type-expression-test-data',
                        value: expressionTestCache[expressionTestCacheId] || '{\n    "payload": "hello world"\n}',
                        stateId: false,
                        focus: false,
                        mode:"ace/mode/json",
                        lineNumbers: false
                    });
                    var changeTimer;
                    $(".red-ui-editor-type-expression-legacy").on("click", function(e) {
                        e.preventDefault();
                        RED.sidebar.help.set(RED._("expressionEditor.compatModeDesc"));
                    })
                    var testExpression = function() {
                        var value = testDataEditor.getValue();
                        var parsedData;
                        var currentExpression = expressionEditor.getValue();
                        var expr;
                        var usesContext = false;
                        var usesEnv = false;
                        var usesMoment = false;
                        var usesClone = false;
                        var legacyMode = /(^|[^a-zA-Z0-9_'".])msg([^a-zA-Z0-9_'"]|$)/.test(currentExpression);
                        $(".red-ui-editor-type-expression-legacy").toggle(legacyMode);
                        try {
                            expr = jsonata(currentExpression);
                            expr.assign('flowContext',function(val) {
                                usesContext = true;
                                return null;
                            });
                            expr.assign('globalContext',function(val) {
                                usesContext = true;
                                return null;
                            });
                            expr.assign("env", function(name) {
                                usesEnv = true;
                                return null;
                            });
                            expr.assign("moment", function(name) {
                                usesMoment = true;
                                return null;
                            });
                            expr.assign("clone", function(name) {
                                usesClone = true;
                                return null;
                            });
                        } catch(err) {
                            testResultEditor.setValue(RED._("expressionEditor.errors.invalid-expr",{message:err.message}),-1);
                            return;
                        }
                        try {
                            parsedData = JSON.parse(value);
                        } catch(err) {
                            testResultEditor.setValue(RED._("expressionEditor.errors.invalid-msg",{message:err.toString()}))
                            return;
                        }

                        try {
                            expr.evaluate(legacyMode?{msg:parsedData}:parsedData, null, (err, result) => {
                                if (err) {
                                    testResultEditor.setValue(RED._("expressionEditor.errors.eval",{message:err.message}),-1);
                                } else {
                                    if (usesContext) {
                                        testResultEditor.setValue(RED._("expressionEditor.errors.context-unsupported"),-1);
                                        return;
                                    }
                                    if (usesEnv) {
                                        testResultEditor.setValue(RED._("expressionEditor.errors.env-unsupported"),-1);
                                        return;
                                    }
                                    if (usesMoment) {
                                        testResultEditor.setValue(RED._("expressionEditor.errors.moment-unsupported"),-1);
                                        return;
                                    }
                                    if (usesClone) {
                                        testResultEditor.setValue(RED._("expressionEditor.errors.clone-unsupported"),-1);
                                        return;
                                    }
        
                                    var formattedResult;
                                    if (result !== undefined) {
                                        formattedResult = JSON.stringify(result,null,4);
                                    } else {
                                        formattedResult = RED._("expressionEditor.noMatch");
                                    }
                                    testResultEditor.setValue(formattedResult,-1);
                                }
                            });
                        } catch(err) {                            
                            testResultEditor.setValue(RED._("expressionEditor.errors.eval",{message:err.message}),-1);
                        }
                    }

                    testDataEditor.getSession().on('change', function() {
                        clearTimeout(changeTimer);
                        changeTimer = setTimeout(testExpression,200);
                        expressionTestCache[expressionTestCacheId] = testDataEditor.getValue();
                    });
                    expressionEditor.getSession().on('change', function() {
                        clearTimeout(changeTimer);
                        changeTimer = setTimeout(testExpression,200);
                    });

                    testResultEditor = RED.editor.createEditor({
                        id: 'red-ui-editor-type-expression-test-result',
                        value: "",
                        stateId: false,
                        focus: false,
                        mode:"ace/mode/json",
                        lineNumbers: false,
                        readOnly: true
                    });
                    panels = RED.panels.create({
                        id:"red-ui-editor-type-expression-panels",
                        resize: function(p1Height,p2Height) {
                            var p1 = $("#red-ui-editor-type-expression-panel-expr");
                            p1Height -= $(p1.children()[0]).outerHeight(true);
                            var editorRow = $(p1.children()[1]);
                            p1Height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
                            $("#red-ui-editor-type-expression").css("height",(p1Height-5)+"px");
                            expressionEditor.resize();

                            var p2 = $("#red-ui-editor-type-expression-panel-info > .form-row > div:first-child");
                            p2Height -= p2.outerHeight(true) + 20;
                            $(".red-ui-editor-type-expression-tab-content").height(p2Height);
                            $("#red-ui-editor-type-expression-test-data").css("height",(p2Height-25)+"px");
                            testDataEditor.resize();
                            $("#red-ui-editor-type-expression-test-result").css("height",(p2Height-25)+"px");
                            testResultEditor.resize();
                        }
                    });

                    $("#node-input-example-reformat").on("click", function(evt) {
                        evt.preventDefault();
                        var v = testDataEditor.getValue()||"";
                        try {
                            v = JSON.stringify(JSON.parse(v),null,4);
                        } catch(err) {
                            // TODO: do an optimistic auto-format
                        }
                        testDataEditor.getSession().setValue(v||"",-1);
                    });

                    testExpression();
                },
                close: function() {
                    if (options.onclose) {
                        options.onclose();
                    }
                    expressionEditor.destroy();
                    testDataEditor.destroy();
                    testResultEditor.destroy();
                },
                show: function() {}
            }
            RED.tray.show(trayOptions);
        }
    }
    RED.editor.registerTypeEditor("_expression", definition);
})();
;RED.editor.iconPicker = (function() {
    function showIconPicker(container, backgroundColor, iconPath, faOnly, done) {
        var picker = $('<div class="red-ui-icon-picker">');
        var searchDiv = $("<div>",{class:"red-ui-search-container"}).appendTo(picker);
        searchInput = $('<input type="text">').attr("placeholder",RED._("editor.searchIcons")).appendTo(searchDiv).searchBox({
            delay: 50,
            change: function() {
                var searchTerm = $(this).val().trim();
                if (searchTerm === "") {
                    iconList.find(".red-ui-icon-list-module").show();
                    iconList.find(".red-ui-icon-list-icon").show();
                } else {
                    iconList.find(".red-ui-icon-list-module").hide();
                    iconList.find(".red-ui-icon-list-icon").each(function(i,n) {
                        if ($(n).data('icon').indexOf(searchTerm) === -1) {
                            $(n).hide();
                        } else {
                            $(n).show();
                        }
                    });
                }
            }
        });

        var row = $('<div>').appendTo(picker);
        var iconList = $('<div class="red-ui-icon-list">').appendTo(picker);
        var metaRow = $('<div class="red-ui-icon-meta"></div>').appendTo(picker);
        var summary = $('<span>').appendTo(metaRow);
        var resetButton = $('<button type="button" class="red-ui-button red-ui-button-small">'+RED._("editor.useDefault")+'</button>').appendTo(metaRow).on("click", function(e) {
            e.preventDefault();
            iconPanel.hide();
            done(null);
        });
        if (!backgroundColor && faOnly) {
            iconList.addClass("red-ui-icon-list-dark");
        }
        setTimeout(function() {
            var iconSets = RED.nodes.getIconSets();
            Object.keys(iconSets).forEach(function(moduleName) {
                if (faOnly && (moduleName !== "font-awesome")) {
                    return;
                }
                var icons = iconSets[moduleName];
                if (icons.length > 0) {
                    // selectIconModule.append($("<option></option>").val(moduleName).text(moduleName));
                    var header = $('<div class="red-ui-icon-list-module"></div>').text(moduleName).appendTo(iconList);
                    $('<i class="fa fa-cube"></i>').prependTo(header);
                    icons.forEach(function(icon) {
                        var iconDiv = $('<div>',{class:"red-ui-icon-list-icon"}).appendTo(iconList);
                        var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(iconDiv);
                        var icon_url = RED.settings.apiRootUrl+"icons/"+moduleName+"/"+icon;
                        iconDiv.data('icon',icon_url);
                        if (backgroundColor) {
                            nodeDiv.css({
                                'backgroundColor': backgroundColor
                            });
                            var borderColor = RED.utils.getDarkerColor(backgroundColor);
                            if (borderColor !== backgroundColor) {
                                nodeDiv.css('border-color',borderColor)
                            }

                        }
                        var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
                        RED.utils.createIconElement(icon_url, iconContainer, true);

                        if (iconPath.module === moduleName && iconPath.file === icon) {
                            iconDiv.addClass("selected");
                        }
                        iconDiv.on("mouseover", function() {
                            summary.text(icon);
                        })
                        iconDiv.on("mouseout", function() {
                            summary.html("&nbsp;");
                        })
                        iconDiv.on("click", function() {
                            iconPanel.hide();
                            done(moduleName+"/"+icon);
                        })
                    })
                }
            });
            setTimeout(function() {
                spinner.remove();
            },50);
        },300);
        var spinner = RED.utils.addSpinnerOverlay(iconList,true);
        var iconPanel = RED.popover.panel(picker);
        iconPanel.show({
            target: container
        })


        picker.slideDown(100);
        searchInput.trigger("focus");
    }
    return {
        show: showIconPicker
    }
})();
;/**
 * 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.
 **/
(function() {


    var template = '<script type="text/x-red" data-template-name="_js"><div class="form-row node-text-editor-row"><div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-js"></div></div></script>';

    var definition = {
        show: function(options) {
            var value = options.value;
            var onCancel = options.cancel;
            var onComplete = options.complete;
            var type = "_js"
            if ($("script[data-template-name='"+type+"']").length === 0) {
                $(template).appendTo("#red-ui-editor-node-configs");
            }
            RED.view.state(RED.state.EDITING);
            var expressionEditor;
            var trayOptions = {
                title: options.title,
                focusElement: options.focusElement,
                width: options.width||"inherit",
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            if (onCancel) { onCancel(); }
                            RED.tray.close();
                        }
                    },
                    {
                        id: "node-dialog-ok",
                        text: RED._("common.label.done"),
                        class: "primary",
                        click: function() {
                            expressionEditor.saveView();
                            if (onComplete) { onComplete(expressionEditor.getValue(), expressionEditor.getCursorPosition(), expressionEditor); }
                            RED.tray.close();
                        }
                    }
                ],
                resize: function(dimensions) {
                    var rows = $("#dialog-form>div:not(.node-text-editor-row)");
                    var editorRow = $("#dialog-form>div.node-text-editor-row");
                    var height = $("#dialog-form").height();
                    for (var i=0;i<rows.size();i++) {
                        height -= $(rows[i]).outerHeight(true);
                    }
                    $(".node-text-editor").css("height",height+"px");
                    expressionEditor.resize();
                },
                open: function(tray) {
                    var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor');
                    expressionEditor = RED.editor.createEditor({
                        id: 'node-input-js',
                        mode: options.mode || 'ace/mode/javascript',
                        stateId: options.stateId,
                        focus: true,
                        value: value,
                        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: options.extraLibs
                    });
                    if (options.cursor && !expressionEditor._initState) {
                        expressionEditor.gotoLine(options.cursor.row+1,options.cursor.column,false);
                    }
                    dialogForm.i18n();
                },
                close: function() {
                    if (options.onclose) {
                        options.onclose();
                    }
                    expressionEditor.destroy();
                },
                show: function() {}
            }
            RED.tray.show(trayOptions);
        }
    }
    RED.editor.registerTypeEditor("_js", definition);

})();
;/**
 * 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.
 **/
(function() {


    // var template = '<script type="text/x-red" data-template-name="_json"></script>';
    var template = '<script type="text/x-red" data-template-name="_json">'+
        '<ul id="red-ui-editor-type-json-tabs"></ul>'+
        '<div id="red-ui-editor-type-json-tab-raw" class="red-ui-editor-type-json-tab-content hide">'+
            '<div class="form-row" style="margin-bottom: 3px; text-align: right;">'+
                '<span class="button-group">'+
                    '<button id="node-input-json-reformat" class="red-ui-button red-ui-button-small"><span data-i18n="jsonEditor.format"></span></button>'+
                '<span class="button-group">'+
            '</div>'+
            '<div class="form-row node-text-editor-row">'+
                '<div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-json"></div>'+
            '</div>'+
        '</div>'+
        '<div id="red-ui-editor-type-json-tab-ui" class="red-ui-editor-type-json-tab-content hide">'+
            '<div id="red-ui-editor-type-json-tab-ui-container"></div>'+
        '</div>'+
    '</script>';

    var activeTab;

    function insertNewItem(parent,index,copyIndex,readOnly) {
        var newValue = "";

        if (parent.children.length > 0) {
            switch (parent.children[Math.max(0,Math.min(parent.children.length-1,copyIndex))].type) {
                case 'string': newValue = ""; break;
                case 'number': newValue = 0; break;
                case 'boolean': newValue = true; break;
                case 'null': newValue = null; break;
                case 'object': newValue = {}; break;
                case 'array': newValue = []; break;
            }
        }
        var newKey;
        if (parent.type === 'array') {
            newKey = parent.children.length;
        } else {
            var usedKeys = {};
            parent.children.forEach(function(child) { usedKeys[child.key] = true })
            var keyRoot = "item";
            var keySuffix = 2;
            newKey = keyRoot;
            while(usedKeys[newKey]) {
                newKey = keyRoot+"-"+(keySuffix++);
            }
        }
        var newItem = handleItem(newKey,newValue,parent.depth+1,parent,readOnly);
        parent.treeList.insertChildAt(newItem, index, true);
        parent.treeList.expand();
    }
    function showObjectMenu(button,item,readOnly) {
        var elementPos = button.offset();
        var options = [];
        if (item.parent) {
            options.push({id:"red-ui-editor-type-json-menu-insert-above", icon:"fa fa-toggle-up", label:RED._('jsonEditor.insertAbove'),onselect:function(){
                var index = item.parent.children.indexOf(item);
                insertNewItem(item.parent,index,index,readOnly);
            }});
            options.push({id:"red-ui-editor-type-json-menu-insert-below", icon:"fa fa-toggle-down", label:RED._('jsonEditor.insertBelow'),onselect:function(){
                var index = item.parent.children.indexOf(item)+1;
                insertNewItem(item.parent,index,index-1,readOnly);
            }});
        }
        if (item.type === 'array' || item.type === 'object') {
            options.push({id:"red-ui-editor-type-json-menu-add-child", icon:"fa fa-plus", label:RED._('jsonEditor.addItem'),onselect:function(){
                insertNewItem(item,item.children.length,item.children.length-1,readOnly);
            }});
        }
        if (item.parent) {
            options.push({id:"red-ui-editor-type-json-menu-copy-path", icon:"fa fa-terminal", label:RED._('jsonEditor.copyPath'),onselect:function(){
                var i = item;
                var path = "";
                var newPath;
                while(i.parent) {
                    if (i.parent.type === "array") {
                        newPath = "["+i.key+"]";
                    } else {
                        if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(i.key)) {
                            newPath = i.key;
                        } else {
                            newPath = "[\""+i.key.replace(/"/,"\\\"")+"\"]"
                        }
                    }
                    path = newPath+(path.length>0 && path[0] !== "["?".":"")+path;
                    i = i.parent;
                }
                RED.clipboard.copyText(path,item.element,"clipboard.copyMessagePath");
            }});

            options.push({id:"red-ui-editor-type-json-menu-duplicate", icon:"fa fa-copy", label:RED._("jsonEditor.duplicate"),onselect:function(){
                var newKey = item.key;
                if (item.parent.type === 'array') {
                    newKey = item.parent.children.length;
                } else {
                    var m = /^(.*?)(-(\d+))?$/.exec(newKey);
                    var usedKeys = {};
                    item.parent.children.forEach(function(child) { usedKeys[child.key] = true })
                    var keyRoot = m[1];
                    var keySuffix = 2;
                    if (m[3] !== undefined) {
                        keySuffix = parseInt(m[3]);
                    }
                    newKey = keyRoot;
                    while(usedKeys[newKey]) {
                        newKey = keyRoot+"-"+(keySuffix++);
                    }
                }
                var newItem = handleItem(newKey,convertToObject(item),item.parent.depth+1,item.parent,readOnly);
                var index = item.parent.children.indexOf(item)+1;

                item.parent.treeList.insertChildAt(newItem, index, true);
                item.parent.treeList.expand();
            }});

            options.push({id:"red-ui-editor-type-json-menu-delete", icon:"fa fa-times", label:RED._('common.label.delete'),onselect:function(){
                item.treeList.remove();
            }});
        }
        if (item.type === 'array' || item.type === 'object') {
            options.push(null)
            options.push({id:"red-ui-editor-type-json-menu-expand-children",icon:"fa fa-angle-double-down", label:RED._('jsonEditor.expandItems'),onselect:function(){
                item.treeList.expand();
                item.children.forEach(function(child) {
                    child.treeList.expand();
                })
            }});
            options.push({id:"red-ui-editor-type-json-menu-collapse-children",icon:"fa fa-angle-double-up", label:RED._('jsonEditor.collapseItems'),onselect:function(){
                item.treeList.collapse();
                item.children.forEach(function(child) {
                    child.treeList.collapse();
                })
            }});
        }

        var menuOptionMenu = RED.menu.init({
            id:"red-ui-editor-type-json-menu",
            options: options
        });
        menuOptionMenu.css({
            position: "absolute"
        })
        menuOptionMenu.on('mouseleave', function(){ $(this).hide() });
        menuOptionMenu.on('mouseup', function() { $(this).hide() });
        menuOptionMenu.appendTo("body");
        var top = elementPos.top;
        var height = menuOptionMenu.height();
        var winHeight = $(window).height();
        if (top+height > winHeight) {
            top -= (top+height)-winHeight + 20;
        }
        menuOptionMenu.css({
            top: top+"px",
            left: elementPos.left+"px"
        })
        menuOptionMenu.show();
    }

    function parseObject(obj,depth,parent,readOnly) {
        var result = [];
        for (var prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                result.push(handleItem(prop,obj[prop],depth,parent,readOnly));
            }
        }
        return result;
    }
    function parseArray(obj,depth,parent,readOnly) {
        var result = [];
        var l = obj.length;
        for (var i=0;i<l;i++) {
            result.push(handleItem(i,obj[i],depth,parent,readOnly));
        }
        return result;
    }
    function handleItem(key,val,depth,parent,readOnly) {
        var item = {depth:depth, type: typeof val};
        var container = $('<span class="red-ui-editor-type-json-editor-label">');
        if (key != null) {
            item.key = key;
            var keyText;
            if (typeof key === 'string') {
                keyText = '"'+key+'"';
            } else {
                keyText = key;
            }
            var keyLabel = $('<span class="red-ui-debug-msg-object-key red-ui-editor-type-json-editor-label-key">').text(keyText).appendTo(container);
            keyLabel.addClass('red-ui-debug-msg-type-'+(typeof key));
            if (parent && parent.type === "array") {
                keyLabel.addClass("red-ui-editor-type-json-editor-label-array-key")
            }
            if(readOnly) {
                keyLabel.addClass("readonly")
            }
            keyLabel.on("click", function(evt) {
                if (item.parent.type === 'array') {
                    return;
                }
                if (readOnly) { return; }
                evt.preventDefault();
                evt.stopPropagation();
                var w = Math.max(150,keyLabel.width());
                var keyInput = $('<input type="text" class="red-ui-editor-type-json-editor-key">').css({width:w+"px"}).val(""+item.key).insertAfter(keyLabel).typedInput({types:['str']});
                $(document).on("mousedown.nr-ui-json-editor", function(evt) {
                    var typedInputElement = keyInput.next(".red-ui-typedInput-container")[0];
                    var target = evt.target;
                    while (target.nodeName !== 'BODY' && target !== typedInputElement && !$(target).hasClass("red-ui-typedInput-options")) {
                        target = target.parentElement;
                    }
                    if (target.nodeName === 'BODY') {
                        var newKey = keyInput.typedInput("value");
                        item.key = newKey;
                        var keyText;
                        if (typeof newKey === 'string') {
                            keyText = '"'+newKey+'"';
                        } else {
                            keyText = newKey;
                        }
                        keyLabel.text(keyText);
                        keyInput.remove();
                        keyLabel.show();
                        $(document).off("mousedown.nr-ui-json-editor");
                        $(document).off("keydown.nr-ui-json-editor");
                    }
                });
                $(document).on("keydown.nr-ui-json-editor",function(evt) {
                    if (evt.keyCode === 27) {
                        // Escape
                        keyInput.remove();
                        keyLabel.show();
                        $(document).off("mousedown.nr-ui-json-editor");
                        $(document).off("keydown.nr-ui-json-editor");
                    }
                });
                keyLabel.hide();
            });
            $('<span>').text(" : ").appendTo(container);
        }

        if (Array.isArray(val)) {
            item.expanded = depth < 2;
            item.type = "array";
            item.deferBuild = depth >= 2;
            item.children = parseArray(val,depth+1,item,readOnly);
        } else if (val !== null && item.type === "object") {
            item.expanded = depth < 2;
            item.children = parseObject(val,depth+1,item,readOnly);
            item.deferBuild = depth >= 2;
        } else {
            item.value = val;
            if (val === null) {
                item.type = 'null'
            }
        }

        var valType;
        var valValue = "";
        var valClass;
        switch(item.type) {
            case 'string': valType = 'str'; valValue = '"'+item.value+'"'; valClass = "red-ui-debug-msg-type-string"; break;
            case 'number': valType = 'num'; valValue = item.value; valClass = "red-ui-debug-msg-type-number";break;
            case 'boolean': valType = 'bool'; valValue = item.value; valClass = "red-ui-debug-msg-type-other";break;
            case 'null': valType = item.type; valValue = item.type; valClass = "red-ui-debug-msg-type-null";break;
            case 'object':
                valType = item.type;
                valValue = item.type;//+"{"+item.children.length+"}";
                valClass = "red-ui-debug-msg-type-meta";
            break;
            case 'array':
                valType = item.type;
                valValue = item.type+"["+item.children.length+"]";
                valClass = "red-ui-debug-msg-type-meta";
            break;
        }
        //
        var orphanedChildren;
        var valueLabel = $('<span class="red-ui-editor-type-json-editor-label-value">').addClass(valClass).text(valValue).appendTo(container);
        if (readOnly) {
            valueLabel.addClass("readonly")
        }
        valueLabel.on("click", function(evt) {
            if (readOnly) { return; }
            evt.preventDefault();
            evt.stopPropagation();
            if (valType === 'str') {
                valValue = valValue.substring(1,valValue.length-1);
            } else if (valType === 'array') {
                valValue = "";
            } else if (valType === 'object') {
                valValue = "";
            }
            var w = Math.max(150,valueLabel.width());
            var val = $('<input type="text" class="red-ui-editor-type-json-editor-value">').css({width:w+"px"}).val(""+valValue).insertAfter(valueLabel).typedInput({
                types:[
                    'str','num','bool',
                    {value:"null",label:RED._("common.type.null"),hasValue:false},
                    {value:"array",label:RED._("common.type.array"),hasValue:false,icon:"red/images/typedInput/json.svg"},
                    {value:"object",label:RED._("common.type.object"),hasValue:false,icon:"red/images/typedInput/json.svg"}
                ],
                default: valType
            });
            $(document).on("mousedown.nr-ui-json-editor", function(evt) {
                var typedInputElement = val.next(".red-ui-typedInput-container")[0];
                var target = evt.target;
                while (target.nodeName !== 'BODY' && target !== typedInputElement && !$(target).hasClass("red-ui-typedInput-options")) {
                    target = target.parentElement;
                }
                if (target.nodeName === 'BODY') {
                    valType = val.typedInput("type");
                    valValue = val.typedInput("value");
                    if (valType === 'num') {
                        valValue = valValue.trim();
                        if (isNaN(valValue)) {
                            valType = 'str';
                        } else if (valValue === "") {
                            valValue = 0;
                        }
                    }
                    item.value = valValue;
                    var valClass;
                    switch(valType) {
                        case 'str':    if (item.children) { orphanedChildren = item.children } item.treeList.makeLeaf(true); item.type = "string";  valClass = "red-ui-debug-msg-type-string"; valValue = '"'+valValue+'"'; break;
                        case 'num':    if (item.children) { orphanedChildren = item.children } item.treeList.makeLeaf(true); item.type = "number";  valClass = "red-ui-debug-msg-type-number"; break;
                        case 'bool':   if (item.children) { orphanedChildren = item.children } item.treeList.makeLeaf(true); item.type = "boolean"; valClass = "red-ui-debug-msg-type-other";  item.value = (valValue === "true"); break;
                        case 'null':   if (item.children) { orphanedChildren = item.children } item.treeList.makeLeaf(true); item.type = "null";    valClass = "red-ui-debug-msg-type-null"; item.value = valValue = "null"; break;
                        case 'object':
                            item.treeList.makeParent(orphanedChildren);
                            item.type = "object";
                            valClass = "red-ui-debug-msg-type-meta";
                            item.value = valValue = "object";
                            item.children.forEach(function(child,i) {
                                if (child.hasOwnProperty('_key')) {
                                    child.key = child._key;
                                    delete child._key;
                                    var keyText;
                                    var keyLabel = child.element.find(".red-ui-editor-type-json-editor-label-key");
                                    keyLabel.removeClass("red-ui-editor-type-json-editor-label-array-key");
                                    if (typeof child.key === 'string') {
                                        keyText = '"'+child.key+'"';
                                        keyLabel.addClass('red-ui-debug-msg-type-string');
                                        keyLabel.removeClass('red-ui-debug-msg-type-number');
                                    } else {
                                        keyText = child.key;
                                        keyLabel.removeClass('red-ui-debug-msg-type-string');
                                        keyLabel.addClass('red-ui-debug-msg-type-number');
                                    }
                                    keyLabel.text(keyText);
                                }
                            })
                            break;
                        case 'array':
                            item.treeList.makeParent(orphanedChildren);
                            item.type = "array";
                            valClass = "red-ui-debug-msg-type-meta";
                            item.value = valValue = "array["+(item.children.length)+"]";
                            item.children.forEach(function(child,i) {
                                child._key = child.key;
                                child.key = i;
                                child.element.find(".red-ui-editor-type-json-editor-label-key")
                                    .addClass("red-ui-editor-type-json-editor-label-array-key")
                                    .text(""+child.key)
                                    .removeClass('red-ui-debug-msg-type-string')
                                    .addClass('red-ui-debug-msg-type-number');
                            })
                            break;
                    }
                    valueLabel.text(valValue).removeClass().addClass("red-ui-editor-type-json-editor-label-value "+valClass);
                    val.remove();
                    valueLabel.show();
                    $(document).off("mousedown.nr-ui-json-editor");
                    $(document).off("keydown.nr-ui-json-editor");
                }
            })

            $(document).on("keydown.nr-ui-json-editor",function(evt) {
                if (evt.keyCode === 27) {
                    // Escape
                    val.remove();
                    valueLabel.show();
                    if (valType === 'str') {
                        valValue = '"'+valValue+'"';
                    }
                    $(document).off("mousedown.nr-ui-json-editor");
                    $(document).off("keydown.nr-ui-json-editor");
                }
            });
            valueLabel.hide();
        })
        item.gutter = $('<span class="red-ui-editor-type-json-editor-item-gutter"></span>');
        if(!readOnly) {
            if (parent) {
                $('<span class="red-ui-editor-type-json-editor-item-handle"><i class="fa fa-bars"></span>').appendTo(item.gutter);
            } else {
                $('<span></span>').appendTo(item.gutter);
            }
            $('<button type="button" class="editor-button editor-button-small"><i class="fa fa-caret-down"></button>').appendTo(item.gutter).on("click", function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                showObjectMenu($(this), item, readOnly);
            });
        }

        item.element = container;
        return item;
    }
    function convertToObject(item) {
        var element;
        switch (item.type) {
            case 'string': element = item.value; break;
            case 'number': element = Number(item.value); break;
            case 'boolean': element = item.value; break;
            case 'null': element = null; break;
            case 'object':
                element = {};
                item.children.forEach(function(child) {
                    element[child.key] = convertToObject(child);
                })
            break;
            case 'array':
                element = item.children.map(function(child) {
                    return convertToObject(child);
                })
            break;
        }
        return element;
    }

    var definition = {
        show: function(options) {
            var value = options.value;
            var onCancel = options.cancel;
            var onComplete = options.complete;
            var type = "_json"
            if ($("script[data-template-name='"+type+"']").length === 0) {
                $(template).appendTo("#red-ui-editor-node-configs");
            }
            RED.view.state(RED.state.EDITING);
            var expressionEditor;
            var changeTimer;

            var checkValid = function() {
                var v = expressionEditor.getValue();
                try {
                    JSON.parse(v);
                    $("#node-dialog-ok").removeClass('disabled');
                    return true;
                } catch(err) {
                    $("#node-dialog-ok").addClass('disabled');
                    return false;
                }
            }
            var rootNode;
            var trayOptions = {
                title: options.title,
                focusElement: options.focusElement,
                width: options.width||700,
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            if (onCancel) { onCancel(); }
                            RED.tray.close();
                        }
                    },
                    {
                        id: "node-dialog-ok",
                        text: RED._("common.label.done"),
                        class: "primary",
                        click: function() {
                            if (options.requireValid && !checkValid()) {
                                return;
                            }
                            var result;
                            if (activeTab === "json-ui") {
                                if (rootNode) {
                                    result = JSON.stringify(convertToObject(rootNode),null,4);
                                } else {
                                    result = expressionEditor.getValue();
                                }
                            } else if (activeTab === "json-raw") {
                                result = expressionEditor.getValue();
                            }
                            expressionEditor.saveView();
                            if (onComplete) { onComplete(result,null,expressionEditor) }
                            RED.tray.close();
                        }
                    }
                ],
                resize: function(dimensions) {
                    var height = $(".red-ui-editor-type-json-tab-content").height();
                    $(".node-text-editor").css("height",(height-45)+"px");
                    expressionEditor.resize();
                },
                open: function(tray) {
                    var trayBody = tray.find('.red-ui-tray-body');
                    var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor');
                    var toolbarButtons = options.toolbarButtons || [];
                    if (toolbarButtons.length) {
                        toolbarButtons.forEach(function (button) {
                            var element = $('<button type="button" class="red-ui-button red-ui-button-small"> </button>')
                                .insertBefore("#node-input-json-reformat")
                                .on("click", function (evt) {
                                    evt.preventDefault();
                                    if (button.click !== undefined) {
                                        button.click.call(element, evt);
                                    }
                                });
                            if (button.id) { element.attr("id", button.id); }
                            if (button.title) { element.attr("title", button.title); }
                            if (button.icon) { element.append($("<i></i>").attr("class", button.icon)); }
                            if (button.label || button.text) {
                                element.append($("<span></span>").text(" " + (button.label || button.text)));
                            }
                        });
                    }
                    var container = $("#red-ui-editor-type-json-tab-ui-container").css({"height":"100%"});
                    var filterDepth = Infinity;
                    var list = $('<div class="red-ui-debug-msg-payload red-ui-editor-type-json-editor">').appendTo(container).treeList({
                        selectable: false,
                        rootSortable: false,
                        sortable: ".red-ui-editor-type-json-editor-item-handle",
                    }).on("treelistchangeparent", function(event, evt) {
                        if (evt.old.type === 'array') {
                            evt.old.element.find(".red-ui-editor-type-json-editor-label-type").text("array["+evt.old.children.length+"]");
                        }
                        if (evt.item.parent.type === 'array') {
                            evt.item.parent.element.find(".red-ui-editor-type-json-editor-label-type").text("array["+evt.item.parent.children.length+"]");
                        }
                    }).on("treelistsort", function(event, item) {
                        item.children.forEach(function(child,i) {
                            if (item.type === 'array') {
                                child.key = i;
                                child.element.find(".red-ui-editor-type-json-editor-label-key")
                                .text(child.key)
                                .removeClass('red-ui-debug-msg-type-string')
                                .addClass('red-ui-debug-msg-type-number');
                            } else {
                                child.element.find(".red-ui-editor-type-json-editor-label-key")
                                    .text('"'+child.key+'"')
                                    .removeClass('red-ui-debug-msg-type-number')
                                    .addClass('red-ui-debug-msg-type-string');
                            }
                        })
                    });

                    expressionEditor = RED.editor.createEditor({
                        id: 'node-input-json',
                        value: value||"",
                        mode:"ace/mode/json",
                        readOnly: !!options.readOnly,
                        stateId: options.stateId,
                        focus: true
                    });

                    if (options.requireValid) {
                        expressionEditor.getSession().on('change', function() {
                            clearTimeout(changeTimer);
                            changeTimer = setTimeout(checkValid,200);
                        });
                        checkValid();
                    }
                    $("#node-input-json-reformat").on("click", function(evt) {
                        evt.preventDefault();
                        var v = expressionEditor.getValue()||"";
                        try {
                            v = JSON.stringify(JSON.parse(v),null,4);
                        } catch(err) {
                            // TODO: do an optimistic auto-format
                        }
                        expressionEditor.getSession().setValue(v||"",-1);
                    });
                    dialogForm.i18n();

                    var finishedBuild = false;
                    var tabs = RED.tabs.create({
                        element: $("#red-ui-editor-type-json-tabs"),
                        onchange:function(tab) {
                            activeTab = tab.id;
                            $(".red-ui-editor-type-json-tab-content").hide();
                            if (finishedBuild) {
                                if (tab.id === "json-raw") {
                                    if (rootNode) {
                                        var result = JSON.stringify(convertToObject(rootNode),null,4);
                                        expressionEditor.getSession().setValue(result||"",-1);
                                    }

                                } else if (tab.id === "json-ui") {
                                    var raw = expressionEditor.getValue().trim() ||"{}";
                                    try {
                                        var parsed = JSON.parse(raw);
                                        rootNode = handleItem(null,parsed,0,null,options.readOnly);
                                        rootNode.class = "red-ui-editor-type-json-root-node"
                                        list.treeList('data',[rootNode]);
                                    } catch(err) {
                                        rootNode = null;
                                        list.treeList('data',[{
                                            label: RED._("jsonEditor.error.invalidJSON")+err.toString()
                                        }]);
                                    }
                                }
                            }
                            tab.content.show();
                            trayOptions.resize();
                        }
                    })

                    tabs.addTab({
                        id: 'json-raw',
                        label: options.readOnly ? RED._('jsonEditor.rawMode-readonly') : RED._('jsonEditor.rawMode'),
                        content: $("#red-ui-editor-type-json-tab-raw")
                    });
                    tabs.addTab({
                        id: 'json-ui',
                        label: options.readOnly ? RED._('jsonEditor.uiMode-readonly') : RED._('jsonEditor.uiMode'),
                        content: $("#red-ui-editor-type-json-tab-ui")
                    });
                    finishedBuild = true;
                },
                close: function() {
                    if (options.onclose) {
                        options.onclose();
                    }
                    expressionEditor.destroy();
                },
                show: function() {}
            }
            RED.tray.show(trayOptions);
        }
    }
    RED.editor.registerTypeEditor("_json", definition);
})();
;/**
 * 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.
 **/
(function() {
    /**
     * Converts dropped image file to date URL
     */
    function file2base64Image(file, cb) {
        var reader = new FileReader();
        reader.onload = (function (fd) {
            return function (e) {
                cb(e.target.result);
            };
        })(file);
        reader.readAsDataURL(file);
    }

    var initialized = false;
    var currentEditor = null;
    /**
     * Initialize handler for image file drag events
     */
    function initImageDrag(elem, editor) {
        $(elem).on("dragenter", function (ev) {
            ev.preventDefault();
            $("#red-ui-image-drop-target").css({display:'table'}).focus();
            currentEditor = editor;
        });

        if (!initialized) {
            initialized = true;
            $("#red-ui-image-drop-target").on("dragover", function (ev) {
                ev.preventDefault();
            }).on("dragleave", function (ev) {
                $("#red-ui-image-drop-target").hide();
            }).on("drop", function (ev) {
                ev.preventDefault();
                if ($.inArray("Files",ev.originalEvent.dataTransfer.types) != -1) {
                    var files = ev.originalEvent.dataTransfer.files;
                    if (files.length === 1) {
                        var file = files[0];
                        var name = file.name.toLowerCase();

                        if (name.match(/\.(apng|avif|gif|jpeg|png|svg|webp)$/)) {
                            file2base64Image(file, function (image) {
                                var session = currentEditor.getSession();
                                var img = `<img src="${image}"/>\n`;
                                var pos = session.getCursorPosition();
                                session.insert(pos, img);
                                $("#red-ui-image-drop-target").hide();
                            });
                            return;
                        }
                    }
                }
                $("#red-ui-image-drop-target").hide();
            });
        }
    }

    var toolbarTemplate = '<div style="margin-bottom: 5px">'+
        '<span class="button-group">'+
        '<button type="button" class="red-ui-button" data-style="h1" style="font-size:1.1em; font-weight: bold">h1</button>'+
        '<button type="button" class="red-ui-button" data-style="h2" style="font-size:1.0em; font-weight: bold">h2</button>'+
        '<button type="button" class="red-ui-button" data-style="h3" style="font-size:0.9em; font-weight: bold">h3</button>'+
        '</span>'+
        '<span class="button-group">'+
            '<button type="button" class="red-ui-button" data-style="b"><i class="fa fa-bold"></i></button>'+
            '<button type="button" class="red-ui-button" data-style="i"><i class="fa fa-italic"></i></button>'+
            '<button type="button" class="red-ui-button" data-style="code"><i class="fa fa-code"></i></button>'+
        '</span>'+
        '<span class="button-group">'+
            '<button type="button" class="red-ui-button" data-style="ol"><i class="fa fa-list-ol"></i></button>'+
            '<button type="button" class="red-ui-button" data-style="ul"><i class="fa fa-list-ul"></i></button>'+
            '<button type="button" class="red-ui-button" data-style="bq"><i class="fa fa-quote-left"></i></button>'+
            '<button type="button" class="red-ui-button" data-style="hr"><i class="fa fa-minus"></i></button>'+
            '<button type="button" class="red-ui-button" data-style="link"><i class="fa fa-link"></i></button>'+
        '</span>'+
    '</div>';

    var template = '<script type="text/x-red" data-template-name="_markdown">'+
        '<div id="red-ui-editor-type-markdown-panels">'+
        '<div id="red-ui-editor-type-markdown-panel-editor" class="red-ui-panel">'+
            '<div style="height: 100%; margin: auto;">'+
                '<div id="red-ui-editor-type-markdown-toolbar"></div>'+
                '<div class="node-text-editor" style="height: 100%" id="red-ui-editor-type-markdown"></div>'+
            '</div>'+
        '</div>'+
        '<div class="red-ui-panel">'+
            '<div class="red-ui-editor-type-markdown-panel-preview red-ui-help"></div>'+
        '</div>'+
        '</script>';


    var panels;

    var definition = {
        show: function(options) {
            var value = options.value;
            var onCancel = options.cancel;
            var onComplete = options.complete;
            var type = "_markdown"
            if ($("script[data-template-name='"+type+"']").length === 0) {
                $(template).appendTo("#red-ui-editor-node-configs");
            }

            RED.view.state(RED.state.EDITING);
            var expressionEditor;

            var trayOptions = {
                title: options.title,
                focusElement: options.focusElement,
                width: options.width||Infinity,
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            if (onCancel) { onCancel(); }
                            RED.tray.close();
                        }
                    },
                    {
                        id: "node-dialog-ok",
                        text: RED._("common.label.done"),
                        class: "primary",
                        click: function() {
                            expressionEditor.saveView();
                            if (onComplete) { onComplete(expressionEditor.getValue(),expressionEditor.getCursorPosition(), expressionEditor); }
                            RED.tray.close();
                        }
                    }
                ],
                resize: function(dimensions) {
                    var width = $("#dialog-form").width();
                    if (panels) {
                        panels.resize(width);
                    }

                },
                open: function(tray) {
                    var trayBody = tray.find('.red-ui-tray-body');
                    trayBody.addClass("red-ui-editor-type-markdown-editor")
                    var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor');
                    expressionEditor = RED.editor.createEditor({
                        id: 'red-ui-editor-type-markdown',
                        value: value,
                        stateId: options.stateId,
                        focus: true,
                        mode:"ace/mode/markdown",
                        expandable: false
                    });
                    var changeTimer;
                    expressionEditor.getSession().on("change", function() {
                        clearTimeout(changeTimer);
                        changeTimer = setTimeout(function() {
                            var currentScrollTop = $(".red-ui-editor-type-markdown-panel-preview").scrollTop();
                            $(".red-ui-editor-type-markdown-panel-preview").html(RED.utils.renderMarkdown(expressionEditor.getValue()));
                            $(".red-ui-editor-type-markdown-panel-preview").scrollTop(currentScrollTop);
                            RED.editor.mermaid.render()
                        },200);
                    })
                    if (options.header) {
                        options.header.appendTo(tray.find('#red-ui-editor-type-markdown-title'));
                    }

                    if (value) {
                        $(".red-ui-editor-type-markdown-panel-preview").html(RED.utils.renderMarkdown(expressionEditor.getValue()));
                        RED.editor.mermaid.render()
                    }
                    panels = RED.panels.create({
                        id:"red-ui-editor-type-markdown-panels",
                        dir: "horizontal",
                        resize: function(p1Width,p2Width) {
                            expressionEditor.resize();
                        }
                    });
                    panels.ratio(1);

                    $('<span class="button-group" style="float:right">'+
                        '<button type="button" id="node-btn-markdown-preview" class="red-ui-button toggle single"><i class="fa fa-eye"></i></button>'+
                    '</span>').appendTo(expressionEditor.toolbar);

                    $("#node-btn-markdown-preview").on("click", function(e) {
                        e.preventDefault();
                        if ($(this).hasClass("selected")) {
                            $(this).removeClass("selected");
                            panels.ratio(1);
                        } else {
                            $(this).addClass("selected");
                            panels.ratio(0.5);
                        }
                    });
                    RED.popover.tooltip($("#node-btn-markdown-preview"), RED._("markdownEditor.toggle-preview"));

                    if(!expressionEditor._initState) {
                        if (options.cursor) {
                            expressionEditor.gotoLine(options.cursor.row+1,options.cursor.column,false);
                        }
                        else {
                            expressionEditor.gotoLine(0, 0, false);
                        }
                    }                        
                    dialogForm.i18n();
                },
                close: function() {
                    if (options.onclose) {
                        options.onclose();
                    }
                    expressionEditor.destroy();
                },
                show: function() {}
            }
            RED.tray.show(trayOptions);
        },

        buildToolbar: function(container, editor) {
            var styleActions = {
                'h1': { newline: true, before:"# ", tooltip:RED._("markdownEditor.heading1")},
                'h2': { newline: true, before:"## ", tooltip:RED._("markdownEditor.heading2")},
                'h3': { newline: true, before:"### ", tooltip:RED._("markdownEditor.heading3")},
                'b': { before:"**", after: "**", tooltip: RED._("markdownEditor.bold")},
                'i': { before:"_", after: "_", tooltip: RED._("markdownEditor.italic")},
                'code': { before:"`", after: "`", tooltip: RED._("markdownEditor.code")},
                'ol': { before:" 1. ", newline: true, tooltip: RED._("markdownEditor.ordered-list")},
                'ul': { before:" - ", newline: true, tooltip: RED._("markdownEditor.unordered-list")},
                'bq': { before:"> ", newline: true, tooltip: RED._("markdownEditor.quote")},
                'link': { before:"[", after: "]()", tooltip: RED._("markdownEditor.link")},
                'hr': { before:"\n---\n\n", tooltip: RED._("markdownEditor.horizontal-rule")}
            }
            var toolbar = $(toolbarTemplate).appendTo(container);
            toolbar.find('button[data-style]').each(function(el) {
                var style = styleActions[$(this).data('style')];
                $(this).on("click", function(e) {
                    e.preventDefault();
                    var current = editor.getSelectedText();
                    var range = editor.selection.getRange();
                    if (style.newline) {
                        var offset = 0;
                        var beforeOffset = ((style.before||"").match(/\n/g)||[]).length;
                        var afterOffset = ((style.after||"").match(/\n/g)||[]).length;
                        for (var i = range.start.row; i<= range.end.row+offset; i++) {
                            if (style.before) {
                                editor.session.insert({row:i, column:0},style.before);
                                offset += beforeOffset;
                                i += beforeOffset;
                            }
                            if (style.after) {
                                editor.session.insert({row:i, column:Infinity},style.after);
                                offset += afterOffset;
                                i += afterOffset;
                            }
                        }
                    } else {
                        editor.session.replace(editor.selection.getRange(), (style.before||"")+current+(style.after||""));
                        if (current === "") {
                            editor.gotoLine(range.start.row+1,range.start.column+(style.before||"").length,false);
                        }
                    }
                    editor.focus();
                });
                if (style.tooltip) {
                    RED.popover.tooltip($(this),style.tooltip);
                }
            })
            return toolbar;
        },
        postInit: function (editor, options) {
            var elem = $("#"+options.id);
            initImageDrag(elem, editor);
         }
    }
    RED.editor.registerTypeEditor("_markdown", definition);
})();
;RED.editor.mermaid = (function () {
    let initializing = false
    let loaded = false
    let pendingEvals = []
    let diagramIds = 0

    function render(selector = '.mermaid') {
        // $(selector).hide()
        if (!loaded) {
            pendingEvals.push(selector)
            
            if (!initializing) {
                initializing = true
                // Find the cache-buster:
                let cacheBuster
                $('script').each(function (i, el) { 
                    if (!cacheBuster) {
                        const src = el.getAttribute('src')
                        const m = /\?v=(.+)$/.exec(src)
                        if (m) {
                            cacheBuster = m[1]
                        }
                    }
                })
                $.ajax({
                    url: `vendor/mermaid/mermaid.min.js?v=${cacheBuster}`,
                    dataType: "script",
                    cache: true,
                    success: function (data, stat, jqxhr) {
                        mermaid.initialize({
                            startOnLoad: false,
                            theme: RED.settings.get('mermaid', {}).theme
                        })
                        loaded = true
                        while(pendingEvals.length > 0) {
                            const pending = pendingEvals.shift()
                            render(pending)
                        }
                    }
                });
            }
        } else {
            const nodes = document.querySelectorAll(selector)

            nodes.forEach(async node => {
                if (!node.getAttribute('mermaid-processed')) {
                    const mermaidContent = node.innerText
                    node.setAttribute('mermaid-processed', true)
                    try {
                        const { svg } = await mermaid.render('mermaid-render-'+Date.now()+'-'+(diagramIds++), mermaidContent);
                        node.innerHTML = svg
                    } catch (err) {
                        $('<div>').css({
                            fontSize: '0.8em',
                            border: '1px solid var(--red-ui-border-color-error)',
                            padding: '5px',
                            marginBottom: '10px',
                        }).text(err.toString()).prependTo(node)
                    }
                }
            })
        }
    }  
    return {
        render: render,
    };
})();
;/**
 * 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.
 **/
(function() {


    var template = '<script type="text/x-red" data-template-name="_text"><div class="form-row node-text-editor-row"><div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-text"></div></div></script>';

    var definition = {
        show: function(options) {
            var value = options.value;
            var onCancel = options.cancel;
            var onComplete = options.complete;
            var type = "_text"
            if ($("script[data-template-name='"+type+"']").length === 0) {
                $(template).appendTo("#red-ui-editor-node-configs");
            }
            RED.view.state(RED.state.EDITING);
            var expressionEditor;
            var trayOptions = {
                title: options.title,
                focusElement: options.focusElement,
                width: options.width||"inherit",
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            if(onCancel) { onCancel(); }
                            RED.tray.close();
                        }
                    },
                    {
                        id: "node-dialog-ok",
                        text: RED._("common.label.done"),
                        class: "primary",
                        click: function() {
                            expressionEditor.saveView();
                            if (onComplete) { onComplete(expressionEditor.getValue(),expressionEditor.getCursorPosition(),expressionEditor);}
                            RED.tray.close();
                        }
                    }
                ],
                resize: function(dimensions) {
                    var rows = $("#dialog-form>div:not(.node-text-editor-row)");
                    var editorRow = $("#dialog-form>div.node-text-editor-row");
                    var height = $("#dialog-form").height();
                    $(".node-text-editor").css("height",height+"px");
                    expressionEditor.resize();
                },
                open: function(tray) {
                    var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor');
                    expressionEditor = RED.editor.createEditor({
                        id: 'node-input-text',
                        value: value||"",
                        stateId: options.stateId,
                        mode:"ace/mode/"+(options.mode||"text"),
                        focus: true,
                    });
                    if (options.cursor && !expressionEditor._initState) {
                        expressionEditor.gotoLine(options.cursor.row+1,options.cursor.column,false);
                    }
                },
                close: function() {
                    if (options.onclose) {
                        options.onclose();
                    }
                    expressionEditor.destroy();
                },
                show: function() {}
            }
            RED.tray.show(trayOptions);
        }
    }
    RED.editor.registerTypeEditor("_text", definition);
})();
;/*
* 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.codeEditor.ace
*/
RED.editor.codeEditor.ace = (function() {

    const type = "ace";
    var initialised = false;
    var initOptions = {};

    function init(options) {
        initOptions = options || {}; 
        initialised = true;
        return initialised;
    }

    function create(options) {
        var editorSettings = RED.editor.codeEditor.settings || {};
        var el = options.element || $("#"+options.id)[0];
        var toolbarRow = $("<div>").appendTo(el);
        el = $("<div>").appendTo(el).addClass("red-ui-editor-text-container")[0];
        var editor = window.ace.edit(el);
        editor.setTheme(editorSettings.theme || initOptions.theme || "ace/theme/tomorrow");
        var session = editor.getSession();
        session.on("changeAnnotation", function () {
            var annotations = session.getAnnotations() || [];
            var i = annotations.length;
            var len = annotations.length;
            while (i--) {
                if (/doctype first\. Expected/.test(annotations[i].text)) { annotations.splice(i, 1); }
                else if (/Unexpected End of file\. Expected/.test(annotations[i].text)) { annotations.splice(i, 1); }
            }
            if (len > annotations.length) { session.setAnnotations(annotations); }
        });
        if (options.mode) {
            session.setMode(options.mode);
        }
        if (options.foldStyle) {
            session.setFoldStyle(options.foldStyle);
        } else {
            session.setFoldStyle('markbeginend');
        }
        if (options.options) {
            editor.setOptions(options.options);
        } else {
            editor.setOptions({
                enableBasicAutocompletion:true,
                enableSnippets:true,
                tooltipFollowsMouse: false
            });
        }
        if (options.readOnly) {
            editor.setOption('readOnly',options.readOnly);
            editor.container.classList.add("ace_read-only");
        }
        if (options.hasOwnProperty('lineNumbers')) {
            editor.renderer.setOption('showGutter',options.lineNumbers);
        }
        editor.$blockScrolling = Infinity;
        if (options.value) {
            session.setValue(options.value,-1);
        }
        if (options.globals) {
            setTimeout(function() {
                if (!!session.$worker) {
                    session.$worker.send("setOptions", [{globals: options.globals, maxerr:1000}]);
                }
            },100);
        }
        if (!options.stateId && options.stateId !== false) {
            options.stateId = RED.editor.generateViewStateId("ace", options, (options.mode || options.title).split("/").pop());
        }
        if (options.mode === 'ace/mode/markdown') {
            $(el).addClass("red-ui-editor-text-container-toolbar");
            editor.toolbar = RED.editor.customEditTypes['_markdown'].buildToolbar(toolbarRow,editor);
            if (options.expandable !== false) {
                var expandButton = $('<button type="button" class="red-ui-button" style="float: right;"><i class="fa fa-expand"></i></button>').appendTo(editor.toolbar);
                RED.popover.tooltip(expandButton, RED._("markdownEditor.expand"));
                expandButton.on("click", function(e) {
                    e.preventDefault();
                    var value = editor.getValue();
                    RED.editor.editMarkdown({
                        value: value,
                        width: "Infinity",
                        stateId: options.stateId,
                        focus: true,
                        cancel: function () {
                            editor.focus();
                        },
                        complete: function(v,cursor) {
                            editor.setValue(v, -1);
                            setTimeout(function() {
                                editor.restoreView();
                                editor.focus();
                            },300);
                        }
                    })
                });
            }
            var helpButton = $('<button type="button" class="red-ui-editor-text-help red-ui-button red-ui-button-small"><i class="fa fa-question"></i></button>').appendTo($(el).parent());
            RED.popover.create({
                target: helpButton,
                trigger: 'click',
                size: "small",
                direction: "left",
                content: RED._("markdownEditor.format"),
                autoClose: 50
            });
            session.setUseWrapMode(true);
        }
        editor._destroy = editor.destroy;
        editor.destroy = function() {
            try {
                editor.saveView();
                editor._initState = null;
                this._destroy();
            } catch (e) { }
            $(el).remove();
            $(toolbarRow).remove();
        }
        editor.on("blur", function () {
            editor.focusMemory = false;
            editor.saveView();
        })
        editor.on("focus", function () {
            if (editor._initState) {
                editor.restoreView(editor._initState);
                editor._initState = null;
            }
        })
        editor.getView = function () {
            var session = editor.getSession();
            return {
                selection: session.selection.toJSON(),
                scrollTop: session.getScrollTop(),
                scrollLeft: session.getScrollLeft(),
                options: session.getOptions()
            }
        }
        editor.saveView = function () {
            if (!options.stateId) { return; } //only possible if created with a unique stateId
            window._editorStateAce = window._editorStateAce || {};
            var state = editor.getView();
            window._editorStateAce[options.stateId] = state;
            return state;
        }
        editor.restoreView = function (state) {
            if (!options.stateId) { return; } //only possible if created with a unique stateId
            window._editorStateAce = window._editorStateAce || {};
            var _state = state || window._editorStateAce[options.stateId];
            if (!_state) { return; } //no view state available
            try {
                var session = editor.getSession();
                session.setOptions(_state.options);
                session.selection.fromJSON(_state.selection);
                session.setScrollTop(_state.scrollTop);
                session.setScrollLeft(_state.scrollLeft);
                editor._initState = _state;
            } catch (error) {
                delete window._editorStateMonaco[options.stateId];
            }
        };
        editor.restoreView();
        editor.type = type;
        return editor;
    }

    return {
        /**
         * Editor type
         * @memberof RED.editor.codeEditor.ace
         */
         get type() { return type; },
         /**
          * Editor initialised
         * @memberof RED.editor.codeEditor.ace
         */
        get initialised() { return initialised; },
        /**
         * Initialise code editor
         * @param {object} options - initialisation options
         * @memberof RED.editor.codeEditor.ace
         */
         init: init,
         /**
          * Create a code editor
          * @param {object} options - the editor options
          * @memberof RED.editor.codeEditor.ace
          */
         create: create
    }
})();;/*
* 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.codeEditor.monaco
*/

/*
    The code editor currenlty supports 2 functions init and create.
    * Init() - setup the editor / must return true
    * Create() - create an editor instance / returns an editor as generated by the editor lib
    * To be compatable with the original ace lib (for contrib nodes using it), the object returned by create() must (at minimum) support the following...
        property .selection = {};
        function .selection.getRange();
        property .session //the editor object
        function .session.insert = function(position, text)
        function .setReadOnly(readOnly)
        property .renderer = {};
        function .renderer.updateFull()
        function setMode(mode, cb)
        function getRange();
        function replace(range, text)
        function selectAll
        function clearSelection
        function getSelectedText()
        function destroy()
        function resize()
        function getSession()
        function getLength()
        function scrollToLine(lineNumber, scrollType)
        function moveCursorTo(lineNumber, colNumber)
        function getAnnotations()
        function gotoLine(row, col)
        function getCursorPosition()
        function setTheme(name)
        function setFontSize(size:Number) //Set a new font size (in pixels) for the editor text.
        function on(name, cb)
        function getUndoManager()

*/

RED.editor.codeEditor.monaco = (function() {
    var initialised = false;
    const type = "monaco";
    const monacoThemes = ["vs","vs-dark","hc-black"]; //TODO: consider setting hc-black autmatically based on acessability?
    let userSelectedTheme;

    //TODO: get from externalModules.js  For now this is enough for feature parity with ACE (and then some).
    const knownModules = {
        "assert": {package: "node", module: "assert", path: "node/assert.d.ts" },
        "assert/strict": {package: "node", module: "assert/strict", path: "node/assert/strict.d.ts" },
        "async_hooks": {package: "node", module: "async_hooks", path: "node/async_hooks.d.ts" },
        "buffer": {package: "node", module: "buffer", path: "node/buffer.d.ts" },
        "child_process": {package: "node", module: "child_process", path: "node/child_process.d.ts" },
        "cluster": {package: "node", module: "cluster", path: "node/cluster.d.ts" },
        "console": {package: "node", module: "console", path: "node/console.d.ts" },
        "crypto": {package: "node", module: "crypto", path: "node/crypto.d.ts" },
        "dgram": {package: "node", module: "dgram", path: "node/dgram.d.ts" },
        "diagnostics_channel.d": {package: "node", module: "diagnostics_channel", path: "node/diagnostics_channel.d.ts" },
        "dns": {package: "node", module: "dns", path: "node/dns.d.ts" },
        "dns/promises": {package: "node", module: "dns/promises", path: "node/dns/promises.d.ts" },
        "domain": {package: "node", module: "domain", path: "node/domain.d.ts" },
        "events": {package: "node", module: "events", path: "node/events.d.ts" },
        "fs": {package: "node", module: "fs", path: "node/fs.d.ts" },
        "fs/promises": {package: "node", module: "fs/promises", path: "node/fs/promises.d.ts" },
        "globals": {package: "node", module: "globals", path: "node/globals.d.ts" },
        "http": {package: "node", module: "http", path: "node/http.d.ts" },
        "http2": {package: "node", module: "http2", path: "node/http2.d.ts" },
        "https": {package: "node", module: "https", path: "node/https.d.ts" },
        "module": {package: "node", module: "module", path: "node/module.d.ts" },
        "net": {package: "node", module: "net", path: "node/net.d.ts" },
        "os": {package: "node", module: "os", path: "node/os.d.ts" },
        "path": {package: "node", module: "path", path: "node/path.d.ts" },
        "perf_hooks": {package: "node", module: "perf_hooks", path: "node/perf_hooks.d.ts" },
        "process": {package: "node", module: "process", path: "node/process.d.ts" },
        "querystring": {package: "node", module: "querystring", path: "node/querystring.d.ts" },
        "readline": {package: "node", module: "readline", path: "node/readline.d.ts" },
        "stream": {package: "node", module: "stream", path: "node/stream.d.ts" },
        "stream/consumers": {package: "node", module: "stream/consumers", path: "node/stream/consumers.d.ts" },
        "stream/promises": {package: "node", module: "stream/promises", path: "node/stream/promises.d.ts" },
        "stream/web": {package: "node", module: "stream/web", path: "node/stream/web.d.ts" },
        "string_decoder": {package: "node", module: "string_decoder", path: "node/string_decoder.d.ts" },
        "test": {package: "node", module: "test", path: "node/test.d.ts" },
        "timers": {package: "node", module: "timers", path: "node/timers.d.ts" },
        "timers/promises": {package: "node", module: "timers/promises", path: "node/timers/promises.d.ts" },
        "tls": {package: "node", module: "tls", path: "node/tls.d.ts" },
        "trace_events": {package: "node", module: "trace_events", path: "node/trace_events.d.ts" },
        "tty": {package: "node", module: "tty", path: "node/tty.d.ts" },
        "url": {package: "node", module: "url", path: "node/url.d.ts" },
        "util": {package: "node", module: "util", path: "node/util.d.ts" },
        "v8": {package: "node", module: "v8", path: "node/v8.d.ts" },
        "vm": {package: "node", module: "vm", path: "node/vm.d.ts" },
        "wasi": {package: "node", module: "wasi", path: "node/wasi.d.ts" },
        "worker_threads": {package: "node", module: "worker_threads", path: "node/worker_threads.d.ts" },
        "zlib": {package: "node", module: "zlib", path: "node/zlib.d.ts" },
        "node-red": {package: "node-red", module: "node-red", path: "node-red/index.d.ts" }, //only used if node-red types are concated by grunt.
        "node-red-util": {package: "node-red", module: "util", path: "node-red/util.d.ts" },
        "node-red-func": {package: "node-red", module: "func", path: "node-red/func.d.ts" },
    }
    const defaultServerSideTypes = [ knownModules["node-red-util"], knownModules["node-red-func"], knownModules["globals"], knownModules["console"], knownModules["buffer"], knownModules["timers"] , knownModules["util"] ];

    const modulesCache = {};

    /**
     * Helper function to load/reload types.
     * @param {string} mod - type lib to load. Only known libs are currently supported.
     * @param {boolean} preloadOnly - only cache the lib
     * @param {object} loadedLibs - an object used to track loaded libs (needed to destroy them upon close)
     */
    function _loadModuleDTS(mod, preloadOnly, loadedLibs, cb) {
        var _module;
        if(typeof mod == "object") {
            _module = mod;
        } else {
            _module = knownModules[mod];
        }
        if(_module) {
            const libPackage = _module.package;
            const libModule = _module.module;
            const libPath = _module.path;
            const def = modulesCache[libPath];
            if( def ) {
                if(!preloadOnly) {
                    loadedLibs.JS[libModule] = monaco.languages.typescript.javascriptDefaults.addExtraLib(def, "file://types/" + libPackage + "/" + libModule + "/index.d.ts");
                }
                if(cb) {
                    setTimeout(function() {
                        cb(null, _module);
                    }, 5);
                }
            } else {
                var typePath = "types/" + libPath;
                $.get(typePath)
                .done(function(data) {
                    modulesCache[libPath] =  data;
                    if(!preloadOnly) {
                        loadedLibs.JS[libModule] = monaco.languages.typescript.javascriptDefaults.addExtraLib(data, "file://types/" + libPackage + "/" + libModule + "/index.d.ts");
                    }
                    if(cb) { cb(null, _module) }
                })
                .fail(function(err) {
                    var warning = "Failed to load '" + typePath + "'";
                    modulesCache[libPath] = "/* " + warning + " */\n"; //populate the extraLibs cache to revent retries
                    if(cb) { cb(err, _module) }
                    console.warn(warning);
                });
            }
        }
    }


    function init(options) {

        //Handles orphaned models
        //ensure loaded models that are not explicitly destroyed by a call to .destroy() are disposed
        RED.events.on("editor:close",function() {
            if (!window.monaco) { return; }
            const editors = window.monaco.editor.getEditors()
            const orphanEditors = editors.filter(editor => editor && !document.body.contains(editor.getDomNode()))
            orphanEditors.forEach(editor => {
                editor.dispose();
            });
            let models = monaco.editor.getModels()
            if(models && models.length) {
                console.warn("Cleaning up monaco models left behind. Any node that calls createEditor() should call .destroy().")
                for (let index = 0; index < models.length; index++) {
                    const model = models[index];
                    if(!model.isDisposed()) {
                        model.dispose();
                    }
                }
            }
        });

        options = options || {};
        window.MonacoEnvironment = window.MonacoEnvironment || {};
        window.MonacoEnvironment.getWorkerUrl = window.MonacoEnvironment.getWorkerUrl || function (moduleId, label) {
            if (label === 'json') { return './vendor/monaco/dist/json.worker.js'; }
            if (label === 'css' || label === 'scss') { return './vendor/monaco/dist/css.worker.js'; }
            if (label === 'html' || label === 'handlebars') { return './vendor/monaco/dist/html.worker.js'; }
            if (label === 'typescript' || label === 'javascript') { return './vendor/monaco/dist/ts.worker.js'; }
            return './vendor/monaco/dist/editor.worker.js';
        };

        var editorSettings = RED.editor.codeEditor.settings || {};
        var editorOptions = editorSettings.options || {};

        //if editorOptions.theme is an object (set in theme.js context()), use the plugin theme name as the monaco theme name
        //if editorOptions.theme is a string, it should be the name of a pre-set theme, load that
        try {
            const addTheme = function (themeThemeName, theme) {
                if ((theme.rules && Array.isArray(theme.rules)) || theme.colors) {
                    monacoThemes.push(themeThemeName); //add to list of loaded themes
                    monaco.editor.defineTheme(themeThemeName, theme);
                    monaco.editor.setTheme(themeThemeName);
                    userSelectedTheme = themeThemeName;
                }
            };
            if (editorOptions.theme) {
                if (typeof editorOptions.theme == "object" && RED.settings.editorTheme.theme) {
                    let themeThemeName = editorOptions.theme.name || RED.settings.editorTheme.theme;
                    addTheme(themeThemeName, editorOptions.theme);
                } else if (typeof editorOptions.theme == "string") {
                    let themeThemeName = editorOptions.theme;
                    if (!monacoThemes.includes(themeThemeName)) {
                        $.get('vendor/monaco/dist/theme/' + themeThemeName + '.json', function (theme) {
                            addTheme(themeThemeName, theme);
                        });
                    }
                }
            }
        } catch (error) {
            console.warn(error);
        }


        //Helper function to simplify snippet setup
        function createMonacoCompletionItem(label, insertText, documentation, range, kind) {
            if (Array.isArray(documentation)) { documentation = documentation.join("\n"); }
            return {
                label: label,
                kind: kind == null ? monaco.languages.CompletionItemKind.Snippet : kind,
                documentation: { value: documentation },
                insertText: insertText,
                insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
                range: range
            }
        }

        function setupJSONata(_monaco) {
            // Register the language 'jsonata'
            _monaco.languages.register({ id: 'jsonata' });

            // Setup tokens for JSONata
            _monaco.languages.setMonarchTokensProvider('jsonata',
                {
                    // Set defaultToken to invalid to see what you do not tokenize yet
                    defaultToken: 'invalid',
                    tokenPostfix: '.js',
                    keywords: ["function", "true", "true", "null", "Infinity", "NaN", "undefined"].concat(Object.keys(jsonata.functions)),
                    // keywords: [
                    //     "function", "$abs", "$append", "$assert", "$average",
                    //     "$base64decode", "$base64encode", "$boolean", "$ceil", "$contains",
                    //     "$count", "$decodeUrl", "$decodeUrlComponent", "$distinct", "$each", "$encodeUrl",
                    //     "$encodeUrlComponent", "$env", "$error", "$eval", "$exists", "$filter", "$floor",
                    //     "$flowContext", "$formatBase", "$formatInteger", "$formatNumber", "$fromMillis",
                    //     "$globalContext", "$join", "$keys", "$length", "$lookup", "$lowercase", "$map",
                    //     "$match", "$max", "$merge", "$millis", "$min", "$moment", "$not", "$now",
                    //     "$number", "$pad", "$parseInteger", "$power", "$random", "$reduce", "$replace",
                    //     "$reverse", "$round", "$shuffle", "$sift", "$single", "$sort", "$split",
                    //     "$spread", "$sqrt", "$string", "$substring", "$substringAfter", "$substringBefore",
                    //     "$sum", "$toMillis", "$trim", "$type", "$uppercase", "$zip"
                    // ],

                    operatorsKeywords: [ 'and', 'or', 'in' ],

                    operators: [
                        '<=', '>=', '!=', '==', '!=', '=>', '+', '-', '*', '/', '%',
                        ':=', '~>', '?', ':', '..', '@', '#', '|', '^', '*', '**',
                    ],

                    // we include these common regular expressions
                    symbols: /[=><!~?:&|+\-*\/\^%@#]+/,
                    escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
                    digits: /\d+(_+\d+)*/,
                    octaldigits: /[0-7]+(_+[0-7]+)*/,
                    binarydigits: /[0-1]+(_+[0-1]+)*/,
                    hexdigits: /[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,

                    regexpctl: /[(){}\[\]\$\^|\-*+?\.]/,
                    regexpesc: /\\(?:[bBdDfnrstvwWn0\\\/]|@regexpctl|c[A-Z]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4})/,

                    // The main tokenizer
                    tokenizer: {
                        root: [
                            [/[{}]/, 'delimiter.bracket'],
                            { include: 'common' }
                        ],

                        common: [
                            // identifiers and keywords
                            [/([a-zA-Z][\w$]*)|([$][\w$]*)/, {
                                cases: {
                                    '@keywords': 'keyword',
                                    '@operatorsKeywords': 'keyword',
                                    '$2': 'variable', //when its not a key word but starts with $, use "tag" for colourisation
                                    //'$2': 'tag', //when its not a key word but starts with $, use "tag" for colourisation
                                    //'$2': 'constant', //alt colourisation
                                    //'$2': 'attribute', //alt colourisation
                                    //'$2': 'identifier.variable', //alt custom colourisation
                                    '@default': 'identifier'
                                }
                            }],
                            [/[$][\w\$]*/, 'variable'],

                            // whitespace
                            { include: '@whitespace' },

                            // regular expression: ensure it is terminated before beginning (otherwise it is an opeator)
                            [/\/(?=([^\\\/]|\\.)+\/([gimsuy]*)(\s*)(\.|;|\/|,|\)|\]|\}|$))/, { token: 'regexp', bracket: '@open', next: '@regexp' }],

                            // delimiters and operators
                            [/[()\[\]]/, '@brackets'],
                            [/[<>](?!@symbols)/, '@brackets'],
                            [/(@symbols)|(\.\.)/, {
                                cases: {
                                    '@operators': 'operator',
                                    '@default': ''
                                }
                            }],

                            // numbers
                            [/(@digits)[eE]([\-+]?(@digits))?/, 'number.float'],
                            [/(@digits)\.(@digits)([eE][\-+]?(@digits))?/, 'number.float'],
                            [/0[xX](@hexdigits)/, 'number.hex'],
                            [/0[oO]?(@octaldigits)/, 'number.octal'],
                            [/0[bB](@binarydigits)/, 'number.binary'],
                            [/(@digits)/, 'number'],

                            // delimiter: after number because of .\d floats
                            [/[?:;,.]/, 'delimiter'],

                            // strings
                            [/"([^"\\]|\\.)*$/, 'string.invalid'],  // non-teminated string
                            [/'([^'\\]|\\.)*$/, 'string.invalid'],  // non-teminated string
                            [/"/, 'string', '@string_double'],
                            [/'/, 'string', '@string_single'],
                            [/`/, 'string', '@string_backtick'],
                        ],

                        whitespace: [
                            [/[ \t\r\n]+/, ''],
                            [/\/\*\*(?!\/)/, 'comment.doc', '@jsdoc'],
                            [/\/\*/, 'comment', '@comment'],
                            [/\/\/.*$/, 'comment'],
                        ],

                        comment: [
                            [/[^\/*]+/, 'comment'],
                            [/\*\//, 'comment', '@pop'],
                            [/[\/*]/, 'comment']
                        ],

                        jsdoc: [
                            [/[^\/*]+/, 'comment.doc'],
                            [/\*\//, 'comment.doc', '@pop'],
                            [/[\/*]/, 'comment.doc']
                        ],

                        // We match regular expression quite precisely
                        regexp: [
                            [/(\{)(\d+(?:,\d*)?)(\})/, ['regexp.escape.control', 'regexp.escape.control', 'regexp.escape.control']],
                            [/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/, ['regexp.escape.control', { token: 'regexp.escape.control', next: '@regexrange' }]],
                            [/(\()(\?:|\?=|\?!)/, ['regexp.escape.control', 'regexp.escape.control']],
                            [/[()]/, 'regexp.escape.control'],
                            [/@regexpctl/, 'regexp.escape.control'],
                            [/[^\\\/]/, 'regexp'],
                            [/@regexpesc/, 'regexp.escape'],
                            [/\\\./, 'regexp.invalid'],
                            [/(\/)([gimsuy]*)/, [{ token: 'regexp', bracket: '@close', next: '@pop' }, 'keyword.other']],
                        ],

                        regexrange: [
                            [/-/, 'regexp.escape.control'],
                            [/\^/, 'regexp.invalid'],
                            [/@regexpesc/, 'regexp.escape'],
                            [/[^\]]/, 'regexp'],
                            [/\]/, { token: 'regexp.escape.control', next: '@pop', bracket: '@close' }],
                        ],

                        string_double: [
                            [/[^\\"]+/, 'string'],
                            [/@escapes/, 'string.escape'],
                            [/\\./, 'string.escape.invalid'],
                            [/"/, 'string', '@pop']
                        ],

                        string_single: [
                            [/[^\\']+/, 'string'],
                            [/@escapes/, 'string.escape'],
                            [/\\./, 'string.escape.invalid'],
                            [/'/, 'string', '@pop']
                        ],

                        string_backtick: [
                            [/\$\{/, { token: 'delimiter.bracket', next: '@bracketCounting' }],
                            [/[^\\`$]+/, 'string'],
                            [/@escapes/, 'string.escape'],
                            [/\\./, 'string.escape.invalid'],
                            [/`/, 'string', '@pop']
                        ],

                        bracketCounting: [
                            [/\{/, 'delimiter.bracket', '@bracketCounting'],
                            [/\}/, 'delimiter.bracket', '@pop'],
                            { include: 'common' }
                        ],
                    },
                }

            );

           // Setup JSONata language config
            _monaco.languages.setLanguageConfiguration('jsonata', {
                comments: {
                    lineComment: '//',
                    blockComment: ['/*', '*/']
                },
                brackets: [
                    ['{', '}'],
                    ['[', ']'],
                    ['(', ')']
                ],
                autoClosingPairs: [
                    { open: '{', close: '}' },
                    { open: '[', close: ']' },
                    { open: '(', close: ')' },
                    { open: "'", close: "'", notIn: ['string', 'comment'] },
                    { open: '"', close: '"', notIn: ['string'] }
                ],
                surroundingPairs: [
                    { open: '{', close: '}' },
                    { open: '[', close: ']' },
                    { open: '(', close: ')' },
                    { open: '"', close: '"' },
                    { open: "'", close: "'" },
                    { open: '<', close: '>' }
                ],
                folding: {
                    markers: {
                        start: new RegExp('^\\s*//\\s*(?:(?:#?region\\b)|(?:<editor-fold\\b))'),
                        end: new RegExp('^\\s*//\\s*(?:(?:#?endregion\\b)|(?:</editor-fold>))')
                    }
                }
            });

            // Register a completion item provider for JSONata snippets
            _monaco.languages.registerCompletionItemProvider('jsonata', {
                provideCompletionItems: function (model, position) {
                    var _word = model.getWordUntilPosition(position);
                    if (!_word) { return; }
                    var startColumn = _word.startColumn;
                    var word = _word.word;
                    if (word[0] !== "$" && position.column > 1) { startColumn--; }
                    var range = {
                        startLineNumber: position.lineNumber,
                        endLineNumber: position.lineNumber,
                        startColumn: startColumn,
                        endColumn: _word.endColumn
                    };
                    var jsonataFunctions = Object.keys(jsonata.functions);
                    var jsonataSuggestions = jsonataFunctions.map(function (f) {
                        var args = RED._('jsonata:' + f + '.args', { defaultValue: '' });
                        var title = f + '(' + args + ')';
                        var body = RED._('jsonata:' + f + '.desc', { defaultValue: '' });
                        var insertText = (jsonata.getFunctionSnippet(f) + '').trim();
                        var documentation = { value: '`' + title + '`\n\n' + body };
                        return createMonacoCompletionItem(f, insertText, documentation, range, monaco.languages.CompletionItemKind.Function);
                    });
                    // sort in length order (long->short) otherwise substringAfter gets matched as substring etc.
                    jsonataFunctions.sort(function (A, B) {
                        return B.length - A.length;
                    });
                    // add snippets to suggestions
                    jsonataSuggestions.unshift(
                        createMonacoCompletionItem("randominteger", '(\n\t\\$minimum := ${1:1};\n\t\\$maximum := ${2:10};\n\t\\$round((\\$random() * (\\$maximum-\\$minimum)) + \\$minimum, 0)\n)', 'Random integer between 2 numbers', range)
                    );//TODO: add more JSONata snippets
                    return { suggestions: jsonataSuggestions };
                }
            });

            // Register a hover provider for JSONata functions
            _monaco.languages.registerHoverProvider('jsonata', {
                provideHover: function (model, position) {
                    var w = model.getWordAtPosition(position);
                    var f = w && w.word;
                    if (!f) {return;}
                    if (f[0] !== "$" && position.column > 1) {
                        f = "$" + f;
                    } else {
                        return;
                    }
                    var args = RED._('jsonata:' + f + ".args", { defaultValue: '' });
                    if (!args) {return;}
                    var title = f + "(" + args + ")";
                    var body = RED._('jsonata:' + f + '.desc', { defaultValue: '' });

                    /** @type {monaco.Range} */
                    var r = new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column + w.word.length);
                    return {
                        range: r,
                        contents: [
                            { value: '**`' + title + '`**' },
                            // { value: '```html\n' + body + '\n```' },
                            { value: body },
                        ]
                    }
                }
            });
        }

        function setupJSON(_monaco) {
            //Setup JSON options
            try {
                var diagnosticOptionsDefault = {validate: true};
                var diagnosticOptions = RED.settings.get('codeEditor.monaco.languages.json.jsonDefaults.diagnosticOptions');
                var modeConfiguration = RED.settings.get('codeEditor.monaco.languages.json.jsonDefaults.modeConfiguration');
                diagnosticOptions = Object.assign({}, diagnosticOptionsDefault, (diagnosticOptions || {}));
                _monaco.languages.json.jsonDefaults.setDiagnosticsOptions(diagnosticOptions);
                if(modeConfiguration) { _monaco.languages.json.jsonDefaults.setModeConfiguration(modeConfiguration); }
            } catch (error) {
                console.warn("monaco - Error setting up json options", error)
            }
        }

        function setupHTML(_monaco) {
            //Setup HTML / Handlebars options
            try {
                var htmlDefaults = RED.settings.get('codeEditor.monaco.languages.html.htmlDefaults.options');
                var handlebarDefaults = RED.settings.get('codeEditor.monaco.languages.html.handlebarDefaults.options');
                if(htmlDefaults) { _monaco.languages.html.htmlDefaults.setOptions(htmlDefaults); }
                if(handlebarDefaults) { _monaco.languages.html.handlebarDefaults.setOptions(handlebarDefaults); }
            } catch (error) {
                console.warn("monaco - Error setting up html options", error)
            }
        }

        function setupCSS(_monaco) {
            //Setup CSS/SCSS/LESS options
            try {
                var cssDefaults_diagnosticsOption = RED.settings.get('codeEditor.monaco.languages.css.cssDefaults.diagnosticsOptions');
                var lessDefaults_diagnosticsOption = RED.settings.get('codeEditor.monaco.languages.css.lessDefaults.diagnosticsOption');
                var scssDefaults_diagnosticsOption = RED.settings.get('codeEditor.monaco.languages.css.scssDefaults.diagnosticsOption');
                var cssDefaults_modeConfiguration = RED.settings.get('codeEditor.monaco.languages.css.cssDefaults.modeConfiguration');
                var lessDefaults_modeConfiguration = RED.settings.get('codeEditor.monaco.languages.css.lessDefaults.modeConfiguration');
                var scssDefaults_modeConfiguration = RED.settings.get('codeEditor.monaco.languages.css.scssDefaults.modeConfiguration');
                if(cssDefaults_diagnosticsOption) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(cssDefaults_diagnosticsOption); }
                if(lessDefaults_diagnosticsOption) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(lessDefaults_diagnosticsOption); }
                if(scssDefaults_diagnosticsOption) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(scssDefaults_diagnosticsOption); }
                if(cssDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(cssDefaults_modeConfiguration); }
                if(lessDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(lessDefaults_modeConfiguration); }
                if(scssDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(scssDefaults_modeConfiguration); }
            } catch (error) {
                console.warn("monaco - Error setting up CSS/SCSS/LESS options", error)
            }
        }

        function setupJS(_monaco) {
            var createJSSnippets = function(range) {
                return [
                    //TODO: i18n for snippet descriptions?
                    createMonacoCompletionItem("dowhile", 'do {\n\t${2}\n} while (${1:condition});','Do-While Statement (JavaScript Language Basics)',range),
                    createMonacoCompletionItem("while", 'while (${1:condition}) {\n\t${2}\n}','While Statement (JavaScript Language Basics)',range),
                    createMonacoCompletionItem("switch", 'switch (${1:msg.topic}) {\n\tcase ${2:"value"}:\n\t\t${3}\n\t\tbreak;\n\tdefault:\n\t\t\n}','Switch Statement (JavaScript Language Basics)',range),
                    createMonacoCompletionItem("trycatch", 'try {\n\t${2}\n} catch (${1:error}) {\n\t\n};','Try-Catch Statement (JavaScript Language Basics)',range),
                    createMonacoCompletionItem("for (for loop)", 'for (let ${1:index} = 0; ${1:index} < ${2:array}.length; ${1:index}++) {\n\tconst element = ${2:array}[${1:index}];\n\t${3}\n}','for loop',range),
                    createMonacoCompletionItem("foreach", '${1:array}.forEach(function(${2:element}) {\n\t${3}\n});','forEach(callbackfn: (value: T, index: number, array: readonly T[]) => void, thisArg?: any): void\n\nA function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.',range),
                    createMonacoCompletionItem("forin", 'for (${1:prop} in ${2:obj}) {\n\tif (${2:obj}.hasOwnProperty(${1:prop})) {\n\t\t${3}\n\t}\n}','for in',range),
                    createMonacoCompletionItem("forof", 'for (const ${1:iterator} of ${2:object}) {\n\t${3}\n}','for of',range),
                    createMonacoCompletionItem("function", 'function ${1:methodName}(${2:arguments}) {\n\t${3}\n}','Function Declaration',range),
                    createMonacoCompletionItem("func (anonymous function)", 'var ${1:fn} = function(${2:arguments}) {\n\t${3}\n}','Function Expression',range),
                    createMonacoCompletionItem("pt (prototype)", '${1:ClassName}.prototype.${2:methodName} = function(${3:arguments}) {\n\t${4}\n}','prototype',range),
                    createMonacoCompletionItem("iife", '(function(${1:arg}) {\n\t${1}\n})(${1:arg});','immediately-invoked function expression',range),
                    createMonacoCompletionItem("call (function call)", '${1:methodName}.call(${2:context}, ${3:arguments})','function call',range),
                    createMonacoCompletionItem("apply (function apply)", '${1:methodName}.apply(${2:context}, [${3:arguments}])','function apply',range),
                    createMonacoCompletionItem("jsonparse", 'JSON.parse(${1:json});','JSON.parse',range),
                    createMonacoCompletionItem("jsonstringify", 'JSON.stringify(${1:obj});','JSON.stringify',range),
                    createMonacoCompletionItem("setinterval", 'setInterval(function() {\n\t${2}\n}, ${1:delay});','setInterval',range),
                    createMonacoCompletionItem("settimeout", 'setTimeout(function() {\n\t${2}\n}, ${1:delay});','setTimeout',range),
                    createMonacoCompletionItem("node.log", 'node.log(${1:"info"});','Write an info message to the console (not sent to sidebar)',range),
                    createMonacoCompletionItem("node.warn", 'node.warn(${1:"my warning"});','Write a warning to the console and debug sidebar',range),
                    createMonacoCompletionItem("node.error", 'node.error(${1:"my error message"}, ${2:msg});','Send an error to the console and debug sidebar. To trigger a Catch node on the same tab, the function should call `node.error` with the original message as a second argument',range),
                    createMonacoCompletionItem("node.send", 'node.send(${1:msg});','async send a msg to the next node',range),
                    createMonacoCompletionItem("node.send (multiple)", 'var ${1:msg1} = {payload:${2:1}};\nvar ${3:msg2} = {payload:${4:2}};\nnode.send([[${1:msg1}, ${3:msg2}]]);','send 1 or more messages out of 1 output',range),
                    createMonacoCompletionItem("node.send (multiple outputs)", 'var ${1:msg1} = {payload:${2:1}};\nvar ${3:msg2} = {payload:${4:2}};\nnode.send([${1:msg1}, ${3:msg2}]);','send more than 1 message out of multiple outputs',range),
                    createMonacoCompletionItem("node.status", 'node.status({fill:"${1|red,green,yellow,blue,grey|}",shape:"${2|ring,dot|}",text:"${3:message}"});','Set the status icon and text underneath the function node',range),
                    createMonacoCompletionItem("get (node context)", 'context.get("${1:name}");','Get a value from node context',range),
                    createMonacoCompletionItem("set (node context)", 'context.set("${1:name}", ${1:value});','Set a value in node context',range),
                    createMonacoCompletionItem("get (flow context)", 'flow.get("${1:name}");','Get a value from flow context',range),
                    createMonacoCompletionItem("set (flow context)", 'flow.set("${1:name}", ${1:value});','Set a value in flow context',range),
                    createMonacoCompletionItem("get (global context)", 'global.get("${1:name}");','Get a value from global context',range),
                    createMonacoCompletionItem("set (global context)", 'global.set("${1:name}", ${1:value});','Set a value in global context',range),
                    createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME,NR_SUBFLOW_NAME,NR_SUBFLOW_ID,NR_SUBFLOW_PATH|}");','Get env variable value',range),
                    createMonacoCompletionItem("cloneMessage (RED.util)", 'RED.util.cloneMessage(${1:msg});',
                        ["```typescript",
                        "RED.util.cloneMessage<T extends registry.NodeMessage>(msg: T): T",
                        "```",
                        "Safely clones a message object. This handles msg.req/msg.res objects that must not be cloned\n",
                        "*@param* `msg` — the msg object\n"],
                        range),
                    createMonacoCompletionItem("getObjectProperty (RED.util)", 'RED.util.getObjectProperty(${1:msg},${2:prop});',
                        ["```typescript",
                        "RED.util.getObjectProperty(msg: object, expr: string): any;",
                        "```",
                        "Gets a property of an object\n",
                        "*@param* `msg` — the msg object\n",
                        "*@param* `prop` — the msg object"],
                        range),
                    createMonacoCompletionItem("setObjectProperty (RED.util)", 'RED.util.setObjectProperty(${1:msg},${2:prop},${3:value},${4:createMissing});',
                        ["```typescript",
                        "RED.util.setObjectProperty(msg: object, prop: string, value: any, createMissing?: boolean): boolean",
                        "```",
                        "Sets a property of an object\n",
                        "`msg` — the object\n",
                        "`prop` — the property expression\n",
                        "`value` — the value to set\n",
                        "`createMissing` — whether to create missing parent properties"],
                        range),
                    createMonacoCompletionItem("getMessageProperty (RED.util)", 'RED.util.getMessageProperty(${1:msg},${2:prop});',
                        ["```typescript",
                        "RED.util.getMessageProperty(msg: object, expr: string): any;",
                        "```",
                        "Gets a property of an object\n",
                        "*@param* `msg` — the msg object\n",
                        "*@param* `prop` — the msg object"],
                        range),
                    createMonacoCompletionItem("setMessageProperty (RED.util)", 'RED.util.setMessageProperty(${1:msg},${2:prop},${3:value},${4:createMissing});',
                        ["```typescript",
                        "RED.util.setMessageProperty(msg: object, prop: string, value: any, createMissing?: boolean): boolean",
                        "```",
                        "Sets a property of an object\n",
                        "`msg` — the object\n",
                        "`prop` — the property expression\n",
                        "`value` — the value to set\n",
                        "`createMissing` — whether to create missing parent properties"],
                        range),
                ];
            }

            //register snippets
            _monaco.languages.registerCompletionItemProvider('javascript', {
                provideCompletionItems: function(model, position) {
                    var word = model.getWordUntilPosition(position);
                    var range = {
                        startLineNumber: position.lineNumber,
                        endLineNumber: position.lineNumber,
                        startColumn: word.startColumn,
                        endColumn: word.endColumn
                    };
                    return {
                        suggestions: createJSSnippets(range)
                    };
                }
            });

            //setup JS/TS compiler & diagnostic options (defaults)
            try {
                //prepare compiler options
                var compilerOptions = {
                    allowJs: true,
                    checkJs: true,
                    allowNonTsExtensions: true,
                    target: monaco.languages.typescript.ScriptTarget.ESNext,
                    strictNullChecks: false,
                    strictPropertyInitialization: true,
                    strictFunctionTypes: true,
                    strictBindCallApply: true,
                    useDefineForClassFields: true,//permit class static fields with private name to have initializer
                    moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
                    module: monaco.languages.typescript.ModuleKind.CommonJS,
                    typeRoots: ["types"],
                    lib: ["esnext"] //dont load DOM by default,
                }
                //apply overrides from codeEditor.monaco.languages.typescript.javascriptDefaults.compilerOptions in settings.js
                var settingsComilerOptions = RED.settings.get('codeEditor.monaco.languages.typescript.javascriptDefaults.compilerOptions') || {};
                compilerOptions = Object.assign({}, compilerOptions, settingsComilerOptions);
                /** @see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.typescript.languageservicedefaults.html#setcompileroptions */
                _monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions);

                //prepare diagnostic options (defaults)
                var diagnosticOptions = {
                    noSemanticValidation: false,
                    noSyntaxValidation: false,
                    diagnosticCodesToIgnore:  [
                        1108,  //return not inside function
                        1375,  //'await' expressions are only allowed at the top level of a file when that file is a module
                        1378,  //Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher
                        //2304,  //Cannot find name - this one is heavy handed and prevents user seeing stupid errors. Would provide better ACE feature parity (i.e. no need for declaration of vars) but misses lots of errors. Lets be bold & leave it out!
                        2307,  //Cannot find module 'xxx' or its corresponding type declarations
                        2322,  //Type 'unknown' is not assignable to type 'string'
                        2339,  //property does not exist on
                        2345,  //Argument of type xxx is not assignable to parameter of type 'DateTimeFormatOptions'
                        2538,  //Ignore symbols as index property error. 
                        7043,  //i forget what this one is,
                        80001, //Convert to ES6 module
                        80004, //JSDoc types may be moved to TypeScript types.
                    ]
                };
                //apply overrides from codeEditor.monaco.languages.typescript.javascriptDefaults.diagnosticsOptions settings.js
                var settingsDiagnosticsOptions = RED.settings.get('codeEditor.monaco.languages.typescript.javascriptDefaults.diagnosticsOptions') || {};
                diagnosticOptions = Object.assign({}, diagnosticOptions, settingsDiagnosticsOptions);
                /** @see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.typescript.languageservicedefaults.html#setdiagnosticsoptions */
                _monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(diagnosticOptions);
            } catch (error) {
                console.warn("monaco - Error setting javascriptDefaults", error)
            }
        }

        setupJS(monaco);
        setupJSONata(monaco);
        setupJSON(monaco);
        setupCSS(monaco);
        setupHTML(monaco);
        defaultServerSideTypes.forEach(function(m) {
            _loadModuleDTS(m, true)//pre-load common libs
        })

        initialised = true;
        return initialised;
    }

    function create(options) {

        var editorSettings = RED.editor.codeEditor.settings || {};
        var loadedLibs = {JS:{}, TS:{}};//for tracking and later disposing of loaded type libs
        var watchTimer;
        var createThemeMenuOption = function (theme, keybinding) {
            return {
                // An unique identifier of the contributed action.
                id: 'set-theme-' + theme,
                // A label of the action that will be presented to the user.
                label: RED._('monaco.setTheme') + ' ' + theme,
                precondition: null,// A precondition for this action.
                keybindingContext: keybinding || null,// A rule to evaluate on top of the precondition in order to dispatch the keybindings.

                // Method that will be executed when the action is triggered.
                // @param editor The editor instance is passed in as a convinience
                run: function (ed) {
                    //monaco.editor.setTheme(theme)
                    ed.setTheme(theme)
                    return null;
                }
            }
        }

        var convertAceModeToMonacoLang = function (mode) {
            if (typeof mode == "object" && mode.path) {
                mode = mode.path;
            }
            if (mode) {
                mode = mode.replace("ace/mode/", "");
            } else {
                mode = "text";
            }
            switch (mode) {
                case "nrjavascript":
                case "mjs":
                    mode = "javascript";
                    break;
                case "vue":
                    mode = "html";
                    break;
                case "appcache":
                case "sh":
                case "bash":
                    mode = "shell";
                    break;
                case "batchfile":
                    mode = "bat";
                    break;
                case "protobuf":
                    mode = "proto";
                    break;
                //TODO: add other compatability types.
            }
            return mode;
        }

        
        if(!options.stateId && options.stateId !== false) {
            options.stateId = RED.editor.generateViewStateId("monaco", options, (options.mode || options.title || "").split("/").pop());
        }
        var el = options.element || $("#"+options.id)[0];
        var toolbarRow = $("<div>").appendTo(el);
        el = $("<div>").appendTo(el).addClass("red-ui-editor-text-container")[0];

        var editorOptions = $.extend({}, editorSettings.options, options);
        editorOptions.language = convertAceModeToMonacoLang(options.mode);

        if(userSelectedTheme) {
            editorOptions.theme = userSelectedTheme;//use user selected theme for this session
        }

        //by default, set javascript editors to text mode.
        //when element becomes visible, it will be (re) set to javascript mode
        //this is to ensure multiple editors sharing the model dont present its
        //consts & lets to each other
        if(editorOptions.language == "javascript") {
            editorOptions._language = editorOptions.language;
            editorOptions.language = "text"
        }

        //apply defaults
        if (!editorOptions.minimap) {
            editorOptions.minimap = {
                enabled: true,
                maxColumn: 50,
                scale: 1,
                showSlider: "mouseover",
                renderCharacters: true
            }
        }

        //common ACE flags - set equivelants in monaco
        if(options.enableBasicAutocompletion === false) {
            editorOptions.showSnippets = false;
            editorOptions.quickSuggestions = false;
            editorOptions.parameterHints = { enabled: false };
            editorOptions.suggestOnTriggerCharacters = false;
            editorOptions.acceptSuggestionOnEnter = "off";
            editorOptions.tabCompletion = "off";
            editorOptions.wordBasedSuggestions = false;
        }
        if (options.enableSnippets === false) { editorOptions.showSnippets = false; }
        if (editorOptions.mouseWheelZoom == null) { editorOptions.mouseWheelZoom = true; }
        if (editorOptions.suggestFontSize == null) { editorOptions.suggestFontSize = 12; }
        if (editorOptions.formatOnPaste == null) { editorOptions.formatOnPaste = true; }
        if (editorOptions.foldingHighlight == null) { editorOptions.foldingHighlight = true; }
        if (editorOptions.foldStyle == null) { editorOptions.foldStyle = true; } //https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html#folding
        if (editorOptions.readOnly != null) { editorOptions.readOnly = editorOptions.readOnly; }
        if (editorOptions.lineNumbers === false) { editorOptions.lineNumbers = false; }
        if (editorOptions.theme == null) { editorOptions.theme = monacoThemes[0]; }
        if (editorOptions.mode == null) { editorOptions.mode = convertAceModeToMonacoLang(options.mode); }
        if (editorOptions.automaticLayout == null) { editorOptions.automaticLayout = true; }

        if (options.foldStyle) {
            switch (options.foldStyle) {
                case "none":
                    editorOptions.foldStyle = false;
                    editorOptions.foldingHighlight = false
                    break;
                default:
                    editorOptions.foldStyle = true;
                    editorOptions.foldingHighlight = true;
                    break;
            }
        } else {
            editorOptions.foldStyle = true;
            editorOptions.foldingHighlight = true;
        }

        //others
        editorOptions.roundedSelection = editorOptions.roundedSelection === false ? false : true; //default to true
        editorOptions.contextmenu = editorOptions.contextmenu === false ? false : true; //(context menu enable) default to true
        editorOptions.snippetSuggestions = editorOptions.enableSnippets === false ? false : true; //default to true //https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html#snippetsuggestions

        editorOptions.value = options.value || "";

        //if wordSeparators are not supplied, override default ones for the "j" langs
        if(!editorOptions.wordSeparators) {
            if (editorOptions.language == "jsonata" || editorOptions.language == "json" || editorOptions.language == "javascript") {
                editorOptions.wordSeparators = "`~!@#%^&*()-=+[{]}\|;:'\",.<>/?"; //dont use $ as separator
            }
        }

        //fixedOverflowWidgets should probably never be set to false
        //fixedOverflowWidgets allows hover tips to be above other parts of UI
        editorOptions.fixedOverflowWidgets = editorOptions.fixedOverflowWidgets === false ? false : true;

        //#region Detect mobile / tablet and reduce functionality for small screen and lower power
        var browser = RED.utils.getBrowserInfo();
        if (browser.mobile || browser.tablet) {
            editorOptions.minimap = { enabled: false };
            editorOptions.formatOnType = false; //try to prevent cursor issues
            editorOptions.formatOnPaste = false; //try to prevent cursor issues
            editorOptions.disableMonospaceOptimizations = true; //speed up
            editorOptions.columnSelection = false; //try to prevent cursor issues
            editorOptions.matchBrackets = "never"; //speed up
            editorOptions.maxTokenizationLineLength = 10000; //speed up //internal default is 20000
            editorOptions.stopRenderingLineAfter = 2000; //speed up //internal default is 10000
            editorOptions.roundedSelection = false; //speed up rendering
            editorOptions.trimAutoWhitespace = false; //try to prevent cursor issues
            editorOptions.parameterHints = { enabled: false };
            editorOptions.suggestOnTriggerCharacters = false;//limit suggestions, user can still use ctrl+space
            editorOptions.wordBasedSuggestions = false;
            editorOptions.suggest = { maxVisibleSuggestions: 6 };
            // editorOptions.codeLens = false;//If Necessary, disable this useful feature?
            // editorOptions.quickSuggestions = false;//If Necessary, disable this useful feature?
            // editorOptions.showSnippets = false; //If Necessary, disable this useful feature?
            // editorOptions.acceptSuggestionOnEnter = "off"; //If Necessary, disable this useful feature?
            // editorOptions.tabCompletion = "off"; //If Necessary, disable this useful feature?
            if (!editorOptions.accessibilitySupport && browser.android) {
                editorOptions.accessibilitySupport = "off"; //ref https://github.com/microsoft/pxt/pull/7099/commits/35fd3e969b0d5b68ca1e35809f96cea81ef243bc
            }
        }
        //#endregion

        //#region Load types for intellisense

        //Determine if this instance is for client side or server side...
        //if clientSideSuggestions option is not specified, check see if
        //requested mode is "nrjavascript" or if options.globals are specifying RED or Buffer
        var serverSideSuggestions = false;
        if (options.clientSideSuggestions == null) {
            if ( ((options.mode + "").indexOf("nrjavascript") >= 0) || (options.globals && (options.globals.RED || options.globals.Buffer )) ) {
                serverSideSuggestions = true;
            }
        }

        // compiler options - enable / disable server-side/client-side suggestions
        var compilerOptions = monaco.languages.typescript.javascriptDefaults.getCompilerOptions();
        if (serverSideSuggestions) {
            compilerOptions.lib = ["esnext"]; //dont include DOM
            defaultServerSideTypes.forEach(function(m) {
                _loadModuleDTS(m, false, loadedLibs); //load common node+node-red types
            })
        } else {
            compilerOptions.lib = ["esnext", "dom"];
        }

        monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions);

        //check if extraLibs are to be loaded (e.g. fs or os)
        refreshModuleLibs(editorOptions.extraLibs)

        function refreshModuleLibs(extraModuleLibs) {
            var defs = [];
            var imports = [];
            const id = "extraModuleLibs/index.d.ts";
            const file = 'file://types/extraModuleLibs/index.d.ts';
            if(!extraModuleLibs || extraModuleLibs.length == 0) {
                loadedLibs.JS[id] = monaco.languages.typescript.javascriptDefaults.addExtraLib(" ", file);
            } else {
                var loadList = [];
                Array.prototype.push.apply(loadList, extraModuleLibs);//Use this instead of spread operator to prevent IE syntax error
                var loadExtraModules = {};
                for (let index = 0; index < extraModuleLibs.length; index++) {
                    const lib = extraModuleLibs[index];
                    const varName = lib.var;
                    const moduleName = lib.module;
                    if(varName && moduleName) {
                        imports.push("import " + varName + "_import = require('" + moduleName + "');\n");
                        defs.push("var " + varName + ": typeof " + varName + "_import;\n");
                    }
                    var km = knownModules[moduleName];
                    if(km) {
                        loadExtraModules[moduleName] = km;
                    } else {
                        loadExtraModules[moduleName] = {package: "other", module: moduleName, path: "other/" +  moduleName + ".d.ts" };
                    }
                }
                Object.values(loadExtraModules).forEach(function(m) {
                    _loadModuleDTS(m, false, loadedLibs, function(err, extraLib) {
                        loadList = loadList.filter(function(e) {return e.module != extraLib.module} );
                        if(loadList.length == 0) {
                            var _imports = imports.join("");
                            var _defs  = "\ndeclare global {\n" + defs.join("") + "\n}";
                            var libSource = _imports + _defs;
                            setTimeout(function() {
                                loadedLibs.JS[id] = monaco.languages.typescript.javascriptDefaults.addExtraLib(libSource, file);
                            }, 500);
                        }
                    });
                });
            }
        }

        //#endregion

        /*********** Create the monaco editor ***************/
        var ed = monaco.editor.create(el, editorOptions);

        //Unbind ctrl-Enter (default action is to insert a newline in editor) This permits the shortcut to close the tray.
        try {
            monaco.editor.addKeybindingRule({keybinding: 0, command: "-editor.action.insertLineAfter"});
        } catch (error) {
            console.warn(error)
        }

        ed.nodered = {
            refreshModuleLibs: refreshModuleLibs //expose this for function node externalModules refresh
        }

        //add f1 menu items for changing theme
        for (var themeIdx = 0; themeIdx < monacoThemes.length; themeIdx++) {
            var themeName = monacoThemes[themeIdx];
            ed.addAction(createThemeMenuOption(themeName));
        }

        //#region "ACE compatability"

        ed.selection = {};
        ed.session = ed;
        ed.renderer = {};

        ed.setMode = function(mode, cb, resize) {
            if(resize==null) { resize = true; }
            mode = convertAceModeToMonacoLang(mode);
            var oldModel = ed.getModel();
            var oldValue = ed.getValue();
            var newModel

            if (oldModel) {
                var oldScrollTop = ed.getScrollTop();
                var oldScrollLeft = ed.getScrollLeft();
                var oldSelections = ed.getSelections();
                var oldPosition = ed.getPosition();
                oldValue = oldModel.getValue() || "";
                try {
                    if(!oldModel.isDisposed()) { oldModel.dispose(); }
                } catch (error) { }
                ed.setModel(null);
                newModel = monaco.editor.createModel((oldValue || ""), mode);
                ed.setModel(newModel);
                ed.setScrollTop(oldScrollTop, 1/* immediate */);
                ed.setScrollLeft(oldScrollLeft, 1/* immediate */);
                ed.setPosition(oldPosition);
                ed.setSelections(oldSelections);
            } else {
                newModel = monaco.editor.createModel((oldValue || ""), mode);
                ed.setModel(newModel);
            }
            if (cb && typeof cb == "function") {
                cb();
            }
            if(resize) {
                this.resize(); //cause a call to layout()
            }
        }

        ed.getRange = function getRange(){
            var r =  ed.getSelection();
            r.start = {
                row: r.selectionStartLineNumber-1,
                column: r.selectionStartColumn-1
            }
            r.end = {
                row: r.endLineNumber-1,
                column: r.endColumn-1
            }
            return r;
        }

        ed.selection.getRange = ed.getRange;

        ed.session.insert = function insert(position, text) {
            //ACE position is zero based, monaco range is 1 based
            var range = new monaco.Range(position.row+1, position.column+1, position.row+1, position.column+1);
            var op = { range: range, text: text, forceMoveMarkers: true };
            _executeEdits(this,[op]);
        }

        ed.setReadOnly = function setReadOnly(readOnly) {
            ed.updateOptions({ readOnly: readOnly })
        }

        ed.session.replace = function replace(range, text) {
            var op = { range: range, text: text, forceMoveMarkers: true };
            _executeEdits(this,[op]);
        }

        function _executeEdits(editor, edits, endCursorState) {
            var isReadOnly = editor.getOption(monaco.editor.EditorOptions.readOnly.id)
            if (isReadOnly) {
                editor.getModel().pushEditOperations(editor.getSelections(), edits, function() {
                    return endCursorState ? endCursorState : null;
                });
            } else {
                editor.executeEdits("editor", edits);
            }
        }

        ed.selectAll =  function selectAll() {
            const range = ed.getModel().getFullModelRange();
            ed.setSelection(range);
        }

        ed.clearSelection = function clearSelection() {
            ed.setPosition({column:1,lineNumber:1});
        }

        ed.getSelectedText = function getSelectedText() {
            return ed.getModel().getValueInRange(ed.getSelection());
        }

        ed.insertSnippet = function insertSnippet(s) {
            //https://github.com/microsoft/monaco-editor/issues/1112#issuecomment-429580604
            //no great way of triggering snippets!
            let contribution = ed.getContribution("snippetController2");
            contribution.insert(s);
        }

        ed.destroy = function destroy() {
            if(watchTimer) { clearInterval(watchTimer); }
            try {
                //dispose serverside addExtraLib disposible we added - this is the only way (https://github.com/microsoft/monaco-editor/issues/1002#issuecomment-564123586)
                if(Object.keys(loadedLibs.JS).length) {
                    let entries = Object.entries(loadedLibs.JS);
                    for (let i = 0; i < entries.length; i++) {
                        try {
                            const entry = entries[i];
                            let name = entry[0];
                            loadedLibs.JS[name].dispose();
                            loadedLibs.JS[name] = null;
                            delete loadedLibs.JS[name];
                        } catch (error) { }
                    }
                }
                if(Object.keys(loadedLibs.TS).length) {
                    let entries = Object.entries(loadedLibs.TS);
                    for (let i = 0; i < entries.length; i++) {
                        try {
                            const entry = entries[i];
                            let name = entry[0];
                            loadedLibs.TS[name].dispose();
                            loadedLibs.TS[name] = null;
                            delete loadedLibs.TS[name];
                        } catch (error) { }
                    }
                }
            } catch (error) { }
            try {
                var m = this.getModel();
                if(m && !m.isDisposed()) {
                    ed._initState = null;
                    m.dispose();
                }
                this.setModel(null);
            } catch (e) { }

            $(el).remove();
            $(toolbarRow).remove();
            ed.dispose();
        }

        ed.resize = function resize() {
            ed.layout();
        }
        ed.renderer.updateFull = ed.resize.bind(ed);//call resize on ed.renderer.updateFull

        ed.getSession = function getSession() {
            return ed;
        }

        ed.getLength = function getLength() {
            var _model = ed.getModel();
            if (_model !== null) {
                return _model.getLineCount();
            }
            return 0;
        }

        /**
         * Scroll vertically as necessary and reveal a line.
         * @param {Number} lineNumber
         * @param {ScrollType|Number} [scrollType] Immediate: = 1, Smooth: = 0
         */
         ed.scrollToLine = function scrollToLine(lineNumber, scrollType) {
            ed.revealLine(lineNumber, scrollType);
        }

        ed.moveCursorTo = function moveCursorTo(lineNumber, colNumber) {
            ed.setPosition({ lineNumber: lineNumber, column: colNumber });
        }

        // monaco.MarkerSeverity
        // 1: "Hint"
        // 2: "Info"
        // 4: "Warning"
        // 8: "Error"
        // Hint: 1
        // Info: 2
        // Warning: 4
        // Error: 8
        ed.getAnnotations = function getAnnotations() {
            let aceCompatibleMarkers;
            try {
                const _model = ed.getModel();
                if (_model !== null) {
                    const id = _model.getLanguageId(); // e.g.  javascript
                    const ra = _model.uri.authority; //   e.g.  model
                    const rp = _model.uri.path; //        e.g.  /18
                    const rs = _model.uri.scheme; //      e.g.  inmemory
                    const modelMarkers = monaco.editor.getModelMarkers(_model) || [];
                    const thisEditorsMarkers = modelMarkers.filter(function (marker) {
                        const _ra = marker.resource.authority; // e.g.  model
                        const _rp = marker.resource.path; //      e.g.  /18
                        const _rs = marker.resource.scheme; //    e.g.  inmemory
                        return marker.owner == id && _ra === ra && _rp === rp && _rs === rs;
                    })
                    aceCompatibleMarkers = thisEditorsMarkers.map(function (marker) {
                        return {
                            row: marker.startLineNumber, //ACE compatible
                            column: marker.startColumn, //future
                            endColumn: marker.endColumn, //future
                            endRow: marker.endLineNumber, //future
                            text: marker.message,//ACE compatible
                            type: monaco.MarkerSeverity[marker.severity] ? monaco.MarkerSeverity[marker.severity].toLowerCase() : marker.severity //ACE compatible
                        }
                    })
                }
            } catch (error) {
                console.log("Failed to get editor Annotations", error);
            }
            return aceCompatibleMarkers || [];
        }


        //ACE row and col are zero based.  Add 1 for monaco lineNumber and column
        ed.gotoLine = function gotoLine(row, col) {
            ed.setPosition({ lineNumber: row+1, column: col+1 });
        }
        //ACE row and col are zero based.  Minus 1 from monaco lineNumber and column
        ed.getCursorPosition = function getCursorPosition() {
            var p = ed.getPosition();
            return { row: p.lineNumber-1, column: p.column-1 };
        }

        ed.setTheme = function(theme) {
            monaco.editor.setTheme(theme);
            userSelectedTheme = theme;//remember users choice for this session
        }

        ed.on = function (name, cb) {
            switch (name) {
                case "change":
                case "input":
                    name = "onDidChangeModelContent";
                    break;
                case "focus":
                    name = "onDidFocusEditorWidget";
                    break;
                case "blur":
                    name = "onDidBlurEditorWidget";
                    break;
                case "paste":
                    name = "onDidPaste";
                    break;
                default:
                    break;
            }
            var fn;
            if (ed[name]) {
                fn = ed[name]
            } else if (monaco.editor[name]) {
                fn = monaco.editor[name];
            } else {
                console.warn("monaco - unknown event: " + name)
                return;
            }
            fn(cb);
        }
        ed.getUndoManager = function getUndoManager() {
            var o = {};
            function isClean() {
                try {
                    return ed.getModel().canUndo() === false
                } catch (error) {
                    return false
                }

            }
            o.isClean = isClean.bind(ed);
            return o;
        }
        ed.setFontSize = function setFontSize(size) {
            ed.updateOptions({ fontSize: size });
        }
        //#endregion "ACE compatability"

        //final setup
        ed.focusMemory = options.focus;
        ed._mode = editorOptions.language;

        //as models are signleton, consts and let are avialable to other javascript instances
        //so when not focused, set editor mode to text temporarily to avoid multiple defs

        if(editorOptions._language) {
            ed._mode = editorOptions._language;
            ed._tempMode = editorOptions.language;
        }

        ed.onDidBlurEditorWidget(function() {
            ed.focusMemory = false;
            ed.saveView();
            if(isVisible(el) == false) {
                onVisibilityChange(false, 0, el);
            }
        });
        ed.onDidFocusEditorWidget(function() {
            onVisibilityChange(true, 10, el);
        });

        function visibilityWatcher(element, callback) {
            try {
                var options = {
                    root: $(element).closest("div.red-ui-tray-content")[0] || document,
                    attributes: true,
                    childList: true,
                };
                var observer = new IntersectionObserver(function(entries, observer) {
                    entries.forEach(function(entry) {
                        callback(entry.intersectionRatio > 0, 5, entry.target);
                    });
                }, options);
                observer.observe(element);
            } catch (e1) {
                //browser not supporting IntersectionObserver? then fall back to polling!
                try {
                    let elVisibleMem = isVisible(el)
                    watchTimer = setInterval(function() {
                        let elVisible = isVisible(el);
                        if(elVisible != elVisibleMem) {
                            callback(elVisible, 5, element);
                        }
                        elVisibleMem = elVisible;
                    }, 100);
                } catch (e2) { }
            }
        }

        function onVisibilityChange(visible, delay, element) {
            delay = delay || 50;
            if (visible) {
                if (ed.focusMemory) {
                    setTimeout(function () {
                        if (element.parentElement) { //ensure el is still in DOM
                            ed.focus();
                        }
                    }, 300)
                }
                if (ed._initState) {
                    setTimeout(function () {
                        if (element.parentElement) { //ensure el is still in DOM
                            ed.restoreViewState(ed._initState);
                            ed._initState = null;
                        }
                    }, delay);
                }
                if (ed._mode == "javascript" && ed._tempMode == "text") {
                    ed._tempMode = "";
                    setTimeout(function () {
                        if (element.parentElement) { //ensure el is still in DOM
                            ed.setMode('javascript', undefined, false);
                        }
                    }, delay);
                }
            } else if (ed._mode == "javascript" && ed._tempMode != "text") {
                if (element.parentElement) { //ensure el is still in DOM
                    ed.setMode('text', undefined, false);
                    ed._tempMode = "text";
                }
            }
        }

        visibilityWatcher(el, onVisibilityChange);


        if (editorOptions.language === 'markdown') {
            $(el).addClass("red-ui-editor-text-container-toolbar");
            ed.toolbar = RED.editor.customEditTypes['_markdown'].buildToolbar(toolbarRow, ed);
            if (options.expandable !== false) {
                var expandButton = $('<button type="button" class="red-ui-button" style="float: right;"><i class="fa fa-expand"></i></button>').appendTo(ed.toolbar);
                RED.popover.tooltip(expandButton, RED._("markdownEditor.expand"));
                expandButton.on("click", function (e) {
                    e.preventDefault();
                    var value = ed.getValue();
                    ed.saveView();
                    RED.editor.editMarkdown({
                        value: value,
                        width: "Infinity",
                        stateId: options.stateId,
                        cancel: function () {
                            ed.focus();
                        },
                        complete: function (v, cursor) {
                            ed.setValue(v, -1);
                            setTimeout(function () {
                                ed.focus();
                                ed.restoreView();
                            }, 300);
                        }
                    })
                });
            }
            var helpButton = $('<button type="button" class="red-ui-editor-text-help red-ui-button red-ui-button-small"><i class="fa fa-question"></i></button>').appendTo($(el).parent());
            RED.popover.create({
                target: helpButton,
                trigger: 'click',
                size: "small",
                direction: "left",
                content: RED._("markdownEditor.format"),
                autoClose: 50
            });
        }
        ed.getView = function () {
            return ed.saveViewState();
        }
        ed.saveView = function (debuginfo) {
            if (!options.stateId) { return; } //only possible if created with a unique stateId
            window._editorStateMonaco = window._editorStateMonaco || {};
            var state = ed.getView();
            window._editorStateMonaco[options.stateId] = state;
            return state;
        }
        ed.restoreView = function (state) {
            if (!options.stateId) { return; } //only possible if created with a unique stateId
            window._editorStateMonaco = window._editorStateMonaco || {};
            var _state = state || window._editorStateMonaco[options.stateId];
            if (!_state) { return; } //no view state available
            try {
                if (ed.type) { //is editor already initialised?
                    ed.restoreViewState(_state);
                } else {
                    ed._initState = _state;
                }
            } catch (error) {
                delete window._editorStateMonaco[options.stateId];
            }
        };
        ed.restoreView();
        if (options.cursor && !ed._initState) {
            var row = options.cursor.row || options.cursor.lineNumber;
            var col = options.cursor.column || options.cursor.col;
            ed.gotoLine(row, col);
        }
        ed.type = type;
        return ed;
    }

    function isVisible(el) {
        if(!el.offsetHeight && !el.offsetWidth) { return false; }
        if(getComputedStyle(el).visibility === 'hidden') { return false; }
        return true;
    }

    return {
        /**
         * Editor type
         * @memberof RED.editor.codeEditor.monaco
         */
         get type() { return type; },
         /**
          * Editor initialised
         * @memberof RED.editor.codeEditor.monaco
         */
        get initialised() { return initialised; },
        /**
         * Initialise code editor
         * @param {object} options - initialisation options
         * @memberof RED.editor.codeEditor.monaco
         */
        init: init,
        /**
         * Create a code editor
         * @param {object} options - the editor options
         * @memberof RED.editor.codeEditor.monaco
         */
        create: create
    }
})();
;/**
 * 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.
 **/
RED.eventLog = (function() {

    var template = '<script type="text/x-red" data-template-name="_eventLog"><div class="form-row node-text-editor-row"><div style="height: 100%;min-height: 150px;" class="node-text-editor" id="red-ui-event-log-editor"></div></div></script>';

    var eventLogEditor;
    var backlog = [];
    var shown = false;

    function appendLogLine(line) {
        backlog.push(line);
        if (backlog.length > 500) {
            backlog = backlog.slice(-500);
        }
        if (eventLogEditor) {
            eventLogEditor.getSession().insert({
                row: eventLogEditor.getSession().getLength(),
                column: 0
            }, "\n" + line);
            eventLogEditor.scrollToLine(eventLogEditor.getSession().getLength());
        }
    }
    return {
        init: function() {
            $(template).appendTo("#red-ui-editor-node-configs");
            RED.actions.add("core:show-event-log",RED.eventLog.show);
        },
        show: function() {
            if (shown) {
                return;
            }
            shown = true;
            var type = "_eventLog"

            var trayOptions = {
                title: RED._("eventLog.title"),
                width: Infinity,
                buttons: [
                    {
                        id: "node-dialog-close",
                        text: RED._("common.label.close"),
                        click: function() {
                            RED.tray.close();
                        }
                    }
                ],
                resize: function(dimensions) {
                    var rows = $("#dialog-form>div:not(.node-text-editor-row)");
                    var editorRow = $("#dialog-form>div.node-text-editor-row");
                    var height = $("#dialog-form").height();
                    for (var i=0;i<rows.size();i++) {
                        height -= $(rows[i]).outerHeight(true);
                    }
                    height -= (parseInt($("#dialog-form").css("marginTop"))+parseInt($("#dialog-form").css("marginBottom")));
                    $(".node-text-editor").css("height",height+"px");
                    eventLogEditor.resize();
                },
                open: function(tray) {
                    var trayBody = tray.find('.red-ui-tray-body');
                    var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor');
                    eventLogEditor = RED.editor.createEditor({
                        mode:"ace/mode/shell",
                        id: 'red-ui-event-log-editor',
                        value: backlog.join("\n"),
                        lineNumbers: false,
                        readOnly: true,
                        options: {
                            showPrintMargin: false
                        }
                    });
                    setTimeout(function() {
                        eventLogEditor.scrollToLine(eventLogEditor.getSession().getLength());
                    },200);
                    dialogForm.i18n();
                },
                close: function() {
                    eventLogEditor.destroy();
                    eventLogEditor = null;
                    shown = false;
                },
                show: function() {}
            }
            RED.tray.show(trayOptions);
        },
        log: function(id,payload) {
            var ts = (new Date(payload.ts)).toISOString()+" ";
            if (payload.type) {
                ts += "["+payload.type+"] "
            }
            if (payload.data) {
                var data = payload.data;
                if (data.endsWith('\n')) {
                    data = data.substring(0,data.length-1);
                }
                var lines = data.split(/\n/);
                lines.forEach(function(line) {
                    appendLogLine(ts+line);
                })
            }
        },
        startEvent: function(name) {
            backlog.push("");
            backlog.push("-----------------------------------------------------------");
            backlog.push((new Date()).toISOString()+" "+name);
            backlog.push("");
        }
    }
})();
;/**
 * 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.
 **/
 RED.tray = (function() {

    var stack = [];
    var editorStack;
    var openingTray = false;
    var stackHidden = false;

    function resize() {

    }
    function showTray(options) {
        var el = $('<div class="red-ui-tray"></div>');
        // `editor-tray-header` is deprecated - use red-ui-tray-body instead
        var header = $('<div class="red-ui-tray-header editor-tray-header"></div>').appendTo(el);
        var bodyWrapper = $('<div class="red-ui-tray-body-wrapper"></div>').appendTo(el);
        // `editor-tray-body` is deprecated - use red-ui-tray-body instead
        var body = $('<div class="red-ui-tray-body editor-tray-body"></div>').appendTo(bodyWrapper);
        // `editor-tray-footer` is deprecated - use red-ui-tray-footer instead
        var footer = $('<div class="red-ui-tray-footer"></div>').appendTo(el);
        var resizer = $('<div class="red-ui-tray-resize-handle"></div>').appendTo(el);
        // var growButton = $('<a class="red-ui-tray-resize-button" style="cursor: w-resize;"><i class="fa fa-angle-left"></i></a>').appendTo(resizer);
        // var shrinkButton = $('<a class="red-ui-tray-resize-button" style="cursor: e-resize;"><i style="margin-left: 1px;" class="fa fa-angle-right"></i></a>').appendTo(resizer);
        if (options.title) {
            var titles = stack.map(function(e) { return e.options.title });
            titles.push(options.title);
            var title = '<ul class="red-ui-tray-breadcrumbs"><li>'+titles.join("</li><li>")+'</li></ul>';

            $('<div class="red-ui-tray-titlebar">'+title+'</div>').appendTo(header);
        }
        if (options.width === Infinity) {
            options.maximized = true;
            resizer.addClass('red-ui-tray-resize-maximised');
        }
        var buttonBar = $('<div class="red-ui-tray-toolbar"></div>').appendTo(header);
        var primaryButton;
        if (options.buttons) {
            for (var i=0;i<options.buttons.length;i++) {
                var button = options.buttons[i];
                var b = $('<button>').button().appendTo(buttonBar);
                if (button.id) {
                    b.attr('id',button.id);
                }
                if (button.text) {
                    b.text(button.text);
                }
                if (button.click) {
                    b.on("click", (function(action) {
                        return function(evt) {
                            if (!$(this).hasClass('disabled')) {
                                action(evt);
                            }
                        };
                    })(button.click));
                }
                if (button.class) {
                    b.addClass(button.class);
                    if (button.class === "primary") {
                        primaryButton = button;
                    }
                }
            }
        }
        el.appendTo(editorStack);
        var tray = {
            tray: el,
            header: header,
            body: body,
            footer: footer,
            options: options,
            primaryButton: primaryButton
        };
        stack.push(tray);

        if (!options.maximized) {
            el.draggable({
                handle: resizer,
                axis: "x",
                start:function(event,ui) {
                    el.width('auto');
                },
                drag: function(event,ui) {
                    var absolutePosition = editorStack.position().left+ui.position.left
                    if (absolutePosition < 7) {
                        ui.position.left += 7-absolutePosition;
                    } else if (ui.position.left > -tray.preferredWidth-1) {
                        ui.position.left = -Math.min(editorStack.position().left-7,tray.preferredWidth-1);
                    }
                    if (tray.options.resize) {
                        setTimeout(function() {
                            tray.options.resize({width: -ui.position.left});
                        },0);
                    }
                    tray.width = -ui.position.left;
                },
                stop:function(event,ui) {
                    el.width(-ui.position.left);
                    el.css({left:''});
                    if (tray.options.resize) {
                        tray.options.resize({width: -ui.position.left});
                    }
                    tray.width = -ui.position.left;
                }
            });
        }

        function finishBuild() {
            $("#red-ui-header-shade").show();
            $("#red-ui-editor-shade").show();
            $("#red-ui-palette-shade").show();
            $(".red-ui-sidebar-shade").show();
            tray.preferredWidth = Math.max(el.width(),500);
            if (!options.maximized) {
                body.css({"minWidth":tray.preferredWidth-40});
            }
            if (options.width) {
                if (options.width > $("#red-ui-editor-stack").position().left-8) {
                    options.width = $("#red-ui-editor-stack").position().left-8;
                }
                el.width(options.width);
            } else {
                el.width(tray.preferredWidth);
            }

            tray.width = el.width();
            if (tray.width > $("#red-ui-editor-stack").position().left-8) {
                tray.width = Math.max(0/*tray.preferredWidth*/,$("#red-ui-editor-stack").position().left-8);
                el.width(tray.width);
            }

            // tray.body.parent().width(Math.min($("#red-ui-editor-stack").position().left-8,tray.width));


            $("#red-ui-main-container").scrollLeft(0);
            el.css({
                right: -(el.width()+10)+"px",
                transition: "right 0.25s ease"
            });
            handleWindowResize();
            openingTray = true;
            setTimeout(function() {
                setTimeout(function() {
                    if (!options.width) {
                        el.width(Math.min(tray.preferredWidth,$("#red-ui-editor-stack").position().left-8));
                    }
                    if (options.resize) {
                        options.resize({width:el.width()});
                    }
                    if (options.show) {
                        options.show();
                    }
                    setTimeout(function() {
                        // Delay resetting the flag, so we don't close prematurely
                        openingTray = false;
                        raiseTrayZ();
                        handleWindowResize();//cause call to monaco layout
                    },200);
                    if(!options.hasOwnProperty("focusElement")) {
                        //focusElement is not inside options - default to focusing 1st
                        body.find(":focusable:first").trigger("focus");
                    } else if(options.focusElement !== false) {
                        //focusElement IS specified, focus that instead (if not false)
                        $(options.focusElement).trigger("focus");
                    } 

                },150);
                el.css({right:0});
            },0);
        }
        if (options.open) {
            if (options.open.length === 1) {
                options.open(el);
                finishBuild();
            } else {
                options.open(el,finishBuild);
            }
        } else {
            finishBuild();
        }
    }

    function handleWindowResize() {
        if (stack.length > 0) {
            var tray = stack[stack.length-1];
            if (tray.options.maximized || tray.width > $("#red-ui-editor-stack").position().left-8) {
                tray.width = $("#red-ui-editor-stack").position().left-8;
                tray.tray.width(tray.width);
                // tray.body.parent().width(tray.width);
            } else if (tray.width < tray.preferredWidth) {
                tray.width = Math.min($("#red-ui-editor-stack").position().left-8,tray.preferredWidth);
                tray.tray.width(tray.width);
                // tray.body.parent().width(tray.width);
            }
            var trayHeight = tray.tray.height()-tray.header.outerHeight()-tray.footer.outerHeight();
            tray.body.height(trayHeight);
            if (tray.options.resize) {
                tray.options.resize({width:tray.width, height:trayHeight});
            }

        }
    }

    //raise tray z-index to prevent editor context menu being clipped by sidebar
    function raiseTrayZ() {
        setTimeout(function(){
            $('#red-ui-editor-stack').css("zIndex","13");
        },300);
    }
    //lower tray z-index back to original place for correct slide animation (related to fix for editor context menu clipped by sidebar)
    function lowerTrayZ(){
        $('#red-ui-editor-stack').css("zIndex","9");
    }

    return {
        init: function init() {
            editorStack = $("#red-ui-editor-stack");
            $(window).on("resize", handleWindowResize);
            RED.events.on("sidebar:resize",handleWindowResize);
            $("#red-ui-editor-shade").on("click", function() {
                if (!openingTray) {
                    var tray = stack[stack.length-1];
                    if (tray && tray.primaryButton) {
                        tray.primaryButton.click();
                    }
                }
            });
        },
        show: function show(options) {
            lowerTrayZ();
            if (!options) {
                if (stack.length > 0) {
                    var tray = stack[stack.length-1];
                    tray.tray.css({right:0});
                    $("#red-ui-header-shade").show();
                    $("#red-ui-editor-shade").show();
                    $("#red-ui-palette-shade").show();
                    $(".red-ui-sidebar-shade").show();
                    stackHidden = false;
                }
            } else if (stackHidden) {
                throw new Error("Cannot add to stack whilst hidden");
            } else if (stack.length > 0 && !options.overlay) {
                var oldTray = stack[stack.length-1];
                if (options.width === "inherit") {
                    options.width = oldTray.tray.width();
                }
                oldTray.tray.css({
                    right: -(oldTray.tray.width()+10)+"px"
                });
                setTimeout(function() {
                    oldTray.tray.detach();
                    showTray(options);
                    RED.events.emit('editor:change')
                },250)
            } else {
                if (stack.length > 0) {
                    stack[stack.length-1].tray.css("z-index", 0);
                }
                RED.events.emit("editor:open");
                showTray(options);
            }

        },
        hide: function hide() {
            lowerTrayZ();
            if (stack.length > 0) {
                var tray = stack[stack.length-1];
                tray.tray.css({
                    right: -(tray.tray.width()+10)+"px"
                });
                $("#red-ui-header-shade").hide();
                $("#red-ui-editor-shade").hide();
                $("#red-ui-palette-shade").hide();
                $(".red-ui-sidebar-shade").hide();
                stackHidden = true;
            }
        },
        resize: handleWindowResize,
        close: function close(done) {
            lowerTrayZ(); //lower tray z-index for correct animation
            if (stack.length > 0) {
                var tray = stack.pop();
                tray.tray.css({
                    right: -(tray.tray.width()+10)+"px"
                });
                setTimeout(function() {
                    try {
                        if (tray.options.close) { tray.options.close(); }
                    } catch (ex) { }
                    tray.tray.remove();
                    if (stack.length > 0) {
                        var oldTray = stack[stack.length-1];
                        if (!oldTray.options.overlay) {
                            oldTray.tray.appendTo("#red-ui-editor-stack");
                            setTimeout(function() {
                                handleWindowResize();
                                oldTray.tray.css({right:0});
                                if (oldTray.options.show) {
                                    raiseTrayZ();
                                    handleWindowResize();//cause call to monaco layout
                                    oldTray.options.show();
                                }
                            },0);
                        } else {
                            handleWindowResize();
                            if (oldTray.options.show) {
                                oldTray.options.show();
                            }
                        }
                    }
                    if (done) {
                        done();
                    }
                    if (stack.length === 0) {
                        $("#red-ui-header-shade").hide();
                        $("#red-ui-editor-shade").hide();
                        $("#red-ui-palette-shade").hide();
                        $(".red-ui-sidebar-shade").hide();
                        RED.events.emit("editor:close");
                        RED.view.focus();
                    } else {
                        stack[stack.length-1].tray.css("z-index", "auto");
                        RED.events.emit('editor:change')
                    }
                },250)
            }
        }
    }
})();
;/**
 * 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.
 **/


RED.clipboard = (function() {

    var dialog;
    var dialogContainer;
    var exportNodesDialog;
    var importNodesDialog;
    var disabled = false;
    var popover;
    var currentPopoverError;
    var activeTab;
    var libraryBrowser;
    var clipboardTabs;

    var activeLibraries = {};

    var pendingImportConfig;


    function downloadData(file, data) {
        if (window.navigator.msSaveBlob) {
            // IE11 workaround
            // IE does not support data uri scheme for downloading data
            var blob = new Blob([data], {
                type: "data:application/json;charset=utf-8"
            });
            navigator.msSaveBlob(blob, file);
        }
        else {
            var element = document.createElement('a');
            element.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(data));
            element.setAttribute('download', file);
            element.style.display = 'none';
            document.body.appendChild(element);
            element.click();
            document.body.removeChild(element);
        }
    }

    function setupDialogs() {
        dialog = $('<div id="red-ui-clipboard-dialog" class="hide"><form class="dialog-form form-horizontal"></form></div>')
            .appendTo("#red-ui-editor")
            .dialog({
                modal: true,
                autoOpen: false,
                width: 700,
                resizable: false,
                classes: {
                    "ui-dialog": "red-ui-editor-dialog",
                    "ui-dialog-titlebar-close": "hide",
                    "ui-widget-overlay": "red-ui-editor-dialog"
                },
                buttons: [
                    { // red-ui-clipboard-dialog-cancel
                        id: "red-ui-clipboard-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            $( this ).dialog( "close" );
                            RED.view.focus();
                        }
                    },
                    { // red-ui-clipboard-dialog-download
                        id: "red-ui-clipboard-dialog-download",
                        class: "primary",
                        text: RED._("clipboard.download"),
                        click: function() {
                            var data = $("#red-ui-clipboard-dialog-export-text").val();
                            downloadData("flows.json", data);
                            $( this ).dialog( "close" );
                            RED.view.focus();
                        }
                    },
                    { // red-ui-clipboard-dialog-export
                        id: "red-ui-clipboard-dialog-export",
                        class: "primary",
                        text: RED._("clipboard.export.copy"),
                        click: function() {
                            if (activeTab === "red-ui-clipboard-dialog-export-tab-clipboard") {
                                var flowData = $("#red-ui-clipboard-dialog-export-text").val();
                                // Close the dialog first otherwise FireFox won't focus the hidden
                                // clipboard element in copyText
                                $( this ).dialog( "close" );
                                copyText(flowData);
                                RED.notify(RED._("clipboard.nodesExported"),{id:"clipboard"});
                                RED.view.focus();
                            } else {
                                var flowToExport = $("#red-ui-clipboard-dialog-export-text").val();
                                var selectedPath = activeLibraries[activeTab].getSelected();
                                if (!selectedPath.children) {
                                    selectedPath = selectedPath.parent;
                                }
                                var filename = $("#red-ui-clipboard-dialog-tab-library-name").val().trim();
                                var saveFlow = function() {
                                    $.ajax({
                                        url:'library/'+selectedPath.library+'/'+selectedPath.type+'/'+selectedPath.path + filename,
                                        type: "POST",
                                        data: flowToExport,
                                        contentType: "application/json; charset=utf-8"
                                    }).done(function() {
                                        $(dialog).dialog( "close" );
                                        RED.view.focus();
                                        RED.notify(RED._("library.exportedToLibrary"),"success");
                                    }).fail(function(xhr,textStatus,err) {
                                        if (xhr.status === 401) {
                                            RED.notify(RED._("library.saveFailed",{message:RED._("user.notAuthorized")}),"error");
                                        } else {
                                            RED.notify(RED._("library.saveFailed",{message:xhr.responseText}),"error");
                                        }
                                    });
                                }
                                if (selectedPath.children) {
                                    var exists = false;
                                    selectedPath.children.forEach(function(f) {
                                        if (f.label === filename) {
                                            exists = true;
                                        }
                                    });
                                    if (exists) {
                                        dialog.dialog("close");
                                        var notification = RED.notify(RED._("clipboard.export.exists",{file:RED.utils.sanitize(filename)}),{
                                            type: "warning",
                                            fixed: true,
                                            buttons: [{
                                                text: RED._("common.label.cancel"),
                                                click: function() {
                                                    notification.hideNotification()
                                                    dialog.dialog( "open" );
                                                }
                                            },{
                                                text: RED._("clipboard.export.overwrite"),
                                                click: function() {
                                                    notification.hideNotification()
                                                    saveFlow();
                                                }
                                            }]
                                        });
                                    } else {
                                        saveFlow();
                                    }
                                } else {
                                    saveFlow();
                                }
                            }
                        }
                    },
                    { // red-ui-clipboard-dialog-ok
                        id: "red-ui-clipboard-dialog-ok",
                        class: "primary",
                        text: RED._("common.label.import"),
                        click: function() {
                            var addNewFlow = ($("#red-ui-clipboard-dialog-import-opt > a.selected").attr('id') === 'red-ui-clipboard-dialog-import-opt-new');
                            if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") {
                                importNodes($("#red-ui-clipboard-dialog-import-text").val(),addNewFlow);
                            } else {
                                var selectedPath = activeLibraries[activeTab].getSelected();
                                if (selectedPath.path) {
                                    $.get('library/'+selectedPath.library+'/'+selectedPath.type+'/'+selectedPath.path, function(data) {
                                        importNodes(data,addNewFlow);
                                    });
                                }
                            }
                            $( this ).dialog( "close" );
                            RED.view.focus();
                        }
                    },
                    { // red-ui-clipboard-dialog-import-conflict
                        id: "red-ui-clipboard-dialog-import-conflict",
                        class: "primary",
                        text: RED._("clipboard.import.importSelected"),
                        click: function() {
                            var importMap = {};
                            $('#red-ui-clipboard-dialog-import-conflicts-list input[type="checkbox"]').each(function() {
                                importMap[$(this).attr("data-node-id")] = this.checked?"import":"skip";
                            })

                            $('.red-ui-clipboard-dialog-import-conflicts-controls input[type="checkbox"]').each(function() {
                                if (!$(this).attr("disabled")) {
                                    importMap[$(this).attr("data-node-id")] = this.checked?"replace":"copy"
                                }
                            })
                            // skip - don't import
                            // import - import as-is
                            // copy - import with new id
                            // replace - import over the top of existing
                            pendingImportConfig.importOptions.importMap = importMap;

                            var newNodes = pendingImportConfig.importNodes.filter(function(n) {
                                if (!importMap[n.id] || importMap[n.z]) {
                                    importMap[n.id] = importMap[n.z];
                                }
                                return importMap[n.id] !== "skip"
                            })
                            // console.table(pendingImportConfig.importNodes.map(function(n) { return {id:n.id,type:n.type,result:importMap[n.id]}}))
                            RED.view.importNodes(newNodes, pendingImportConfig.importOptions);
                            $( this ).dialog( "close" );
                            RED.view.focus();
                        }
                    }
                ],
                open: function( event, ui ) {
                    RED.keyboard.disable();
                },
                beforeClose: function(e) {
                    if (clipboardTabs && activeTab === "red-ui-clipboard-dialog-export-tab-clipboard") {
                        const jsonTabIndex = clipboardTabs.getTabIndex('red-ui-clipboard-dialog-export-tab-clipboard-json')
                        const activeTabIndex = clipboardTabs.activeIndex()
                        RED.settings.set("editor.dialog.export.json-view", activeTabIndex === jsonTabIndex )
                    }
                },
                close: function(e) {
                    RED.keyboard.enable();
                    if (popover) {
                        popover.close(true);
                        currentPopoverError = null;
                    }
                }
            });

        dialogContainer = dialog.children(".dialog-form");

        exportNodesDialog =
            '<div class="form-row">'+
                '<div style="display: flex; justify-content: space-between;">'+
                    '<div class="form-row">'+
                        '<label style="width:auto;margin-right: 10px;" data-i18n="common.label.export"></label>'+
                        '<span id="red-ui-clipboard-dialog-export-rng-group" class="button-group">'+
                            '<a id="red-ui-clipboard-dialog-export-rng-selected" class="red-ui-button toggle" href="#" data-i18n="clipboard.export.selected"></a>'+
                            '<a id="red-ui-clipboard-dialog-export-rng-flow" class="red-ui-button toggle" href="#" data-i18n="clipboard.export.current"></a>'+
                            '<a id="red-ui-clipboard-dialog-export-rng-full" class="red-ui-button toggle" href="#" data-i18n="clipboard.export.all"></a>'+
                        '</span>'+
                    '</div>'+
                    '<div class="form-row">'+
                        '<label style="width:auto;margin-right: 10px;" data-i18n="common.label.format"></label>'+
                        '<span id="red-ui-clipboard-dialog-export-fmt-group" class="button-group">'+
                            '<a id="red-ui-clipboard-dialog-export-fmt-mini" class="red-ui-button red-ui-button toggle" href="#" data-i18n="clipboard.export.compact"></a>'+
                            '<a id="red-ui-clipboard-dialog-export-fmt-full" class="red-ui-button red-ui-button toggle" href="#" data-i18n="clipboard.export.formatted"></a>'+
                        '</span>'+
                    '</div>'+
                '</div>'+
            '</div>'+
            '<div class="red-ui-clipboard-dialog-box">'+
                '<div class="red-ui-clipboard-dialog-tabs">'+
                    '<ul id="red-ui-clipboard-dialog-export-tabs"></ul>'+
                '</div>'+
                '<div id="red-ui-clipboard-dialog-export-tabs-content" class="red-ui-clipboard-dialog-tabs-content">'+
                    '<div id="red-ui-clipboard-dialog-export-tab-clipboard" class="red-ui-clipboard-dialog-tab-clipboard">'+
                        '<div id="red-ui-clipboard-dialog-export-tab-clipboard-tab-bar">'+
                            '<ul id="red-ui-clipboard-dialog-export-tab-clipboard-tabs"></ul>'+
                        '</div>'+
                        '<div class="red-ui-clipboard-dialog-export-tab-clipboard-tab" id="red-ui-clipboard-dialog-export-tab-clipboard-preview">'+
                            '<div id="red-ui-clipboard-dialog-export-tab-clipboard-preview-list"></div>'+
                        '</div>'+
                        '<div class="red-ui-clipboard-dialog-export-tab-clipboard-tab" id="red-ui-clipboard-dialog-export-tab-clipboard-json">'+
                            '<div class="form-row" style="height:calc(100% - 10px)">'+
                                '<textarea readonly id="red-ui-clipboard-dialog-export-text"></textarea>'+
                            '</div>'+
                        '</div>'+
                    '</div>'+
                    '<div class="form-row" id="red-ui-clipboard-dialog-export-tab-library-filename">'+
                        '<label data-i18n="clipboard.export.exportAs"></label><input id="red-ui-clipboard-dialog-tab-library-name" type="text">'+
                    '</div>'+
                '</div>'+
            '</div>'
            ;


        importNodesDialog =
            '<div class="red-ui-clipboard-dialog-box" style="margin-bottom: 12px">'+
                '<div class="red-ui-clipboard-dialog-tabs">'+
                    '<ul id="red-ui-clipboard-dialog-import-tabs"></ul>'+
                '</div>'+
                '<div id="red-ui-clipboard-dialog-import-tabs-content" class="red-ui-clipboard-dialog-tabs-content">'+
                    '<div id="red-ui-clipboard-dialog-import-tab-clipboard" class="red-ui-clipboard-dialog-tab-clipboard">'+
                        '<div class="form-row"><span data-i18n="clipboard.pasteNodes"></span>'+
                            ' <a class="red-ui-button" id="red-ui-clipboard-dialog-import-file-upload-btn"><i class="fa fa-upload"></i> <span data-i18n="clipboard.selectFile"></span></a>'+
                            '<input type="file" id="red-ui-clipboard-dialog-import-file-upload" accept=".json" style="display:none">'+
                        '</div>'+
                        '<div class="form-row" style="height:calc(100% - 47px)">'+
                            '<textarea id="red-ui-clipboard-dialog-import-text"></textarea>'+
                        '</div>'+
                    '</div>'+
                '</div>'+
            '</div>'+
            '<div class="form-row">'+
                '<label style="width:auto;margin-right: 10px;" data-i18n="clipboard.import.import"></label>'+
                '<span id="red-ui-clipboard-dialog-import-opt" class="button-group">'+
                    '<a id="red-ui-clipboard-dialog-import-opt-current" class="red-ui-button toggle selected" href="#" data-i18n="clipboard.export.current"></a>'+
                    '<a id="red-ui-clipboard-dialog-import-opt-new" class="red-ui-button toggle" href="#" data-i18n="clipboard.import.newFlow"></a>'+
                '</span>'+
            '</div>';

            importConflictsDialog =
                '<div class="form-row">'+
                    '<div class="form-row"><p data-i18n="clipboard.import.conflictNotification1"></p><p data-i18n="clipboard.import.conflictNotification2"></p></div>'+
                    '<div class="red-ui-clipboard-dialog-import-conflicts-list-container">'+
                        '<div id="red-ui-clipboard-dialog-import-conflicts-list"></div>'+
                    '</div>'+
                '</div>';

    }

    var validateExportFilenameTimeout
    function validateExportFilename() {
        if (validateExportFilenameTimeout) {
            clearTimeout(validateExportFilenameTimeout);
        }
        validateExportFilenameTimeout = setTimeout(function() {
            var filenameInput = $("#red-ui-clipboard-dialog-tab-library-name");
            var filename = filenameInput.val().trim();
            var valid = filename.length > 0 && !/[\/\\]/.test(filename);
            if (valid) {
                filenameInput.removeClass("input-error");
                $("#red-ui-clipboard-dialog-export").button("enable");
            } else {
                filenameInput.addClass("input-error");
                $("#red-ui-clipboard-dialog-export").button("disable");
            }
        },100);
    }

    /**
     * Validates if the provided string looks like valid flow json
     * @param {string} flowString the string to validate
     * @returns If valid, returns the node array
     */
    function validateFlowString(flowString) {
        const res = JSON.parse(flowString)
        if (!Array.isArray(res)) {
            throw new Error(RED._("clipboard.import.errors.notArray"));
        }
        for (let i = 0; i < res.length; i++) {
            if (typeof res[i] !== "object") {
                throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i}));
            }
            if (!Object.hasOwn(res[i], 'id')) {
                throw new Error(RED._("clipboard.import.errors.missingId",{index:i}));
            }
            if (!Object.hasOwn(res[i], 'type')) {
                throw new Error(RED._("clipboard.import.errors.missingType",{index:i}));
            }
        }
        return res
    }

    var validateImportTimeout;
    function validateImport() {
        if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") {
            if (validateImportTimeout) {
                clearTimeout(validateImportTimeout);
            }
            validateImportTimeout = setTimeout(function() {
                var importInput = $("#red-ui-clipboard-dialog-import-text");
                var v = importInput.val().trim();
                if (v === "") {
                    popover.close(true);
                    currentPopoverError = null;
                    importInput.removeClass("input-error");
                    $("#red-ui-clipboard-dialog-ok").button("disable");
                    return;
                }
                try {
                    validateFlowString(v)
                    currentPopoverError = null;
                    popover.close(true);
                    importInput.removeClass("input-error");
                    importInput.val(v);
                    $("#red-ui-clipboard-dialog-ok").button("enable");
                } catch(err) {
                    if (v !== "") {
                        importInput.addClass("input-error");
                        var errString = err.toString();
                        if (errString !== currentPopoverError) {
                            // Display the error as-is.
                            // Error messages are only in English. Each browser has its
                            // own set of messages with very little consistency.
                            // To provide translated messages this code will either need to:
                            // - reduce everything down to 'unexpected token at position x'
                            //   which is the least useful, but most consistent message
                            // - use a custom/library parser that gives consistent messages
                            //   which can be translated.
                            var message = $('<div class="red-ui-clipboard-import-error"></div>').text(errString);
                            var errorPos;
                            // Chrome error messages
                            var m = /at position (\d+)/i.exec(errString);
                            if (m) {
                                errorPos = parseInt(m[1]);
                            } else {
                                // Firefox error messages
                                m = /at line (\d+) column (\d+)/i.exec(errString);
                                if (m) {
                                    var line = parseInt(m[1])-1;
                                    var col = parseInt(m[2])-1;
                                    var lines = v.split("\n");
                                    errorPos = 0;
                                    for (var i=0;i<line;i++) {
                                        errorPos += lines[i].length+1;
                                    }
                                    errorPos += col;
                                } else {
                                    // Safari doesn't provide any position information
                                    // IE: tbd
                                }
                            }

                            if (errorPos !== undefined) {
                                v = v.replace(/\n/g,"↵");
                                var index = parseInt(m[1]);
                                var parseError = $('<div>').appendTo(message);
                                var code = $('<pre>').appendTo(parseError);
                                $('<span>').text(v.substring(errorPos-12,errorPos)).appendTo(code)
                                $('<span class="error">').text(v.charAt(errorPos)).appendTo(code);
                                $('<span>').text(v.substring(errorPos+1,errorPos+12)).appendTo(code);
                            }
                            popover.close(true).setContent(message).open();
                            currentPopoverError = errString;
                        }
                    } else {
                        currentPopoverError = null;
                    }
                    $("#red-ui-clipboard-dialog-ok").button("disable");
                }
            },100);
        } else {
            var file = activeLibraries[activeTab].getSelected();
            if (file && file.label && !file.children) {
                $("#red-ui-clipboard-dialog-ok").button("enable");
            } else {
                $("#red-ui-clipboard-dialog-ok").button("disable");
            }
        }
    }

    function showImportNodes(library = 'clipboard') {
        if (disabled) {
            return;
        }

        dialogContainer.empty();
        dialogContainer.append($(importNodesDialog));

        var tabs = RED.tabs.create({
            id: "red-ui-clipboard-dialog-import-tabs",
            vertical: true,
            onchange: function(tab) {
                $("#red-ui-clipboard-dialog-import-tabs-content").children().hide();
                $("#" + tab.id).show();
                activeTab = tab.id;
                if (popover) {
                    popover.close(true);
                    currentPopoverError = null;
                }
                if (tab.id === "red-ui-clipboard-dialog-import-tab-clipboard") {
                    $("#red-ui-clipboard-dialog-import-text").trigger("focus");
                } else {
                    activeLibraries[tab.id].focus();
                }
                validateImport();
            }
        });
        tabs.addTab({
            id: "red-ui-clipboard-dialog-import-tab-clipboard",
            label: RED._("clipboard.clipboard")
        });

        var libraries = RED.settings.libraries || [];
        libraries.forEach(function(lib) {
            var tabId = "red-ui-clipboard-dialog-import-tab-"+lib.id
            tabs.addTab({
                id: tabId,
                label: RED._(lib.label||lib.id)
            })

            var content = $('<div id="red-ui-clipboard-dialog-import-tab-library" class="red-ui-clipboard-dialog-tab-library"></div>')
                .attr("id",tabId)
                .hide()
                .appendTo("#red-ui-clipboard-dialog-import-tabs-content");

            var browser = RED.library.createBrowser({
                container: content,
                onselect: function(file) {
                    if (file && file.label && !file.children) {
                        $("#red-ui-clipboard-dialog-ok").button("enable");
                    } else {
                        $("#red-ui-clipboard-dialog-ok").button("disable");
                    }
                },
                onconfirm: function(item) {
                    if (item && item.label && !item.children) {
                        $("#red-ui-clipboard-dialog-ok").trigger("click");
                    }
                }
            })
            loadFlowLibrary(browser,lib);
            activeLibraries[tabId] = browser;
        })

        $("#red-ui-clipboard-dialog-tab-library-name").on("keyup", validateExportFilename);
        $("#red-ui-clipboard-dialog-tab-library-name").on('paste',function() { setTimeout(validateExportFilename,10)});
        $("#red-ui-clipboard-dialog-export").button("enable");

        dialogContainer.i18n();

        $("#red-ui-clipboard-dialog-ok").show();
        $("#red-ui-clipboard-dialog-cancel").show();
        $("#red-ui-clipboard-dialog-export").hide();
        $("#red-ui-clipboard-dialog-download").hide();
        $("#red-ui-clipboard-dialog-import-conflict").hide();

        $("#red-ui-clipboard-dialog-ok").button("disable");
        $("#red-ui-clipboard-dialog-import-text").on("keyup", validateImport);
        $("#red-ui-clipboard-dialog-import-text").on('paste',function() { setTimeout(validateImport,10)});

        if (RED.workspaces.active() === 0 || RED.workspaces.isLocked()) {
            $("#red-ui-clipboard-dialog-import-opt-current").addClass('disabled').removeClass("selected");
            $("#red-ui-clipboard-dialog-import-opt-new").addClass("selected");
        } else {
            $("#red-ui-clipboard-dialog-import-opt-current").removeClass('disabled').addClass("selected");
            $("#red-ui-clipboard-dialog-import-opt-new").removeClass("selected");
        }
        $("#red-ui-clipboard-dialog-import-opt > a").on("click", function(evt) {
            evt.preventDefault();
            if ($(this).hasClass('disabled') || $(this).hasClass('selected')) {
                return;
            }
            $(this).parent().children().removeClass('selected');
            $(this).addClass('selected');
        });

        $("#red-ui-clipboard-dialog-import-file-upload").on("change", function() {
            var fileReader = new FileReader();
            fileReader.onload = function () {
                $("#red-ui-clipboard-dialog-import-text").val(fileReader.result);
                validateImport();
            };
            fileReader.readAsText($(this).prop('files')[0]);
        })
        $("#red-ui-clipboard-dialog-import-file-upload-btn").on("click", function(evt) {
            evt.preventDefault();
            $("#red-ui-clipboard-dialog-import-file-upload").trigger("click");
        })

        tabs.activateTab("red-ui-clipboard-dialog-import-tab-"+library);
        if (library === 'clipboard') {
            setTimeout(function() {
                $("#red-ui-clipboard-dialog-import-text").trigger("focus");
            },100)
        }

        var dialogHeight = 400;
        var winHeight = $(window).height();
        if (winHeight < 600) {
            dialogHeight = 400 - (600 - winHeight);
        }
        $(".red-ui-clipboard-dialog-box").height(dialogHeight);

        dialog.dialog("option","title",RED._("clipboard.importNodes"))
              .dialog("option","width",700)
              .dialog("open");
        popover = RED.popover.create({
            target: $("#red-ui-clipboard-dialog-import-text"),
            trigger: "manual",
            direction: "bottom",
            content: ""
        });
    }

    /**
     * Show the export dialog
     * @params library which export destination to show
     * @params mode whether to default to 'auto' (default) or 'flow'
     **/
    function showExportNodes(library = 'clipboard', mode = 'auto' ) {
        if (disabled) {
            return;
        }

        dialogContainer.empty();
        dialogContainer.append($(exportNodesDialog));
        clipboardTabs = null
        var tabs = RED.tabs.create({
            id: "red-ui-clipboard-dialog-export-tabs",
            vertical: true,
            onchange: function(tab) {
                $("#red-ui-clipboard-dialog-export-tabs-content").children().hide();
                $("#" + tab.id).show();
                activeTab = tab.id;
                if (tab.id === "red-ui-clipboard-dialog-export-tab-clipboard") {
                    $("#red-ui-clipboard-dialog-export").button("option","label", RED._("clipboard.export.copy"))
                    $("#red-ui-clipboard-dialog-download").show();
                    $("#red-ui-clipboard-dialog-export-tab-library-filename").hide();
                } else {
                    $("#red-ui-clipboard-dialog-export").button("option","label", RED._("clipboard.export.export"))
                    $("#red-ui-clipboard-dialog-download").hide();
                    $("#red-ui-clipboard-dialog-export-tab-library-filename").show();
                    activeLibraries[activeTab].focus();
                }

            }
        });
        tabs.addTab({
            id: "red-ui-clipboard-dialog-export-tab-clipboard",
            label: RED._("clipboard.clipboard")
        });


        var libraries = RED.settings.libraries || [];

        libraries.forEach(function(lib) {
            if (lib.readOnly) {
                return
            }
            var tabId = "red-ui-clipboard-dialog-export-tab-library-"+lib.id
            tabs.addTab({
                id: tabId,
                label: RED._(lib.label||lib.id)
            })

            var content = $('<div class="red-ui-clipboard-dialog-export-tab-library-browser red-ui-clipboard-dialog-tab-library"></div>')
                .attr("id",tabId)
                .hide()
                .insertBefore("#red-ui-clipboard-dialog-export-tab-library-filename");

            var browser = RED.library.createBrowser({
                container: content,
                folderTools: true,
                onselect: function(file) {
                    if (file && file.label && !file.children) {
                        $("#red-ui-clipboard-dialog-tab-library-name").val(file.label);
                    }
                },
            })
            loadFlowLibrary(browser,lib);
            activeLibraries[tabId] = browser;
        })

        $("#red-ui-clipboard-dialog-tab-library-name").on("keyup", validateExportFilename);
        $("#red-ui-clipboard-dialog-tab-library-name").on('paste',function() { setTimeout(validateExportFilename,10)});
        $("#red-ui-clipboard-dialog-export").button("enable");

        clipboardTabs = RED.tabs.create({
            id: "red-ui-clipboard-dialog-export-tab-clipboard-tabs",
            onchange: function(tab) {
                $(".red-ui-clipboard-dialog-export-tab-clipboard-tab").hide();
                $("#" + tab.id).show();
            }
        });

        clipboardTabs.addTab({
            id: "red-ui-clipboard-dialog-export-tab-clipboard-preview",
            label: RED._("clipboard.exportNodes")
        });

        clipboardTabs.addTab({
            id: "red-ui-clipboard-dialog-export-tab-clipboard-json",
            label: RED._("editor.types.json")
        });
        if (RED.settings.get("editor.dialog.export.json-view") === true) {
            clipboardTabs.activateTab("red-ui-clipboard-dialog-export-tab-clipboard-json");
        }

        var previewList = $("#red-ui-clipboard-dialog-export-tab-clipboard-preview-list").css({position:"absolute",top:0,right:0,bottom:0,left:0}).treeList({
            data: []
        })
        refreshExportPreview();

        $("#red-ui-clipboard-dialog-tab-library-name").val("flows.json").select();

        dialogContainer.i18n();
        
        var format = RED.settings.flowFilePretty ? "red-ui-clipboard-dialog-export-fmt-full" : "red-ui-clipboard-dialog-export-fmt-mini";
        const userFormat = RED.settings.get("editor.dialog.export.pretty")
        if (userFormat === false || userFormat === true) {
            format = userFormat ? "red-ui-clipboard-dialog-export-fmt-full" : "red-ui-clipboard-dialog-export-fmt-mini";
        }

        $("#red-ui-clipboard-dialog-export-fmt-group > a").on("click", function(evt) {
            evt.preventDefault();
            if ($(this).hasClass('disabled') || $(this).hasClass('selected')) {
                $("#red-ui-clipboard-dialog-export-text").trigger("focus");
                return;
            }
            $(this).parent().children().removeClass('selected');
            $(this).addClass('selected');

            var flow = $("#red-ui-clipboard-dialog-export-text").val();
            if (flow.length > 0) {
                var nodes = JSON.parse(flow);

                format = $(this).attr('id');
                const pretty = format === "red-ui-clipboard-dialog-export-fmt-full";
                if (pretty) {
                    flow = JSON.stringify(nodes,null,4);
                } else {
                    flow = JSON.stringify(nodes);
                }
                $("#red-ui-clipboard-dialog-export-text").val(flow);
                setTimeout(function() { $("#red-ui-clipboard-dialog-export-text").scrollTop(0); },50);

                $("#red-ui-clipboard-dialog-export-text").trigger("focus");
                RED.settings.set("editor.dialog.export.pretty", pretty)
            }
        });

        $("#red-ui-clipboard-dialog-export-rng-group > a").on("click", function(evt) {
            evt.preventDefault();
            if ($(this).hasClass('disabled') || $(this).hasClass('selected')) {
                return;
            }
            $(this).parent().children().removeClass('selected');
            $(this).addClass('selected');
            var type = $(this).attr('id').substring("red-ui-clipboard-dialog-export-rng-".length);
            var flow = "";
            var nodes = null;
            if (type === 'selected') {
                var selection = RED.workspaces.selection();
                if (selection.length > 0) {
                    nodes = [];
                    selection.forEach(function(n) {
                        nodes.push(n);
                        nodes = nodes.concat(RED.nodes.groups(n.id));
                        nodes = nodes.concat(RED.nodes.filterNodes({z:n.id}));
                    });
                } else {
                    nodes = RED.view.selection().nodes||[];
                }
                // Don't include the subflow meta-port nodes in the exported selection
                nodes = RED.nodes.createExportableNodeSet(nodes.filter(function(n) { return n.type !== 'subflow'}));
            } else if (type === 'flow') {
                var activeWorkspace = RED.workspaces.active();
                nodes = RED.nodes.groups(activeWorkspace);
                nodes = nodes.concat(RED.nodes.junctions(activeWorkspace));
                nodes = nodes.concat(RED.nodes.filterNodes({z:activeWorkspace}));
                RED.nodes.eachConfig(function(n) {
                    if (n.z === RED.workspaces.active() && n._def.hasUsers === false) {
                        // Grab any config nodes scoped to this flow that don't
                        // require any flow-nodes to use them
                        nodes.push(n);
                    }
                });
                var parentNode = RED.nodes.workspace(activeWorkspace)||RED.nodes.subflow(activeWorkspace);
                nodes.unshift(parentNode);
                nodes = RED.nodes.createExportableNodeSet(nodes);
            } else if (type === 'full') {
                nodes = RED.nodes.createCompleteNodeSet({ credentials: false });
            }
            if (nodes !== null) {
                if (format === "red-ui-clipboard-dialog-export-fmt-full") {
                    flow = JSON.stringify(nodes,null,4);
                } else {
                    flow = JSON.stringify(nodes);
                }
            }
            if (flow.length > 0) {
                $("#red-ui-clipboard-dialog-export").removeClass('disabled');
            } else {
                $("#red-ui-clipboard-dialog-export").addClass('disabled');
            }
            $("#red-ui-clipboard-dialog-export-text").val(flow);
            setTimeout(function() {
                $("#red-ui-clipboard-dialog-export-text").scrollTop(0);
                refreshExportPreview(type);
            },50);
        })

        $("#red-ui-clipboard-dialog-ok").hide();
        $("#red-ui-clipboard-dialog-cancel").hide();
        $("#red-ui-clipboard-dialog-export").hide();
        $("#red-ui-clipboard-dialog-import-conflict").hide();

        if (RED.workspaces.active() === 0) {
            $("#red-ui-clipboard-dialog-export-rng-selected").addClass('disabled').removeClass('selected');
            $("#red-ui-clipboard-dialog-export-rng-flow").addClass('disabled').removeClass('selected');
            $("#red-ui-clipboard-dialog-export-rng-full").trigger("click");
        } else {
            var selection = RED.workspaces.selection();
            if (selection.length > 0) {
                $("#red-ui-clipboard-dialog-export-rng-selected").trigger("click");
            } else {
                selection = RED.view.selection();
                if (selection.nodes) {
                    $("#red-ui-clipboard-dialog-export-rng-selected").trigger("click");
                } else {
                    $("#red-ui-clipboard-dialog-export-rng-selected").addClass('disabled').removeClass('selected');
                    $("#red-ui-clipboard-dialog-export-rng-flow").trigger("click");
                }
            }
        }
        if (mode === 'flow' && !$("#red-ui-clipboard-dialog-export-rng-flow").hasClass('disabled')) {
            $("#red-ui-clipboard-dialog-export-rng-flow").trigger("click");
        }
        if (format === "red-ui-clipboard-dialog-export-fmt-full") {
            $("#red-ui-clipboard-dialog-export-fmt-full").trigger("click");
        } else {
            $("#red-ui-clipboard-dialog-export-fmt-mini").trigger("click");
        }
        tabs.activateTab("red-ui-clipboard-dialog-export-tab-"+library);

        var dialogHeight = 400;
        var winHeight = $(window).height();
        if (winHeight < 600) {
            dialogHeight = 400 - (600 - winHeight);
        }
        $(".red-ui-clipboard-dialog-box").height(dialogHeight);

        dialog.dialog("option","title",RED._("clipboard.exportNodes"))
              .dialog("option","width",700)
              .dialog("open");

        $("#red-ui-clipboard-dialog-export-text").trigger("focus");
        $("#red-ui-clipboard-dialog-cancel").show();
        $("#red-ui-clipboard-dialog-export").show();
        $("#red-ui-clipboard-dialog-download").show();
        $("#red-ui-clipboard-dialog-import-conflict").hide();

    }

    function refreshExportPreview(type) {

        var flowData = $("#red-ui-clipboard-dialog-export-text").val() || "[]";
        var flow = JSON.parse(flowData);
        var flows = {};
        var subflows = {};
        var nodes = [];
        var nodesByZ = {};

        var treeFlows = [];
        var treeSubflows = [];

        flow.forEach(function(node) {
            if (node.type === "tab") {
                flows[node.id] = {
                    element: getFlowLabel(node),
                    deferBuild: type !== "flow",
                    expanded: type === "flow",
                    children: []
                };
                treeFlows.push(flows[node.id])
            } else if (node.type === "subflow") {
                subflows[node.id] = {
                    element: getNodeLabel(node,false),
                    deferBuild: true,
                    children: []
                };
                treeSubflows.push(subflows[node.id])
            } else {
                nodes.push(node);
            }
        });

        var globalNodes = [];
        var parentlessNodes = [];

        nodes.forEach(function(node) {
            var treeNode = {
                element: getNodeLabel(node, false, false)
            };
            if (node.z) {
                if (!flows[node.z] && !subflows[node.z]) {
                    parentlessNodes.push(treeNode)
                } else if (flows[node.z]) {
                    flows[node.z].children.push(treeNode)
                } else if (subflows[node.z]) {
                    subflows[node.z].children.push(treeNode)
                }
            } else {
                globalNodes.push(treeNode);
            }
        });
        var treeData = [];

        if (parentlessNodes.length > 0) {
            treeData = treeData.concat(parentlessNodes);
        }
        if (type === "flow") {
            treeData = treeData.concat(treeFlows);
        } else if (treeFlows.length > 0) {
            treeData.push({
                label: RED._("menu.label.flows"),
                deferBuild: treeFlows.length > 20,
                expanded: treeFlows.length <= 20,
                children: treeFlows
            })
        }
        if (treeSubflows.length > 0) {
            treeData.push({
                label: RED._("menu.label.subflows"),
                deferBuild: treeSubflows.length > 10,
                expanded: treeSubflows.length <= 10,
                children: treeSubflows
            })
        }
        if (globalNodes.length > 0) {
            treeData.push({
                label: RED._("sidebar.info.globalConfig"),
                deferBuild: globalNodes.length > 10,
                expanded: globalNodes.length <= 10,
                children: globalNodes
            })
        }

        $("#red-ui-clipboard-dialog-export-tab-clipboard-preview-list").treeList('data',treeData);
    }

    function loadFlowLibrary(browser,library) {
        var icon = 'fa fa-hdd-o';
        if (library.icon) {
            var fullIcon = RED.utils.separateIconPath(library.icon);
            icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file;
        }
        browser.data([{
            library: library.id,
            type: "flows",
            icon: icon,
            label: RED._(library.label||library.id),
            path: "",
            expanded: true,
            children: [{
                library: library.id,
                type: "flows",
                icon: 'fa fa-cube',
                label: "flows",
                path: "",
                expanded: true,
                children: function(done, item) {
                    RED.library.loadLibraryFolder(library.id,"flows","",function(children) {
                        item.children = children;
                        done(children);
                    })
                }
            }]
        }], true);

    }

    function hideDropTarget() {
        $("#red-ui-drop-target").hide();
    }
    function copyText(value,element,msg) {
        var truncated = false;
        var currentFocus = document.activeElement;
        if (typeof value !== "string" ) {
            value = JSON.stringify(value, function(key,value) {
                if (value !== null && typeof value === 'object') {
                    if (value.__enc__) {
                        if (value.hasOwnProperty('data') && value.hasOwnProperty('length')) {
                            truncated = value.data.length !== value.length;
                            return value.data;
                        }
                        if (value.type === 'function' || value.type === 'internal') {
                            return undefined
                        }
                        if (value.type === 'number') {
                            // Handle NaN and Infinity - they are not permitted
                            // in JSON. We can either substitute with a String
                            // representation or null
                            return null;
                        }
                        if (value.type === 'bigint') {
                            return value.data.toString();
                        }
                        if (value.type === 'undefined') {
                            return undefined;
                        }
                    }
                }
                return value;
            });
        }
        if (truncated) {
            msg += "_truncated";
        }
        var clipboardHidden = $('<textarea type="text" id="red-ui-clipboard-hidden" tabIndex="-1">').appendTo(document.body);
        clipboardHidden.val(value).focus().select();
        var result =  document.execCommand("copy");
        if (result && element) {
            var popover = RED.popover.create({
                target: element,
                direction: 'left',
                size: 'small',
                content: RED._(msg)
            });
            setTimeout(function() {
                popover.close();
            },1000);
            popover.open();
        }
        clipboardHidden.remove();
        if (currentFocus) {
            $(currentFocus).focus();
        }
        return result;
    }

    function importNodes(nodesStr,addFlow) {
        let newNodes = nodesStr;
        if (typeof nodesStr === 'string') {
            try {
                nodesStr = nodesStr.trim();
                if (nodesStr.length === 0) {
                    return;
                }
                newNodes = validateFlowString(nodesStr)
            } catch(err) {
                const e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
                e.code = "NODE_RED";
                throw e;
            }
        }
        var importOptions = {generateIds: false, addFlow: addFlow};
        try {
            RED.view.importNodes(newNodes, importOptions);
        } catch(error) {
            // Thrown for import_conflict
            confirmImport(error.importConfig, newNodes, importOptions);
        }
    }

    function confirmImport(importConfig,importNodes,importOptions) {
        var notification = RED.notify("<p>"+RED._("clipboard.import.conflictNotification1")+"</p>",{
            type: "info",
            fixed: true,
            buttons: [
                {text: RED._("common.label.cancel"), click: function() { notification.close(); }},
                {text: RED._("clipboard.import.viewNodes"), click: function() {
                    notification.close();
                    showImportConflicts(importConfig,importNodes,importOptions);
                }},
                {text: RED._("clipboard.import.importCopy"), click: function() {
                    notification.close();
                    // generateIds=true to avoid conflicts
                    // and default to the 'old' behaviour around matching
                    // config nodes and subflows
                    importOptions.generateIds = true;
                    RED.view.importNodes(importNodes, importOptions);
                }}
            ]
        })
    }

    function showImportConflicts(importConfig,importNodes,importOptions) {

        pendingImportConfig = {
            importConfig: importConfig,
            importNodes: importNodes,
            importOptions: importOptions
        }

        var id,node;
        var treeData = [];
        var container;
        var addedHeader = false;
        for (id in importConfig.subflows) {
            if (importConfig.subflows.hasOwnProperty(id)) {
                if (!addedHeader) {
                    treeData.push({gutter:$('<span data-i18n="menu.label.subflows"></span>'), label: '', class:"red-ui-clipboard-dialog-import-conflicts-item-header"})
                    addedHeader = true;
                }
                node = importConfig.subflows[id];
                var isConflicted = importConfig.conflicted[node.id];
                var isSelected = !isConflicted;
                var elements = getNodeElement(node, isConflicted, isSelected );
                container = {
                    id: node.id,
                    gutter: elements.gutter.element,
                    element: elements.element,
                    class: isSelected?"":"disabled",
                    deferBuild: true,
                    children: []
                }
                treeData.push(container);
                if (importConfig.zMap[id]) {
                    importConfig.zMap[id].forEach(function(node) {
                        var childElements = getNodeElement(node, importConfig.conflicted[node.id], isSelected, elements.gutter.cb);
                        container.children.push({
                            id: node.id,
                            gutter: childElements.gutter.element,
                            element: childElements.element,
                            class: isSelected?"":"disabled"
                        })
                    });
                }
            }
        }
        addedHeader = false;
        for (id in importConfig.tabs) {
            if (importConfig.tabs.hasOwnProperty(id)) {
                if (!addedHeader) {
                    treeData.push({gutter:$('<span data-i18n="menu.label.flows"></span>'), label: '', class:"red-ui-clipboard-dialog-import-conflicts-item-header"})
                    addedHeader = true;
                }
                node = importConfig.tabs[id];
                var isConflicted = importConfig.conflicted[node.id];
                var isSelected = true;
                var elements = getNodeElement(node, isConflicted, isSelected);
                container = {
                    id: node.id,
                    gutter: elements.gutter.element,
                    element: elements.element,
                    icon: "red-ui-icons red-ui-icons-flow",
                    deferBuild: true,
                    class: isSelected?"":"disabled",
                    children: []
                }
                treeData.push(container);
                if (importConfig.zMap[id]) {
                    importConfig.zMap[id].forEach(function(node) {
                        var childElements = getNodeElement(node, importConfig.conflicted[node.id], isSelected, elements.gutter.cb);
                        container.children.push({
                            id: node.id,
                            gutter: childElements.gutter.element,
                            element: childElements.element,
                            class: isSelected?"":"disabled"
                        })
                        // console.log("   ["+(importConfig.conflicted[node.id]?"*":" ")+"] "+node.type+" "+node.id);
                    });
                }
            }
        }
        addedHeader = false;
        var extraNodes = [];
        importConfig.all.forEach(function(node) {
            if (node.type !== "tab" && node.type !== "subflow" && !importConfig.tabs[node.z] && !importConfig.subflows[node.z]) {
                var isConflicted = importConfig.conflicted[node.id];
                var isSelected = !isConflicted || !importConfig.configs[node.id];
                var elements = getNodeElement(node, isConflicted, isSelected);
                var item = {
                    id: node.id,
                    gutter: elements.gutter.element,
                    element: elements.element,
                    class: isSelected?"":"disabled"
                }
                if (importConfig.configs[node.id]) {
                    extraNodes.push(item);
                } else {
                    if (!addedHeader) {
                        treeData.push({gutter:$('<span data-i18n="menu.label.nodes"></span>'), label: '', class:"red-ui-clipboard-dialog-import-conflicts-item-header"})
                        addedHeader = true;
                    }
                    treeData.push(item);
                }
                // console.log("["+(importConfig.conflicted[node.id]?"*":" ")+"] "+node.type+" "+node.id);
            }
        })
        if (extraNodes.length > 0) {
            treeData.push({gutter:$('<span data-i18n="menu.label.displayConfig"></span>'), label: '', class:"red-ui-clipboard-dialog-import-conflicts-item-header"})
            addedHeader = true;
            treeData = treeData.concat(extraNodes);

        }
        dialogContainer.empty();
        dialogContainer.append($(importConflictsDialog));


        var nodeList = $("#red-ui-clipboard-dialog-import-conflicts-list").css({position:"absolute",top:0,right:0,bottom:0,left:0}).treeList({
            data: treeData
        })

        dialogContainer.i18n();
        var dialogHeight = 400;
        var winHeight = $(window).height();
        if (winHeight < 600) {
            dialogHeight = 400 - (600 - winHeight);
        }
        $(".red-ui-clipboard-dialog-box").height(dialogHeight);

        $("#red-ui-clipboard-dialog-ok").hide();
        $("#red-ui-clipboard-dialog-cancel").show();
        $("#red-ui-clipboard-dialog-export").hide();
        $("#red-ui-clipboard-dialog-download").hide();
        $("#red-ui-clipboard-dialog-import-conflict").show();


        dialog.dialog("option","title",RED._("clipboard.importNodes"))
              .dialog("option","width",500)
              .dialog( "open" );

    }

    function getNodeElement(n, isConflicted, isSelected, parent) {
        var element;
        if (n.type === "tab") {
            element = getFlowLabel(n, isConflicted);
        } else {
            element = getNodeLabel(n, isConflicted, isSelected, parent);
        }
        var controls = $('<div>',{class:"red-ui-clipboard-dialog-import-conflicts-controls"}).appendTo(element);
        controls.on("click", function(evt) { evt.stopPropagation(); });
        if (isConflicted && !parent) {
            var cb = $('<label><input '+(isSelected?'':'disabled ')+'type="checkbox" data-node-id="'+n.id+'"> <span data-i18n="clipboard.import.replace"></span></label>').appendTo(controls);
            if (n.type === "tab" || (n.type !== "subflow" && n.hasOwnProperty("x") && n.hasOwnProperty("y"))) {
                cb.hide();
            }
        }
        return {
            element: element,
            gutter: getGutter(n, isSelected, parent)
        }
    }

    function getGutter(n, isSelected, parent) {
        var span = $("<label>",{class:"red-ui-clipboard-dialog-import-conflicts-gutter"});
        var cb = $('<input data-node-id="'+n.id+'" type="checkbox" '+(isSelected?"checked":"")+'>').appendTo(span);

        if (parent) {
            cb.attr("disabled",true);
            parent.addChild(cb);
        }
        span.on("click", function(evt) {
            evt.stopPropagation();
        })
        cb.on("change", function(evt) {
            var state = this.checked;
            span.parent().toggleClass("disabled",!!!state);
            span.parent().find('.red-ui-clipboard-dialog-import-conflicts-controls  input[type="checkbox"]').attr("disabled",!!!state);
            childItems.forEach(function(c) {
                c.attr("checked",state);
                c.trigger("change");
            });
        })
        var childItems = [];

        var checkbox = {
            addChild: function(c) {
                childItems.push(c);
            }
        }

        return {
            cb: checkbox,
            element: span
        }
    }

    function getFlowLabel(n, isConflicted) {
        n = JSON.parse(JSON.stringify(n));
        n._def = RED.nodes.getType(n.type) || {};
        if (n._def) {
            n._ = n._def._;
        }

        var div = $('<div>',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow red-ui-node-list-item"});
        var contentDiv = $('<div>',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div);
        var label = (typeof n === "string")? n : n.label;
        var newlineIndex = label.indexOf("\\n");
        if (newlineIndex > -1) {
            label = label.substring(0,newlineIndex)+"...";
        }
        contentDiv.text(label);

        if (!!isConflicted) {
            const conflictIcon = $('<span style="padding: 0 10px;"><i class="fa fa-exclamation-circle"></span>').appendTo(div)
            RED.popover.tooltip(conflictIcon, RED._('clipboard.import.alreadyExists'))
        }

        // A conflicted flow should not be imported by default.
        return div;
    }

    function getNodeLabel(n, isConflicted, isSelected, parent) {
        n = JSON.parse(JSON.stringify(n));
        n._def = RED.nodes.getType(n.type) || {};
        if (n._def) {
            n._ = n._def._;
        }
        var div = $('<div>',{class:"red-ui-node-list-item"});
        RED.utils.createNodeIcon(n,true).appendTo(div);

        if (!parent && !!isConflicted) {
            const conflictIcon = $('<span style="padding: 0 10px;"><i class="fa fa-exclamation-circle"></span>').appendTo(div)
            RED.popover.tooltip(conflictIcon, RED._('clipboard.import.alreadyExists'))
        }
        return div;
    }

    return {
        init: function() {
            setupDialogs();

            RED.actions.add("core:show-export-dialog",showExportNodes);
            RED.actions.add("core:show-import-dialog",showImportNodes);

            RED.actions.add("core:show-library-export-dialog",function() { showExportNodes('library') });
            RED.actions.add("core:show-library-import-dialog",function() { showImportNodes('library') });

            RED.actions.add("core:show-examples-import-dialog",function() { showImportNodes('examples') });

            RED.events.on("editor:open",function() { disabled = true; });
            RED.events.on("editor:close",function() { disabled = false; });
            RED.events.on("search:open",function() { disabled = true; });
            RED.events.on("search:close",function() { disabled = false; });
            RED.events.on("actionList:open",function() { disabled = true; });
            RED.events.on("actionList:close",function() { disabled = false; });
            RED.events.on("type-search:open",function() { disabled = true; });
            RED.events.on("type-search:close",function() { disabled = false; });

            $('<div id="red-ui-drop-target"><div data-i18n="[append]workspace.dropFlowHere"><i class="fa fa-download"></i><br></div></div>').appendTo('#red-ui-editor');

            RED.keyboard.add("#red-ui-drop-target", "escape" ,hideDropTarget);

            $('#red-ui-workspace-chart').on("dragenter",function(event) {
                if (!RED.workspaces.isLocked() && (
                    $.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1 ||
                     $.inArray("Files",event.originalEvent.dataTransfer.types) != -1)) {
                    $("#red-ui-drop-target").css({display:'table'}).focus();
                }
            });

            $('#red-ui-drop-target').on("dragover",function(event) {
                if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1 ||
                     $.inArray("Files",event.originalEvent.dataTransfer.types) != -1 ||
                        RED.workspaces.isLocked()) {
                    event.preventDefault();
                }
            })
            .on("dragleave",function(event) {
                hideDropTarget();
            })
            .on("drop",function(event) {
                if (!RED.workspaces.isLocked()) {
                    try {
                        if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) {
                            var data = event.originalEvent.dataTransfer.getData("text/plain");
                            data = data.substring(data.indexOf('['),data.lastIndexOf(']')+1);
                            importNodes(data);
                        } else if ($.inArray("Files",event.originalEvent.dataTransfer.types) != -1) {
                            var files = event.originalEvent.dataTransfer.files;
                            if (files.length === 1) {
                                var file = files[0];
                                var reader = new FileReader();
                                reader.onload = (function(theFile) {
                                    return function(e) {
                                        importNodes(e.target.result);
                                    };
                                })(file);
                                reader.readAsText(file);
                            }
                        }
                    } catch(err) {
                        console.warn('Import failed: ', err)
                        // Ensure any errors throw above doesn't stop the drop target from
                        // being hidden.
                    }
                }
                hideDropTarget();
                event.preventDefault();
            });

        },
        import: showImportNodes,
        export: showExportNodes,
        copyText: copyText
    }
})();
;/**
 * 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.
 **/
RED.library = (function() {

    var loadLibraryBrowser;
    var saveLibraryBrowser;
    var libraryEditor;
    var activeLibrary;

    var _libraryLookup = '<div id="red-ui-library-dialog-load" class="hide">'+
        '<form class="form-horizontal">'+
            '<div class="red-ui-library-dialog-box" style="height: 400px; position:relative; ">'+
                '<div id="red-ui-library-dialog-load-panes">'+
                    '<div class="red-ui-panel" id="red-ui-library-dialog-load-browser"></div>'+
                    '<div class="red-ui-panel">'+
                        '<div id="red-ui-library-dialog-load-preview">'+
                            '<div class="red-ui-panel" id="red-ui-library-dialog-load-preview-text" style="position:relative; height: 50%; overflow-y: hidden;"></div>'+
                            '<div class="red-ui-panel" id="red-ui-library-dialog-load-preview-details">'+
                                '<table id="red-ui-library-dialog-load-preview-details-table" class="red-ui-info-table"></table>'+
                            '</div>'+
                        '</div>'+
                    '</div>'+
                '</div>'+
            '</div>'+
        '</form>'+
    '</div>'


    var _librarySave = '<div id="red-ui-library-dialog-save" class="hide">'+
        '<form class="form-horizontal">'+
        '<div class="red-ui-library-dialog-box" style="height: 400px; position:relative; ">'+
            '<div id="red-ui-library-dialog-save-browser"></div>'+
            '<div class="form-row">'+
                '<label data-i18n="clipboard.export.exportAs"></label><input id="red-ui-library-dialog-save-filename" type="text">'+
            '</div>'+
        '</div>'+
        '</form>'+
    '</div>'

    function saveToLibrary() {
        var elementPrefix = activeLibrary.elementPrefix || "node-input-";
        var name = $("#"+elementPrefix+"name").val().trim();
        if (name === "") {
            name = RED._("library.unnamedType",{type:activeLibrary.type});
        }
        var filename = $("#red-ui-library-dialog-save-filename").val().trim()
        var selectedPath = saveLibraryBrowser.getSelected();
        if (!selectedPath.children) {
            selectedPath = selectedPath.parent;
        }

        var queryArgs = [];
        var data = {};
        for (var i=0; i < activeLibrary.fields.length; i++) {
            var field = activeLibrary.fields[i];
            if (field === "name") {
                data.name = name;
            } else if (typeof(field) === 'object') {
                data[field.name] = field.get();
            } else {
                data[field] = $("#" + elementPrefix + field).val();
            }
        }
        data.text = activeLibrary.editor.getValue();
        var saveFlow = function() {
            $.ajax({
                url:"library/"+selectedPath.library+'/'+selectedPath.type+'/'+selectedPath.path + filename,
                type: "POST",
                data: JSON.stringify(data),
                contentType: "application/json; charset=utf-8"
            }).done(function(data,textStatus,xhr) {
                RED.notify(RED._("library.savedType", {type:activeLibrary.type}),"success");
            }).fail(function(xhr,textStatus,err) {
                if (xhr.status === 401) {
                    RED.notify(RED._("library.saveFailed",{message:RED._("user.notAuthorized")}),"error");
                } else {
                    RED.notify(RED._("library.saveFailed",{message:xhr.responseText}),"error");
                }
            });
        }
        if (selectedPath.children) {
            var exists = false;
            selectedPath.children.forEach(function(f) {
                if (f.label === filename) {
                    exists = true;
                }
            });
            if (exists) {
                $( "#red-ui-library-dialog-save" ).dialog("close");
                var notification = RED.notify(RED._("clipboard.export.exists",{file:RED.utils.sanitize(filename)}),{
                    type: "warning",
                    fixed: true,
                    buttons: [{
                        text: RED._("common.label.cancel"),
                        click: function() {
                            notification.hideNotification()
                            $( "#red-ui-library-dialog-save" ).dialog( "open" );
                        }
                    },{
                        text: RED._("clipboard.export.overwrite"),
                        click: function() {
                            notification.hideNotification()
                            saveFlow();
                        }
                    }]
                });
            } else {
                saveFlow();
            }
        } else {
            saveFlow();
        }
    }

    function loadLibraryFolder(library,type,root,done) {
        $.getJSON("library/"+library+"/"+type+"/"+root,function(data) {
            var items = data.map(function(d) {
                if (typeof d === "string") {
                    return {
                        library: library,
                        type: type,
                        icon: 'fa fa-folder',
                        label: d,
                        path: root+d+"/",
                        children: function(done, item) {
                            loadLibraryFolder(library,type,root+d+"/", function(children) {
                                item.children = children; // TODO: should this be done by treeList for us
                                done(children);
                            })
                        }
                    };
                } else {
                    return {
                        library: library,
                        type: type,
                        icon: 'fa fa-file-o',
                        label: d.fn,
                        path: root+d.fn,
                        props: d
                    };
                }
            });
            items.sort(function(A,B){
                if (A.children && !B.children) {
                    return -1;
                } else if (!A.children && B.children) {
                    return 1;
                } else {
                    return A.label.localeCompare(B.label);
                }
            });
            done(items);
        });
    }

    var validateExportFilenameTimeout;
    function validateExportFilename(filenameInput) {
        if (validateExportFilenameTimeout) {
            clearTimeout(validateExportFilenameTimeout);
        }
        validateExportFilenameTimeout = setTimeout(function() {
            var filename = filenameInput.val().trim();
            var valid = filename.length > 0 && !/[\/\\]/.test(filename);
            if (valid) {
                filenameInput.removeClass("input-error");
                $("#red-ui-library-dialog-save-button").button("enable");
            } else {
                filenameInput.addClass("input-error");
                $("#red-ui-library-dialog-save-button").button("disable");
            }
        },100);
    }

    function createUI(options) {
        var libraryData = {};
        var elementPrefix = options.elementPrefix || "node-input-";

        // Orion editor has set/getText
        // ACE editor has set/getValue
        // normalise to set/getValue
        if (options.editor.setText) {
            // Orion doesn't like having pos passed in, so proxy the call to drop it
            options.editor.setValue = function(text,pos) {
                options.editor.setText.call(options.editor,text);
            }
        }
        if (options.editor.getText) {
            options.editor.getValue = options.editor.getText;
        }

        // Add the library button to the name <input> in the edit dialog
        $('#'+elementPrefix+"name").css("width","calc(100% - 52px)").after(
            '<div style="margin-left:5px; display: inline-block;position: relative;">'+
            '<a id="node-input-'+options.type+'-lookup" class="red-ui-button"><i class="fa fa-book"></i> <i class="fa fa-caret-down"></i></a>'+
            '</div>'

            // '<ul class="red-ui-menu-dropdown pull-right" role="menu">'+
            // '<li><a id="node-input-'+options.type+'-menu-open-library" tabindex="-1" href="#">'+RED._("library.openLibrary")+'</a></li>'+
            // '<li><a id="node-input-'+options.type+'-menu-save-library" tabindex="-1" href="#">'+RED._("library.saveToLibrary")+'</a></li>'+
            // '</ul></div>'
        );
        RED.menu.init({id:'node-input-'+options.type+'-lookup', options: [
            { id:'node-input-'+options.type+'-menu-open-library',
                label: RED._("library.openLibrary"),
                onselect: function() {
                    var editorOpts = {
                        id: 'red-ui-library-dialog-load-preview-text',
                        mode: options.mode,
                        readOnly: true,
                        highlightActiveLine: false,
                        highlightGutterLine: false,
                        contextmenu: false
                    }
                    libraryEditor = RED.editor.createEditor(editorOpts); //use red.editor
                    if(libraryEditor.isACE) {
                        if (options.mode) {
                            libraryEditor.getSession().setMode(options.mode);
                        }
                        libraryEditor.setOptions({
                            readOnly: true,
                            highlightActiveLine: false,
                            highlightGutterLine: false
                        });
                        libraryEditor.renderer.$cursorLayer.element.style.opacity=0;
                        libraryEditor.$blockScrolling = Infinity;
                    }

                    activeLibrary = options;
                    var listing = [];
                    var libraries = RED.settings.libraries || [];
                    libraries.forEach(function(lib) {
                        if (lib.types && lib.types.indexOf(options.url) === -1) {
                            return;
                        }
                        let icon = 'fa fa-hdd-o';
                        if (lib.icon) {
                            const fullIcon = RED.utils.separateIconPath(lib.icon);
                            icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file;
                        }
                        listing.push({
                            library: lib.id,
                            type: options.url,
                            icon,
                            label: RED._(lib.label||lib.id),
                            path: "",
                            expanded: true,
                            writable: false,
                            children: [{
                                library: lib.id,
                                type: options.url,
                                icon: 'fa fa-cube',
                                label: options.type,
                                path: "",
                                expanded: false,
                                children: function(done, item) {
                                    loadLibraryFolder(lib.id, options.url, "", function(children) {
                                        item.children = children;
                                        done(children);
                                    })
                                }
                            }]
                        })
                    });
                    loadLibraryBrowser.data(listing);
                    setTimeout(function() {
                        loadLibraryBrowser.select(listing[0].children[0]);
                    },200);


                    var dialogHeight = 400;
                    var winHeight = $(window).height();
                    if (winHeight < 570) {
                        dialogHeight = 400 - (570 - winHeight);
                    }
                    $("#red-ui-library-dialog-load .red-ui-library-dialog-box").height(dialogHeight);

                    $( "#red-ui-library-dialog-load" ).dialog("option","title",RED._("library.typeLibrary", {type:options.type})).dialog( "open" );
                }
            },
            { id:'node-input-'+options.type+'-menu-save-library',
                label: RED._("library.saveToLibrary"),
                onselect: function() {
                    activeLibrary = options;
                    //var found = false;
                    var name = $("#"+elementPrefix+"name").val().replace(/(^\s*)|(\s*$)/g,"");
                    var filename = name.replace(/[^\w-]/g,"-");
                    if (filename === "") {
                        filename = "unnamed-"+options.type;
                    }
                    $("#red-ui-library-dialog-save-filename").attr("value",filename+"."+(options.ext||"txt"));

                    var listing = [];
                    var libraries = RED.settings.libraries || [];
                    libraries.forEach(function(lib) {
                        if (lib.types && lib.types.indexOf(options.url) === -1) {
                            return;
                        }
                        let icon = 'fa fa-hdd-o';
                        if (lib.icon) {
                            const fullIcon = RED.utils.separateIconPath(lib.icon);
                            icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file;
                        }
                        listing.push({
                            library: lib.id,
                            type: options.url,
                            icon,
                            label: RED._(lib.label||lib.id),
                            path: "",
                            expanded: true,
                            writable: false,
                            children: [{
                                library: lib.id,
                                type: options.url,
                                icon: 'fa fa-cube',
                                label: options.type,
                                path: "",
                                expanded: false,
                                children: function(done, item) {
                                    loadLibraryFolder(lib.id, options.url, "", function(children) {
                                        item.children = children;
                                        done(children);
                                    })
                                }
                            }]
                        })
                    });
                    saveLibraryBrowser.data(listing);
                    setTimeout(function() {
                        saveLibraryBrowser.select(listing[0].children[0]);
                    },200);

                    var dialogHeight = 400;
                    var winHeight = $(window).height();
                    if (winHeight < 570) {
                        dialogHeight = 400 - (570 - winHeight);
                    }
                    $("#red-ui-library-dialog-save .red-ui-library-dialog-box").height(dialogHeight);


                    $( "#red-ui-library-dialog-save" ).dialog( "open" );
                }
            }
        ]})
    }

    function exportFlow() {
        console.warn("Deprecated call to RED.library.export");
    }

    var menuOptionMenu;
    function createBrowser(options) {
        var panes = $('<div class="red-ui-library-browser"></div>').appendTo(options.container);
        var dirList = $("<div>").css({width: "100%", height: "100%"}).appendTo(panes)
            .treeList({}).on('treelistselect', function(event, item) {
                if (options.onselect) {
                    options.onselect(item);
                }
            }).on('treelistconfirm', function(event, item) {
                if (options.onconfirm) {
                    options.onconfirm(item);
                }
            });
        var itemTools = null;
        if (options.folderTools) {
            dirList.on('treelistselect', function(event, item) {
                if (item.writable !== false && item.treeList) {
                    if (itemTools) {
                        itemTools.remove();
                    }
                    itemTools = $("<div>").css({position: "absolute",bottom:"6px",right:"8px"});
                    var menuButton = $('<button class="red-ui-button red-ui-button-small" type="button"><i class="fa fa-ellipsis-h"></i></button>')
                        .on("click", function(evt) {
                            evt.preventDefault();
                            evt.stopPropagation();
                            var elementPos = menuButton.offset();

                            var menuOptionMenu
                                = RED.menu.init({id:"red-ui-library-browser-menu",
                                      options: [
                                          {id:"red-ui-library-browser-menu-addFolder",label:RED._("library.newFolder"), onselect: function() {
                                              var defaultFolderName = "new-folder";
                                              var defaultFolderNameMatches = {};

                                              var selected = dirList.treeList('selected');
                                              if (!selected.children) {
                                                  selected = selected.parent;
                                              }
                                              var complete = function() {
                                                  selected.children.forEach(function(c) {
                                                      if (/^new-folder/.test(c.label)) {
                                                          defaultFolderNameMatches[c.label] = true
                                                      }
                                                  });
                                                  var folderIndex = 2;
                                                  while(defaultFolderNameMatches[defaultFolderName]) {
                                                      defaultFolderName = "new-folder-"+(folderIndex++)
                                                  }

                                                  selected.treeList.expand();
                                                  var input = $('<input type="text" class="red-ui-treeList-input">').val(defaultFolderName);
                                                  var newItem = {
                                                      icon: "fa fa-folder-o",
                                                      children:[],
                                                      path: selected.path,
                                                      element: input
                                                  }
                                                  var confirmAdd = function() {
                                                      var val = input.val().trim();
                                                      if (val === "") {
                                                          cancelAdd();
                                                          return;
                                                      } else {
                                                          for (var i=0;i<selected.children.length;i++) {
                                                              if (selected.children[i].label === val) {
                                                                  cancelAdd();
                                                                  return;
                                                              }
                                                          }
                                                      }
                                                      newItem.treeList.remove();
                                                      var finalItem = {
                                                          library: selected.library,
                                                          type: selected.type,
                                                          icon: "fa fa-folder",
                                                          children:[],
                                                          label: val,
                                                          path: newItem.path+val+"/"
                                                      }
                                                      selected.treeList.addChild(finalItem,true);
                                                  }
                                                  var cancelAdd = function() {
                                                      newItem.treeList.remove();
                                                  }
                                                  input.on('keydown', function(evt) {
                                                      evt.stopPropagation();
                                                      if (evt.keyCode === 13) {
                                                          confirmAdd();
                                                      } else if (evt.keyCode === 27) {
                                                          cancelAdd();
                                                      }
                                                  })
                                                  input.on("blur", function() {
                                                      confirmAdd();
                                                  })
                                                  selected.treeList.addChild(newItem);
                                                  setTimeout(function() {
                                                      input.trigger("focus");
                                                      input.select();
                                                  },400);
                                              }
                                              selected.treeList.expand(complete);

                                          } },
                                          // null,
                                          // {id:"red-ui-library-browser-menu-rename",label:"Rename", onselect: function() {} },
                                          // {id:"red-ui-library-browser-menu-delete",label:"Delete", onselect: function() {} }
                                      ]
                                                }).on('mouseleave', function(){ $(this).remove(); dirList.focus() })
                                .on('mouseup', function() { var self = $(this);self.hide(); dirList.focus(); setTimeout(function() { self.remove() },100)})
                                .appendTo("body");
                            menuOptionMenu.css({
                                position: "absolute",
                                top: elementPos.top+"px",
                                left: (elementPos.left - menuOptionMenu.width() + 20)+"px"
                            }).show();

                        }).appendTo(itemTools);
                    
                    itemTools.appendTo(item.treeList.label);
                }
            });
        }

        return {
            select: function(item) {
                dirList.treeList('select',item);
            },
            getSelected: function() {
                return dirList.treeList('selected');
            },
            focus: function() {
                dirList.focus();
            },
            data: function(content,selectFirst) {
                dirList.treeList('data',content);
                if (selectFirst) {
                    setTimeout(function() {
                        dirList.treeList('select',content[0]);
                    },100);
                }
            }
        }
    }

    // var libraryPlugins = {};
    //
    // function showLibraryDetailsDialog(container, lib, done) {
    //     var dialog = $('<div>').addClass("red-ui-projects-dialog-list-dialog").hide().appendTo(container);
    //     $('<div>').addClass("red-ui-projects-dialog-list-dialog-header").text(lib?"Edit library source":"Add library source").appendTo(dialog);
    //     var formRow = $('<div class="red-ui-settings-row"></div>').appendTo(dialog);
    //     $('<label>').text("Type").appendTo(formRow);
    //     var typeSelect = $('<select>').appendTo(formRow);
    //     for (var type in libraryPlugins) {
    //         if (libraryPlugins.hasOwnProperty(type)) {
    //             $('<option>').attr('value',type).attr('selected',(lib && lib.type === type)?true:null).text(libraryPlugins[type].name).appendTo(typeSelect);
    //         }
    //     }
    //     var dialogBody = $("<div>").addClass("red-ui-settings-section").appendTo(dialog);
    //     var libraryFields = {};
    //     var fieldsModified = {};
    //     function validateFields() {
    //         var validForm = true;
    //         for (var p in libraryFields) {
    //             if (libraryFields.hasOwnProperty(p)) {
    //                 var v = libraryFields[p].input.val().trim();
    //                 if (v === "") {
    //                     validForm = false;
    //                     if (libraryFields[p].modified) {
    //                         libraryFields[p].input.addClass("input-error");
    //                     }
    //                 } else {
    //                     libraryFields[p].input.removeClass("input-error");
    //                 }
    //             }
    //         }
    //         okayButton.attr("disabled",validForm?null:"disabled");
    //     }
    //     typeSelect.on("change", function(evt) {
    //         dialogBody.empty();
    //         libraryFields = {};
    //         fieldsModified = {};
    //         var libDef = libraryPlugins[$(this).val()];
    //         var defaultIcon = lib?lib.icon:(libDef.icon || "font-awesome/fa-image");
    //         formRow = $('<div class="red-ui-settings-row"></div>').appendTo(dialogBody);
    //         $('<label>').text(RED._("editor.settingIcon")).appendTo(formRow);
    //         libraryFields['icon'] = {input: $('<input type="hidden">').val(defaultIcon) };
    //         var iconButton = $('<button type="button" class="red-ui-button"></button>').appendTo(formRow);
    //         iconButton.on("click", function(evt) {
    //             evt.preventDefault();
    //             var icon = libraryFields['icon'].input.val() || "";
    //             var iconPath = (icon ? RED.utils.separateIconPath(icon) : {});
    //             RED.editor.iconPicker.show(iconButton, null, iconPath, true, function (newIcon) {
    //                 iconButton.empty();
    //                 var path = newIcon || "";
    //                 var newPath = RED.utils.separateIconPath(path);
    //                 if (newPath) {
    //                     $('<i class="fa"></i>').addClass(newPath.file).appendTo(iconButton);
    //                 }
    //                 libraryFields['icon'].input.val(path);
    //             });
    //         })
    //         var newPath = RED.utils.separateIconPath(defaultIcon);
    //         $('<i class="fa '+newPath.file+'"></i>').appendTo(iconButton);
    //
    //         var libProps = libDef.defaults;
    //         var libPropKeys = Object.keys(libProps).map(function(p) { return {id: p, def: libProps[p]}});
    //         libPropKeys.unshift({id: "label", def: {value:""}})
    //
    //         libPropKeys.forEach(function(prop) {
    //             var p = prop.id;
    //             var def = prop.def;
    //             formRow = $('<div class="red-ui-settings-row"></div>').appendTo(dialogBody);
    //             var label = libDef._(def.label || "label."+p,{defaultValue: p});
    //             if (label === p) {
    //                 label = libDef._("editor:common.label."+p,{defaultValue:p});
    //             }
    //             $('<label>').text(label).appendTo(formRow);
    //             libraryFields[p] = {
    //                 input: $('<input type="text">').val(lib?(lib[p]||lib.config[p]):def.value).appendTo(formRow),
    //                 modified: false
    //             }
    //             if (def.type === "password") {
    //                 libraryFields[p].input.attr("type","password").typedInput({type:"cred"})
    //             }
    //
    //             libraryFields[p].input.on("change paste keyup", function(evt) {
    //                 if (!evt.key || evt.key.length === 1) {
    //                     libraryFields[p].modified = true;
    //                 }
    //                 validateFields();
    //             })
    //             var desc = libDef._("desc."+p, {defaultValue: ""});
    //             if (desc) {
    //                 $('<label class="red-ui-projects-edit-form-sublabel"></label>').append($('<small>').text(desc)).appendTo(formRow);
    //             }
    //         });
    //         validateFields();
    //     })
    //
    //     var dialogButtons = $('<span class="button-row" style="position: relative; float: right; margin: 10px;"></span>').appendTo(dialog);
    //     var cancelButton = $('<button class="red-ui-button"></button>').text(RED._("common.label.cancel")).appendTo(dialogButtons).on("click", function(evt) {
    //         evt.preventDefault();
    //         done(false);
    //     })
    //     var okayButton = $('<button class="red-ui-button"></button>').text(lib?"Update library":"Add library").appendTo(dialogButtons).on("click", function(evt) {
    //         evt.preventDefault();
    //         var item;
    //         if (!lib) {
    //             item = {
    //                 id: libraryFields['label'].input.val().trim().toLowerCase().replace(/( |[^a-z0-9])/g,"-"),
    //                 user: true,
    //                 type: typeSelect.val(),
    //                 config: {}
    //             }
    //         } else {
    //             item = lib;
    //         }
    //
    //         item.label = libraryFields['label'].input.val().trim();
    //         item.icon = libraryFields['icon'].input.val();
    //
    //         for (var p in libraryFields) {
    //             if (libraryFields.hasOwnProperty(p) && p !== 'label') {
    //                 item.config[p] = libraryFields[p].input.val().trim();
    //             }
    //         }
    //         done(item);
    //     });
    //
    //     typeSelect.trigger("change");
    //     if (lib) {
    //         typeSelect.attr('disabled',true);
    //     }
    //
    //     dialog.slideDown(200);
    // }
    //
    // function createSettingsPane() {
    //     var pane = $('<div id="red-ui-settings-tab-library-manager"></div>');
    //     var toolbar = $('<div>').css("text-align","right").appendTo(pane);
    //     var addButton = $('<button class="red-ui-button"><i class="fa fa-plus"></i> Add library</button>').appendTo(toolbar);
    //
    //     var addingLibrary = false;
    //
    //     var libraryList = $("<ol>").css({
    //         position: "absolute",
    //         left: "10px",
    //         right: "10px",
    //         top: "50px",
    //         bottom: "10px"
    //     }).appendTo(pane).editableList({
    //         addButton: false,
    //         addItem: function(row,index,itemData) {
    //             if (itemData.id) {
    //                 row.addClass("red-ui-settings-tab-library-entry");
    //                 var iconCell = $("<span>").appendTo(row);
    //                 if (itemData.icon) {
    //                     var iconPath = RED.utils.separateIconPath(itemData.icon);
    //                     if (iconPath) {
    //                         $("<i>").addClass("fa "+iconPath.file).appendTo(iconCell);
    //                     }
    //                 }
    //                 $("<span>").text(RED._(itemData.label)).appendTo(row);
    //                 $("<span>").text(RED._(itemData.type)).appendTo(row);
    //                 $('<button class="red-ui-button red-ui-button-small"></button>').text(RED._("sidebar.project.projectSettings.edit")).appendTo(
    //                     $('<span>').appendTo(row)
    //                 ).on("click", function(evt) {
    //                     if (addingLibrary) {
    //                         return;
    //                     }
    //                     evt.preventDefault();
    //                     addingLibrary = true;
    //                     row.empty();
    //                     row.removeClass("red-ui-settings-tab-library-entry");
    //                     showLibraryDetailsDialog(row,itemData,function(newItem) {
    //                         var itemIndex = libraryList.editableList("indexOf", itemData);
    //                         libraryList.editableList("removeItem", itemData);
    //                         if (newItem) {
    //                             libraryList.editableList("insertItemAt", newItem, itemIndex);
    //                         } else {
    //                             libraryList.editableList("insertItemAt", itemData,itemIndex);
    //                         }
    //                         addingLibrary = false;
    //
    //                     })
    //                 })
    //
    //             } else {
    //                 showLibraryDetailsDialog(row,null,function(newItem) {
    //                     libraryList.editableList("removeItem", itemData);
    //                     if (newItem) {
    //                         libraryList.editableList("addItem", newItem);
    //                     }
    //                     addingLibrary = false;
    //                 })
    //
    //             }
    //         }
    //     });
    //
    //     addButton.on('click', function(evt) {
    //         evt.preventDefault();
    //         if (!addingLibrary) {
    //             addingLibrary = true;
    //             libraryList.editableList("addItem",{user:true});
    //         }
    //     })
    //     var libraries = RED.settings.libraries || [];
    //     libraries.forEach(function(library) {
    //         if (library.user) {
    //             libraryList.editableList("addItem",library)
    //         }
    //     })
    //
    //     return pane;
    // }
    //
    //
    return {
        init: function() {
            // RED.events.on("registry:plugin-added", function(id) {
            //     var plugin = RED.plugins.getPlugin(id);
            //     if (plugin.type === "node-red-library-source") {
            //         libraryPlugins[id] = plugin;
            //     }
            // });
            //
            // RED.userSettings.add({
            //     id:'library-manager',
            //     title: "NLS: Libraries",
            //     get: createSettingsPane,
            //     close: function() {}
            // });
            $(_librarySave).appendTo("#red-ui-editor").i18n();
            $(_libraryLookup).appendTo("#red-ui-editor").i18n();

            $( "#red-ui-library-dialog-save" ).dialog({
                title: RED._("library.saveToLibrary"),
                modal: true,
                autoOpen: false,
                width: 800,
                resizable: false,
                open: function( event, ui ) { RED.keyboard.disable() },
                close: function( event, ui ) { RED.keyboard.enable() },
                classes: {
                    "ui-dialog": "red-ui-editor-dialog",
                    "ui-dialog-titlebar-close": "hide",
                    "ui-widget-overlay": "red-ui-editor-dialog"
                },
                buttons: [
                    {
                        text: RED._("common.label.cancel"),
                        click: function() {
                            $( this ).dialog( "close" );
                        }
                    },
                    {
                        id: "red-ui-library-dialog-save-button",
                        text: RED._("common.label.save"),
                        class: "primary",
                        click: function() {
                            saveToLibrary(false);
                            $( this ).dialog( "close" );
                        }
                    }
                ]
            });

            saveLibraryBrowser = RED.library.createBrowser({
                container: $("#red-ui-library-dialog-save-browser"),
                folderTools: true,
                onselect: function(item) {
                    if (item.label) {
                        if (!item.children) {
                            $("#red-ui-library-dialog-save-filename").val(item.label);
                            item = item.parent;
                        }
                        if (item.writable === false) {
                            $("#red-ui-library-dialog-save-button").button("disable");
                        } else {
                            $("#red-ui-library-dialog-save-button").button("enable");
                        }
                    }
                }
            });
            $("#red-ui-library-dialog-save-filename").on("keyup", function() { validateExportFilename($(this))});
            $("#red-ui-library-dialog-save-filename").on('paste',function() { var input = $(this); setTimeout(function() { validateExportFilename(input)},10)});

            $( "#red-ui-library-dialog-load" ).dialog({
                modal: true,
                autoOpen: false,
                width: 800,
                resizable: false,
                classes: {
                    "ui-dialog": "red-ui-editor-dialog",
                    "ui-dialog-titlebar-close": "hide",
                    "ui-widget-overlay": "red-ui-editor-dialog"
                },
                buttons: [
                    {
                        text: RED._("common.label.cancel"),
                        click: function() {
                            $( this ).dialog( "close" );
                        }
                    },
                    {
                        text: RED._("common.label.load"),
                        class: "primary",
                        click: function () {
                            if (selectedLibraryItem) {
                                var elementPrefix = activeLibrary.elementPrefix || "node-input-";
                                for (var i = 0; i < activeLibrary.fields.length; i++) {
                                    var field = activeLibrary.fields[i];
                                    if (typeof(field) === 'object') {
                                        var val = selectedLibraryItem[field.name];
                                        field.set(val);
                                    }
                                    else {
                                        $("#"+elementPrefix+field).val(selectedLibraryItem[field]);
                                    }
                                }
                                activeLibrary.editor.setValue(libraryEditor.getValue(), -1);
                            }
                            $( this ).dialog( "close" );
                        }
                    }
                ],
                open: function(e) {
                    RED.keyboard.disable();
                    $(this).parent().find(".ui-dialog-titlebar-close").hide();
                    libraryEditor.resize();
                },
                close: function(e) {
                    RED.keyboard.enable();
                    if (libraryEditor) {
                        libraryEditor.destroy();
                        libraryEditor = null;
                    }
                }
            });
            loadLibraryBrowser = RED.library.createBrowser({
                container: $("#red-ui-library-dialog-load-browser"),
                onselect: function(file) {
                    var table = $("#red-ui-library-dialog-load-preview-details-table").empty();
                    selectedLibraryItem = file.props;
                    if (file && file.label && !file.children) {
                        $.get("library/"+file.library+"/"+file.type+"/"+file.path, function(data) {
                            //TODO: nls + sanitize
                            var propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("library.type")+'</td><td></td></tr>').appendTo(table);
                            $(propRow.children()[1]).text(activeLibrary.type);
                            if (file.props.hasOwnProperty('name')) {
                                propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("library.name")+'</td><td>'+file.props.name+'</td></tr>').appendTo(table);
                                $(propRow.children()[1]).text(file.props.name);
                            }
                            for (var p in file.props) {
                                if (file.props.hasOwnProperty(p) && p !== 'name' && p !== 'fn') {
                                    propRow = $('<tr class="red-ui-help-info-row"><td></td><td></td></tr>').appendTo(table);
                                    $(propRow.children()[0]).text(p);
                                    RED.utils.createObjectElement(file.props[p]).appendTo(propRow.children()[1]);
                                }
                            }
                            libraryEditor.setValue(data,-1);
                        });
                    } else {
                        libraryEditor.setValue("",-1);
                    }
                }
            });
            RED.panels.create({
                container:$("#red-ui-library-dialog-load-panes"),
                dir: "horizontal",
                resize: function() {
                    libraryEditor.resize();
                }
            });
            RED.panels.create({
                container:$("#red-ui-library-dialog-load-preview"),
                dir: "vertical",
                resize: function() {
                    libraryEditor.resize();
                }
            });
        },
        create: createUI,
        createBrowser:createBrowser,
        export: exportFlow,
        loadLibraryFolder: loadLibraryFolder
    }
})();
;/**
 * 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.
 **/
RED.notifications = (function() {

    /*
    If RED.notifications.hide is set to true, all notifications will be hidden.
    This is to help with UI testing in certain cases and not intended for the
    end-user.

    // Example usage for a modal dialog with buttons
    var myNotification = RED.notify("This is the message to display",{
        modal: true,
        fixed: true,
        type: 'warning', // 'compact', 'success', 'warning', 'error'
        buttons: [
            {
                text: "cancel",
                click: function(e) {
                    myNotification.close();
                }
            },
            {
                text: "okay",
                class:"primary",
                click: function(e) {
                    myNotification.close();
                }
            }
        ]
    });
    */

    var persistentNotifications = {};

    var shade = (function() {
        var shadeUsers = 0;
        return {
            show: function() {
                shadeUsers++;
                $("#red-ui-full-shade").show();
            },
            hide: function() {
                shadeUsers--;
                if (shadeUsers === 0) {
                    $("#red-ui-full-shade").hide();
                }
            }
        }
    })();

    var currentNotifications = [];
    var c = 0;
    function notify(msg,type,fixed,timeout) {
        var options = {};
        if (type !== null && typeof type === 'object') {
            options = type;
            fixed = options.fixed;
            timeout = options.timeout;
            type = options.type;
        } else {
            options.type = type;
            options.fixed = fixed;
            options.timeout = options.timeout;
        }

        if (options.id && persistentNotifications.hasOwnProperty(options.id)) {
            persistentNotifications[options.id].update(msg,options);
            return persistentNotifications[options.id];
        }

        if (options.modal) {
            shade.show();
        }

        if (currentNotifications.length > 4) {
            var ll = currentNotifications.length;
            for (var i = 0;ll > 4 && i<currentNotifications.length;i+=1) {
                var notifiction = currentNotifications[i];
                if (!notifiction.fixed) {
                    window.clearTimeout(notifiction.timeoutid);
                    notifiction.close();
                    ll -= 1;
                }
            }
        }
        var n = document.createElement("div");
        n.id="red-ui-notification-"+c;
        n.className = "red-ui-notification";
        n.options = options;

        n.fixed = fixed;
        if (type) {
            n.className = "red-ui-notification red-ui-notification-"+type;
        }
        if (options.width) {
            var parentWidth = $("#red-ui-notifications").width();
            if (options.width > parentWidth) {
                var margin = -(options.width-parentWidth)/2;
                $(n).css({
                    width: options.width+"px",
                    marginLeft: margin+"px"
                })
            }
        }
        n.style.display = "none";
        if (typeof msg === "string") {
            if (!/<p>/i.test(msg)) {
                msg = "<p>"+msg+"</p>";
            }
            n.innerHTML = msg;
        } else {
            $(n).append(msg);
        }
        if (options.buttons) {
            var buttonSet = $('<div class="ui-dialog-buttonset"></div>').appendTo(n)
            options.buttons.forEach(function(buttonDef) {
                var b = $('<button>').html(buttonDef.text).on("click", buttonDef.click).appendTo(buttonSet);
                if (buttonDef.id) {
                    b.attr('id',buttonDef.id);
                }
                if (buttonDef.class) {
                    b.addClass(buttonDef.class);
                }
            })
        }

        $("#red-ui-notifications").append(n);
        if (!RED.notifications.hide) {
            $(n).slideDown(300);
        }
        n.close = (function() {
            var nn = n;
            return function() {
                if (nn.closed) {
                    return;
                }
                nn.closed = true;
                currentNotifications.splice(currentNotifications.indexOf(nn),1);
                if (options.id) {
                    delete persistentNotifications[options.id];
                    if (Object.keys(persistentNotifications).length === 0) {
                        notificationButtonWrapper.hide();
                    }
                }
                if (!RED.notifications.hide) {
                    $(nn).slideUp(300, function() {
                        nn.parentNode.removeChild(nn);
                    });
                } else {
                    nn.parentNode.removeChild(nn);
                }
                if (nn.options.modal) {
                    shade.hide();
                }
            };
        })();
        n.hideNotification = (function() {
            var nn = n;
            return function() {
                if (nn.closed) {
                    return
                }
                nn.hidden = true;
                if (!RED.notifications.hide) {
                    $(nn).slideUp(300);
                }
            }
        })();
        n.showNotification = (function() {
            var nn = n;
            return function() {
                if (nn.closed || !nn.hidden) {
                    return
                }
                nn.hidden = false;
                if (!RED.notifications.hide) {
                    $(nn).slideDown(300);
                }
            }
        })();

        n.update = (function() {
            var nn = n;
            return function(msg,newOptions) {
                if (typeof msg === "string") {
                    if (!/<p>/i.test(msg)) {
                        msg = "<p>"+msg+"</p>";
                    }
                    nn.innerHTML = msg;
                } else {
                    $(nn).empty().append(msg);
                }
                var newTimeout;
                if (typeof newOptions === 'number') {
                    newTimeout = newOptions;
                    nn.options.timeout = newTimeout;
                } else if (newOptions !== undefined) {

                    if (!options.modal && newOptions.modal) {
                        nn.options.modal = true;
                        shade.show();
                    } else if (options.modal && newOptions.modal === false) {
                        nn.options.modal = false;
                        shade.hide();
                    }

                    var newType = newOptions.hasOwnProperty('type')?newOptions.type:type;
                    if (newType) {
                        n.className = "red-ui-notification red-ui-notification-"+newType;
                    }
                    newTimeout = newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout
                    if (!fixed || newOptions.fixed === false) {
                        newTimeout = newTimeout || 5000
                    }
                    if (newOptions.buttons) {
                        var buttonSet = $('<div class="ui-dialog-buttonset"></div>').appendTo(nn)
                        newOptions.buttons.forEach(function(buttonDef) {
                            var b = $('<button>').text(buttonDef.text).on("click", buttonDef.click).appendTo(buttonSet);
                            if (buttonDef.id) {
                                b.attr('id',buttonDef.id);
                            }
                            if (buttonDef.class) {
                                b.addClass(buttonDef.class);
                            }
                        })
                    }
                }
                $(nn).off("click.red-ui-notification-close");
                if (newTimeout !== undefined && newTimeout > 0) {
                    window.clearTimeout(nn.timeoutid);
                    nn.timeoutid = window.setTimeout(nn.close,newTimeout);
                    setTimeout(function() {
                        $(nn).on("click.red-ui-notification-close", function() {
                            nn.close();
                            window.clearTimeout(nn.timeoutid);
                        });
                    },50);
                } else {
                    window.clearTimeout(nn.timeoutid);
                }
                if (nn.hidden) {
                    nn.showNotification();
                } else if (!newOptions || !newOptions.silent){
                    $(nn).addClass("red-ui-notification-shake-horizontal");
                    setTimeout(function() {
                        $(nn).removeClass("red-ui-notification-shake-horizontal");
                    },300);
                }

            }
        })();

        if (!fixed) {
            $(n).on("click.red-ui-notification-close", (function() {
                var nn = n;
                return function() {
                    nn.close();
                    window.clearTimeout(nn.timeoutid);
                };
            })());
            n.timeoutid = window.setTimeout(n.close,timeout||5000);
        } else if (timeout) {
            $(n).on("click.red-ui-notification-close", (function() {
                var nn = n;
                return function() {
                    nn.hideNotification();
                    window.clearTimeout(nn.timeoutid);
                };
            })());
            n.timeoutid = window.setTimeout(n.hideNotification,timeout||5000);
        }
        currentNotifications.push(n);
        if (options.id) {
            persistentNotifications[options.id] = n;
            if (options.fixed) {
                notificationButtonWrapper.show();
            }
        }
        c+=1;
        return n;
    }

    RED.notify = notify;


    function hidePersistent() {
        for(var i in persistentNotifications) {
            if (persistentNotifications.hasOwnProperty(i)) {
                persistentNotifications[i].hideNotification();
            }
        }
    }
    function showPersistent() {
        for(var i in persistentNotifications) {
            if (persistentNotifications.hasOwnProperty(i)) {
                persistentNotifications[i].showNotification();
            }
        }
    }

    var notificationButtonWrapper;

    return {
        init: function() {
            $('<div id="red-ui-notifications"></div>').appendTo("#red-ui-editor");

            notificationButtonWrapper = $('<li></li>').prependTo(".red-ui-header-toolbar").hide();
            $('<a class="button" href="#"><i class="fa fa-warning"></i></a>')
                .appendTo(notificationButtonWrapper)
                .on("click", function() {
                    showPersistent();
                })
        },
        notify: notify
    }
})();
;/**
 * 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.
 **/
RED.search = (function() {

    var disabled = false;
    var dialog = null;
    var searchInput;
    var searchResults;
    var selected = -1;
    var visible = false;

    var searchHistory = [];
    var index = {};
    var currentResults = [];
    var activeResults = [];
    var currentIndex = 0;
    var previousActiveElement;

    function indexProperty(node,label,property) {
        if (typeof property === 'string' || typeof property === 'number') {
            property = (""+property).toLowerCase();
            index[property] = index[property] || {};
            index[property][node.id] = {node:node,label:label};
        } else if (Array.isArray(property)) {
            property.forEach(function(prop) {
                indexProperty(node,label,prop);
            })
        } else if (typeof property === 'object') {
            for (var prop in property) {
                if (property.hasOwnProperty(prop)) {
                    indexProperty(node,label,property[prop])
                }
            }
        }
    }
    function indexNode(n) {
        var l = RED.utils.getNodeLabel(n);
        if (l) {
            l = (""+l).toLowerCase();
            index[l] = index[l] || {};
            index[l][n.id] = {node:n,label:l}
        }
        l = l||n.label||n.name||n.id||"";

        var properties = ['id','type','name','label','info'];
        const node_def = n && n._def;
        if (node_def) {
            if (node_def.defaults) {
                properties = properties.concat(Object.keys(node_def.defaults));
            }
            if (n.type !== "group" && node_def.paletteLabel && node_def.paletteLabel !== node_def.type) {
                try {
                    const label = ("" + (typeof node_def.paletteLabel === "function" ? node_def.paletteLabel.call(node_def) : node_def.paletteLabel)).toLowerCase();
                    if(label && label !== (""+node_def.type).toLowerCase()) {
                        indexProperty(n, l, label);
                    }
                } catch(err) {
                    console.warn(`error indexing ${l}`, err);
                }
            }
        }
        for (var i=0;i<properties.length;i++) {
            if (n.hasOwnProperty(properties[i])) {
                if (n.type === "group" && properties[i] === "nodes") {
                    continue;
                }
                indexProperty(n, l, n[properties[i]]);
            }
        }
    }

    function extractFlag(val, flagName, flags) {
        // is:XYZ

        var regEx = new RegExp("(?:^| )is:"+flagName+"(?: |$)");
        var m = regEx.exec(val);
        if (m) {
            val = val.replace(regEx," ").trim();
            flags[flagName] = true;
        }
        return val;
    }

    function extractValue(val, flagName, flags) {
        // flagName:XYZ
        var regEx = new RegExp("(?:^| )"+flagName+":([^ ]+)(?: |$)");
        var m
        while(!!(m = regEx.exec(val))) {
            val = val.replace(regEx," ").trim();
            flags[flagName] = flags[flagName] || [];
            flags[flagName].push(m[1]);
        }
        return val;
    }

    function extractType(val, flags) {
        // extracts:  type:XYZ  &  type:"X Y Z"
        const regEx = /(?:type):\s*(?:"([^"]+)"|([^" ]+))/;
        let m
        while ((m = regEx.exec(val)) !== null) {
            // avoid infinite loops with zero-width matches
            if (m.index === regEx.lastIndex) {
                regEx.lastIndex++;
            }
            val = val.replace(m[0]," ").trim()
            const flag = m[2] || m[1] // quoted entries in capture group 1, unquoted in capture group 2
            flags.type = flags.type || [];
            flags.type.push(flag);
        }
        return val;
    }

    function search(val) {
        const results = [];
        const flags = {};
        val = extractFlag(val,"invalid",flags);
        val = extractFlag(val,"unused",flags);
        val = extractFlag(val,"config",flags);
        val = extractFlag(val,"subflow",flags);
        val = extractFlag(val,"hidden",flags);
        val = extractFlag(val,"modified",flags);
        val = extractValue(val,"flow",flags);// flow:current or flow:<flow-id>
        val = extractValue(val,"uses",flags);// uses:<node-id>
        val = extractType(val,flags);// type:<node-type>
        val = val.trim();
        const hasFlags = Object.keys(flags).length > 0;
        const hasTypeFilter = flags.type && flags.type.length > 0
        if (flags.flow && flags.flow.indexOf("current") >= 0) {
            let idx = flags.flow.indexOf("current");
            flags.flow[idx] = RED.workspaces.active();//convert 'current' to active flow ID
        }
        if (flags.flow && flags.flow.length) {
            flags.flow = [ ...new Set(flags.flow) ]; //deduplicate
        }
        if (val.length > 0 || hasFlags) {
            val = val.toLowerCase();
            let i;
            let j;
            let list = [];
            const nodes = {};
            let keys = [];
            if (flags.uses) {
                keys = flags.uses;
            } else {
                keys = Object.keys(index);
            }
            for (i=0;i<keys.length;i++) {
                const key = keys[i];
                const kpos = val ? keys[i].indexOf(val) : -1;
                if (kpos > -1 || (val === "" && hasFlags)) {
                    const ids = Object.keys(index[key]||{});
                    for (j=0;j<ids.length;j++) {
                        var node = index[key][ids[j]];
                        var isConfigNode = node.node._def.category === "config" && node.node.type !== 'group';
                        if (flags.uses && key === node.node.id) {
                            continue;
                        }
                        if (flags.hasOwnProperty("invalid")) {
                            const nodeIsValid = !node.node.hasOwnProperty("valid") || node.node.valid;
                            if (flags.invalid === nodeIsValid) {
                                continue;
                            }
                        }
                        if (flags.hasOwnProperty("config")) {
                            if (flags.config !== isConfigNode) {
                                continue;
                            }
                        }
                        if (flags.hasOwnProperty("subflow")) {
                            if (flags.subflow !== (node.node.type === 'subflow')) {
                                continue;
                            }
                        }
                        if (flags.hasOwnProperty("modified")) {
                            if (!node.node.changed && !node.node.moved) {
                                continue;
                            }
                        }
                        if (flags.hasOwnProperty("hidden")) {
                            // Only tabs can be hidden
                            if (node.node.type !== 'tab') {
                                continue
                            }
                            if (!RED.workspaces.isHidden(node.node.id)) {
                                continue
                            }
                        }
                        if (flags.hasOwnProperty("unused")) {
                            const isUnused = (node.node.type === 'subflow' && node.node.instances.length === 0) ||
                                           (isConfigNode && node.node.users.length === 0 && node.node._def.hasUsers !== false)
                            if (flags.unused !== isUnused) {
                                continue;
                            }
                        }
                        if (flags.hasOwnProperty("flow")) {
                            if (flags.flow.indexOf(node.node.z || node.node.id) < 0) {
                                continue;
                            }
                        }
                        let typeIndex = -1
                        if(hasTypeFilter) {
                            typeIndex = flags.type.indexOf(node.node.type)
                        }
                        if (!hasTypeFilter || typeIndex > -1) {
                            nodes[node.node.id] = nodes[node.node.id] || {
                                node: node.node,
                                label: node.label
                            };
                            nodes[node.node.id].index = Math.min(nodes[node.node.id].index || Infinity, typeIndex > -1 ? typeIndex : kpos);
                        }
                    }
                }
            }
            list = Object.keys(nodes);
            list.sort(function(A,B) {
                return nodes[A].index - nodes[B].index;
            });

            for (i=0;i<list.length;i++) {
                results.push(nodes[list[i]]);
            }
        }
        return results;
    }

    function ensureSelectedIsVisible() {
        var selectedEntry = searchResults.find("li.selected");
        if (selectedEntry.length === 1) {
            var scrollWindow = searchResults.parent();
            var scrollHeight = scrollWindow.height();
            var scrollOffset = scrollWindow.scrollTop();
            var y = selectedEntry.position().top;
            var h = selectedEntry.height();
            if (y+h > scrollHeight) {
                scrollWindow.animate({scrollTop: '-='+(scrollHeight-(y+h)-10)},50);
            } else if (y<0) {
                scrollWindow.animate({scrollTop: '+='+(y-10)},50);
            }
        }
    }

    function populateSearchHistory() {
        if (searchHistory.length > 0) {
            searchResults.editableList('addItem',{
                historyHeader: true
            });
            searchHistory.forEach(function(entry) {
                searchResults.editableList('addItem',{
                    history: true,
                    value: entry
                });
            })
        }

    }
    function createDialog() {
        dialog = $("<div>",{id:"red-ui-search",class:"red-ui-search"}).appendTo("#red-ui-main-container");
        var searchDiv = $("<div>",{class:"red-ui-search-container"}).appendTo(dialog);
        searchInput = $('<input type="text" data-i18n="[placeholder]menu.label.searchInput">').appendTo(searchDiv).searchBox({
            delay: 200,
            change: function() {
                searchResults.editableList('empty');
                selected = -1;
                var value = $(this).val();
                if (value === "") {
                    populateSearchHistory();
                    return;
                }
                currentResults = search(value);
                if (currentResults.length > 0) {
                    for (let i=0;i<Math.min(currentResults.length,25);i++) {
                        searchResults.editableList('addItem',currentResults[i])
                    }
                    if (currentResults.length > 25) {
                        searchResults.editableList('addItem', {
                            more: {
                                results: currentResults,
                                start: 25
                            }
                        })
                    }
                } else {
                    searchResults.editableList('addItem',{});
                }
            },
            options: getSearchOptions()
        });
        var copySearchContainer = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-caret-right"></button>').appendTo(searchDiv).on('click', function(evt) {
            evt.preventDefault();
            RED.sidebar.info.outliner.search(searchInput.val())
            hide();
        });

        searchInput.on('keydown',function(evt) {
            var children;
            if (currentResults.length > 0) {
                if (evt.keyCode === 40) {
                    // Down
                    children = searchResults.children();
                    if (selected < children.length-1) {
                        if (selected > -1) {
                            $(children[selected]).removeClass('selected');
                        }
                        selected++;
                    }
                    $(children[selected]).addClass('selected');
                    ensureSelectedIsVisible();
                    evt.preventDefault();
                } else if (evt.keyCode === 38) {
                    // Up
                    children = searchResults.children();
                    if (selected > 0) {
                        if (selected < children.length) {
                            $(children[selected]).removeClass('selected');
                        }
                        selected--;
                    }
                    $(children[selected]).addClass('selected');
                    ensureSelectedIsVisible();
                    evt.preventDefault();
                } else if (evt.keyCode === 13) {
                    // Enter
                    children = searchResults.children();
                    if ($(children[selected]).hasClass("red-ui-search-more")) {
                        var object = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
                        if (object) {
                            searchResults.editableList('removeItem',object);
                            for (i=object.more.start;i<Math.min(currentResults.length,object.more.start+25);i++) {
                                searchResults.editableList('addItem',currentResults[i])
                            }
                            if (currentResults.length > object.more.start+25) {
                                searchResults.editableList('addItem', {
                                    more: {
                                        results: currentResults,
                                        start: object.more.start+25
                                    }
                                })
                            }
                        }
                    } if ($(children[selected]).hasClass("red-ui-search-history")) {
                        var object = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
                        if (object) {
                            searchInput.searchBox('value',object.value)
                        }
                    } else if (!$(children[selected]).hasClass("red-ui-search-historyHeader")) {
                        if (currentResults.length > 0) {
                            currentIndex = Math.max(0,selected);
                            reveal(currentResults[currentIndex].node);
                        }
                    }
                }
            }
        });
        searchInput.i18n();

        var searchResultsDiv = $("<div>",{class:"red-ui-search-results-container"}).appendTo(dialog);
        searchResults = $('<ol>',{style:"position: absolute;top: 5px;bottom: 5px;left: 5px;right: 5px;"}).appendTo(searchResultsDiv).editableList({
            addButton: false,
            addItem: function(container,i,object) {
                var node = object.node;
                var div;
                if (object.historyHeader) {
                    container.parent().addClass("red-ui-search-historyHeader")
                    $('<div>',{class:"red-ui-search-empty"}).text(RED._("search.history")).appendTo(container);
                    $('<button type="button" class="red-ui-button red-ui-button-small"></button>').text(RED._("search.clear")).appendTo(container).on("click", function(evt) {
                        evt.preventDefault();
                        searchHistory = [];
                        searchResults.editableList('empty');
                    });
                } else if (object.history) {
                    container.parent().addClass("red-ui-search-history")
                    div = $('<a>',{href:'#',class:"red-ui-search-result"}).appendTo(container);
                    div.text(object.value);
                    div.on("click", function(evt) {
                        evt.preventDefault();
                        searchInput.searchBox('value',object.value)
                        searchInput.focus();
                    })
                    $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-remove"></i></button>').appendTo(container).on("click", function(evt) {
                        evt.preventDefault();
                        var index = searchHistory.indexOf(object.value);
                        searchHistory.splice(index,1);
                        searchResults.editableList('removeItem', object);
                    });


                } else if (object.more) {
                    container.parent().addClass("red-ui-search-more")
                    div = $('<a>',{href:'#',class:"red-ui-search-result red-ui-search-empty"}).appendTo(container);
                    div.text(RED._("palette.editor.more",{count:object.more.results.length-object.more.start}));
                    div.on("click", function(evt) {
                        evt.preventDefault();
                        searchResults.editableList('removeItem',object);
                        for (i=object.more.start;i<Math.min(currentResults.length,object.more.start+25);i++) {
                            searchResults.editableList('addItem',currentResults[i])
                        }
                        if (currentResults.length > object.more.start+25) {
                            searchResults.editableList('addItem', {
                                more: {
                                    results: currentResults,
                                    start: object.more.start+25
                                }
                            })
                        }
                    });

                } else if (node === undefined) {
                    $('<div>',{class:"red-ui-search-empty"}).text(RED._('search.empty')).appendTo(container);
                } else {
                    var def = node._def;
                    div = $('<a>',{href:'#',class:"red-ui-search-result"}).appendTo(container);

                    RED.utils.createNodeIcon(node).appendTo(div);
                    var contentDiv = $('<div>',{class:"red-ui-search-result-node-description"}).appendTo(div);
                    if (node.z) {
                        var workspace = RED.nodes.workspace(node.z);
                        if (!workspace) {
                            workspace = RED.nodes.subflow(node.z);
                            workspace = "subflow:"+workspace.name;
                        } else {
                            workspace = "flow:"+workspace.label;
                        }
                        $('<div>',{class:"red-ui-search-result-node-flow"}).text(workspace).appendTo(contentDiv);
                    }

                    $('<div>',{class:"red-ui-search-result-node-label"}).text(object.label || node.id).appendTo(contentDiv);
                    $('<div>',{class:"red-ui-search-result-node-type"}).text(node.type).appendTo(contentDiv);
                    $('<div>',{class:"red-ui-search-result-node-id"}).text(node.id).appendTo(contentDiv);

                    div.on("click", function(evt) {
                        evt.preventDefault();
                        currentIndex = i;
                        reveal(node);
                    });
                }
            },
            scrollOnAdd: false
        });

    }

    function reveal(node) {
        var searchVal = searchInput.val();
        var existingIndex = searchHistory.indexOf(searchVal);
        if (existingIndex > -1) {
            searchHistory.splice(existingIndex,1);
        }
        searchHistory.unshift(searchVal);
        $("#red-ui-view-searchtools-search").data("term", searchVal);
        activeResults = Object.assign([], currentResults);
        hide(null, activeResults.length > 0);
        RED.view.reveal(node.id);
    }

    function revealPrev() {
        if (disabled) {
            updateSearchToolbar();
            return;
        }
        if (!searchResults || !activeResults.length) {
            show();
            return;
        }
        if (currentIndex > 0) {
            currentIndex--;
        } else {
            currentIndex = activeResults.length - 1;
        }
        const n = activeResults[currentIndex];
        if (n && n.node && n.node.id) {
            RED.view.reveal(n.node.id);
            $("#red-ui-view-searchtools-prev").trigger("focus");
        }
        updateSearchToolbar();
    }
    function revealNext() {
        if (disabled) {
            updateSearchToolbar();
            return;
        }
        if (!searchResults || !activeResults.length) {
            show();
            return;
        }
        if (currentIndex < activeResults.length - 1) {
            currentIndex++
        } else {
            currentIndex = 0;
        }
        const n = activeResults[currentIndex];
        if (n && n.node && n.node.id) {
            RED.view.reveal(n.node.id);
            $("#red-ui-view-searchtools-next").trigger("focus");
        }
        updateSearchToolbar();
    }

    function show(v) {
        if (disabled) {
            updateSearchToolbar();
            return;
        }
        if (!visible) {
            previousActiveElement = document.activeElement;
            $("#red-ui-header-shade").show();
            $("#red-ui-editor-shade").show();
            $("#red-ui-palette-shade").show();
            $("#red-ui-sidebar-shade").show();
            $("#red-ui-sidebar-separator").hide();

            if (dialog === null) {
                createDialog();
            } else {
                searchResults.editableList('empty');
            }
            dialog.slideDown(300);
            searchInput.searchBox('value',v)
            if (!v || v === "") {
                populateSearchHistory();
            }
            RED.events.emit("search:open");
            visible = true;
        }
        searchInput.trigger("focus");
    }

    function hide(el, keepSearchToolbar) {
        if (visible) {
            visible = false;
            $("#red-ui-header-shade").hide();
            $("#red-ui-editor-shade").hide();
            $("#red-ui-palette-shade").hide();
            $("#red-ui-sidebar-shade").hide();
            $("#red-ui-sidebar-separator").show();
            if (dialog !== null) {
                dialog.slideUp(200,function() {
                    searchInput.searchBox('value','');
                });
            }
            RED.events.emit("search:close");
            if (previousActiveElement && (!keepSearchToolbar || !activeResults.length)) {
                $(previousActiveElement).trigger("focus");
            }
            previousActiveElement = null;
        }
        if(!keepSearchToolbar) {
            clearActiveSearch();
        }
        updateSearchToolbar();
        if(keepSearchToolbar && activeResults.length) {
            $("#red-ui-view-searchtools-next").trigger("focus");
        }
    }
    function updateSearchToolbar() {
        if (!disabled && currentIndex >= 0 && activeResults && activeResults.length) {
            let term = $("#red-ui-view-searchtools-search").data("term") || "";
            if (term.length > 16) {
                term = term.substring(0, 12) + "..."
            }
            const i18nSearchCounterData = {
                term: term,
                result: (currentIndex + 1),
                count: activeResults.length
            }
            $("#red-ui-view-searchtools-counter").text(RED._('actions.search-counter', i18nSearchCounterData));
            $("#view-search-tools > :not(:first-child)").show(); //show other tools
        } else {
            clearActiveSearch();
            $("#view-search-tools > :not(:first-child)").hide(); //hide all but search button
        }
    }
    function clearIndex() {
        index = {};
    }

    function addItemToIndex(item) {
        indexNode(item);
    }
    function removeItemFromIndex(item) {
        var keys = Object.keys(index);
        for (var i=0,l=keys.length;i<l;i++) {
            delete index[keys[i]][item.id];
            if (Object.keys(index[keys[i]]).length === 0) {
                delete index[keys[i]];
            }
        }
    }
    function updateItemOnIndex(item) {
        removeItemFromIndex(item);
        addItemToIndex(item);
    }

    function clearActiveSearch() {
        activeResults = [];
        currentIndex = 0;
        $("#red-ui-view-searchtools-search").data("term", "");
    }

    function getSearchOptions() {
        return [
            {label:RED._("search.options.configNodes"), value:"is:config"},
            {label:RED._("search.options.unusedConfigNodes"), value:"is:config is:unused"},
            {label:RED._("search.options.modifiedNodes"), value:"is:modified"},
            {label:RED._("search.options.invalidNodes"), value: "is:invalid"},
            {label:RED._("search.options.uknownNodes"), value: "type:unknown"},
            {label:RED._("search.options.unusedSubflows"), value:"is:subflow is:unused"},
            {label:RED._("search.options.hiddenFlows"), value:"is:hidden"},
            {label:RED._("search.options.thisFlow"), value:"flow:current"},
        ]
    }

    function init() {
        RED.actions.add("core:search",show);
        RED.actions.add("core:search-previous",revealPrev);
        RED.actions.add("core:search-next",revealNext);

        RED.events.on("editor:open",function() { disabled = true; });
        RED.events.on("editor:close",function() { disabled = false; });
        RED.events.on("type-search:open",function() { disabled = true; });
        RED.events.on("type-search:close",function() { disabled = false; });
        RED.events.on("actionList:open",function() { disabled = true; });
        RED.events.on("actionList:close",function() { disabled = false; });

        RED.keyboard.add("red-ui-search","escape",hide);

        RED.keyboard.add("view-search-tools","escape",function() {
            clearActiveSearch();
            updateSearchToolbar();
        });

        $("#red-ui-header-shade").on('mousedown',hide);
        $("#red-ui-editor-shade").on('mousedown',hide);
        $("#red-ui-palette-shade").on('mousedown',hide);
        $("#red-ui-sidebar-shade").on('mousedown',hide);

        $("#red-ui-view-searchtools-close").on("click", function close() {
            clearActiveSearch();
            updateSearchToolbar();
        });
        $("#red-ui-view-searchtools-close").trigger("click");

        RED.events.on("workspace:clear", clearIndex);

        RED.events.on("flows:add", addItemToIndex);
        RED.events.on("flows:remove", removeItemFromIndex);
        RED.events.on("flows:change", updateItemOnIndex);

        RED.events.on("subflows:add", addItemToIndex);
        RED.events.on("subflows:remove", removeItemFromIndex);
        RED.events.on("subflows:change", updateItemOnIndex);

        RED.events.on("nodes:add",addItemToIndex);
        RED.events.on("nodes:remove",removeItemFromIndex);
        RED.events.on("nodes:change",updateItemOnIndex);

        RED.events.on("groups:add",addItemToIndex);
        RED.events.on("groups:remove",removeItemFromIndex);
        RED.events.on("groups:change",updateItemOnIndex);

    }

    return {
        init: init,
        show: show,
        hide: hide,
        search: search,
        getSearchOptions: getSearchOptions
    };

})();
;RED.contextMenu = (function () {

    let menu;

    function disposeMenu() {
        $(document).off("mousedown.red-ui-workspace-context-menu");
        if (menu) {
            menu.remove();
        }
        menu = null;
    }
    function show(options) {
        if (menu) {
            menu.remove()
        }
        let menuItems = []
        if (options.options) {
            menuItems = options.options
        } else if (options.type === 'workspace') {
            const selection = RED.view.selection()
            const noSelection = !selection || Object.keys(selection).length === 0
            const hasSelection = (selection.nodes && selection.nodes.length > 0);
            const hasMultipleSelection = hasSelection && selection.nodes.length > 1;
            const virtulLinks = (selection.links && selection.links.filter(e => !!e.link)) || [];
            const wireLinks = (selection.links && selection.links.filter(e => !e.link)) || [];
            const hasLinks = wireLinks.length > 0;
            const isSingleLink = !hasSelection && hasLinks && wireLinks.length === 1
            const isMultipleLinks = !hasSelection && hasLinks && wireLinks.length > 1
            const canDelete = hasSelection || hasLinks
            const isGroup = hasSelection && selection.nodes.length === 1 && selection.nodes[0].type === 'group'
            const canEdit = !RED.workspaces.isLocked()
            const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g
            let hasGroup, isAllGroups = true, hasDisabledNode, hasEnabledNode, hasLabeledNode, hasUnlabeledNode;
            if (hasSelection) {
                const nodes = selection.nodes.slice();
                while (nodes.length) {
                    const n = nodes.shift();
                    if (n.type === 'group') {
                        hasGroup = true;
                        nodes.push(...n.nodes);
                    } else {
                        isAllGroups = false;
                        if (n.d) {
                            hasDisabledNode = true;
                        } else {
                            hasEnabledNode = true;
                        }
                    }
                    if (n.l === undefined || n.l) {
                        hasLabeledNode = true;
                    } else {
                        hasUnlabeledNode = true;
                    }
                }
            }

            const scale = RED.view.scale()
            const offset = $("#red-ui-workspace-chart").offset()
            let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale
            let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale

            if (RED.view.snapGrid) {
                const gridSize = RED.view.gridSize()
                addX = gridSize * Math.round(addX / gridSize)
                addY = gridSize * Math.round(addY / gridSize)
            }

            if (RED.settings.theme("menu.menu-item-action-list", true)) {
                menuItems.push(
                    { onselect: 'core:show-action-list', label: RED._("contextMenu.showActionList"), onpostselect: function () { } }
                )
            }
            const insertOptions = []
            menuItems.push({ label: RED._("contextMenu.insert"), options: insertOptions })
            insertOptions.push(
                {
                    label: RED._("contextMenu.node"),
                    onselect: function () {
                        RED.view.showQuickAddDialog({
                            position: [addX, addY],
                            touchTrigger: 'ontouchstart' in window,
                            splice: isSingleLink ? selection.links[0] : undefined,
                            // spliceMultiple: isMultipleLinks
                        })
                    },
                    disabled: !canEdit
                },
                (hasLinks) ? { // has least 1 wire selected
                    label: RED._("contextMenu.junction"),
                    onselect: function () {
                        RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY })
                    },
                    disabled: !canEdit || !hasLinks
                } : {
                    label: RED._("contextMenu.junction"),
                    onselect: function () {
                        const nn = {
                            _def: { defaults: {} },
                            type: 'junction',
                            z: RED.workspaces.active(),
                            id: RED.nodes.id(),
                            x: addX,
                            y: addY,
                            w: 0, h: 0,
                            outputs: 1,
                            inputs: 1,
                            dirty: true,
                            moved: true
                        }
                        const junction = RED.nodes.addJunction(nn);
                        const historyEvent = {
                            dirty: RED.nodes.dirty(),
                            t: 'add',
                            junctions: [junction]
                        }
                        RED.history.push(historyEvent);
                        RED.nodes.dirty(true);
                        RED.view.select({nodes: [junction] });
                        RED.view.redraw(true)
                    },
                    disabled: !canEdit
                },
                {
                    label: RED._("contextMenu.linkNodes"),
                    onselect: 'core:split-wire-with-link-nodes',
                    disabled: !canEdit || !hasLinks
                },
                null
            )
            if (RED.settings.theme("menu.menu-item-import-library", true)) {
                insertOptions.push(
                    { onselect: 'core:show-import-dialog', label: RED._('common.label.import')},
                    { onselect: 'core:show-examples-import-dialog', label: RED._('menu.label.importExample') }
                )
            }


            if (hasSelection && canEdit) {
                const nodeOptions = []
                if (!hasMultipleSelection && !isGroup) {
                    nodeOptions.push(
                        { onselect: 'core:show-node-help', label: RED._('menu.label.showNodeHelp') },
                        null
                    )
                }
                nodeOptions.push(
                    { onselect: 'core:enable-selected-nodes', label: RED._('menu.label.enableSelectedNodes'), disabled: !hasDisabledNode },
                    { onselect: 'core:disable-selected-nodes', label: RED._('menu.label.disableSelectedNodes'), disabled: !hasEnabledNode },
                    null,
                    { onselect: 'core:show-selected-node-labels', label: RED._('menu.label.showSelectedNodeLabels'), disabled: !hasUnlabeledNode },
                    { onselect: 'core:hide-selected-node-labels', label: RED._('menu.label.hideSelectedNodeLabels'), disabled: !hasLabeledNode }
                )
                menuItems.push({
                    label: RED._('sidebar.info.node'),
                    options: nodeOptions
                })
                menuItems.push({
                    label: RED._('sidebar.info.group'),
                    options: [
                        { onselect: 'core:group-selection', label: RED._("menu.label.groupSelection") },
                        { onselect: 'core:ungroup-selection', label: RED._("menu.label.ungroupSelection"), disabled: !hasGroup },
                    ]
                })
                if (hasGroup) {
                    menuItems[menuItems.length - 1].options.push(
                        { onselect: 'core:merge-selection-to-group', label: RED._("menu.label.groupMergeSelection") }
                    )

                }
                if (canRemoveFromGroup) {
                    menuItems[menuItems.length - 1].options.push(
                        { onselect: 'core:remove-selection-from-group', label: RED._("menu.label.groupRemoveSelection") }
                    )
                }
                menuItems[menuItems.length - 1].options.push(
                    null,
                    { onselect: 'core:copy-group-style', label: RED._("keyboard.copyGroupStyle"), disabled: !hasGroup },
                    { onselect: 'core:paste-group-style', label: RED._("keyboard.pasteGroupStyle"), disabled: !hasGroup}
                )
            }
            if (canEdit && hasMultipleSelection) {
                menuItems.push({
                    label: RED._('menu.label.arrange'),
                    options: [
                        { label:RED._("menu.label.alignLeft"), onselect: "core:align-selection-to-left"},
                        { label:RED._("menu.label.alignCenter"), onselect: "core:align-selection-to-center"},
                        { label:RED._("menu.label.alignRight"), onselect: "core:align-selection-to-right"},
                        null,
                        { label:RED._("menu.label.alignTop"), onselect: "core:align-selection-to-top"},
                        { label:RED._("menu.label.alignMiddle"), onselect: "core:align-selection-to-middle"},
                        { label:RED._("menu.label.alignBottom"), onselect: "core:align-selection-to-bottom"},
                        null,
                        { label:RED._("menu.label.distributeHorizontally"), onselect: "core:distribute-selection-horizontally"},
                        { label:RED._("menu.label.distributeVertically"), onselect: "core:distribute-selection-vertically"}
                    ]
                })
            }


            menuItems.push(
                null,
                { onselect: 'core:undo', label: RED._("keyboard.undoChange"), disabled: RED.history.list().length === 0 },
                { onselect: 'core:redo', label: RED._("keyboard.redoChange"), disabled: RED.history.listRedo().length === 0 },
                null,
                { onselect: 'core:cut-selection-to-internal-clipboard', label: RED._("keyboard.cutNode"), disabled: !canEdit || !hasSelection },
                { onselect: 'core:copy-selection-to-internal-clipboard', label: RED._("keyboard.copyNode"), disabled: !hasSelection },
                { onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !canEdit || !RED.view.clipboard() },
                { onselect: 'core:delete-selection', label: RED._('keyboard.deleteSelected'), disabled: !canEdit || !canDelete },
                { onselect: 'core:delete-selection-and-reconnect', label: RED._('keyboard.deleteReconnect'), disabled: !canEdit || !canDelete },
            )
            if (RED.settings.theme("menu.menu-item-export-library", true)) {
                menuItems.push(
                    { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }
                )
            }
            menuItems.push(
                { onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") }
            )
        }

        var direction = "right";
        var MENU_WIDTH = 500; // can not use menu width here
        if ((options.x -$(document).scrollLeft()) >
            ($(window).width() -MENU_WIDTH)) {
            direction = "left";
        }

        menu = RED.menu.init({
            direction: direction,
            onpreselect: function() {
                disposeMenu()
            },
            onpostselect: function () {
                RED.view.focus()
            },
            options: menuItems
        });

        menu.attr("id", "red-ui-workspace-context-menu");
        menu.css({
            position: "absolute"
        })
        menu.appendTo("body");

        // TODO: prevent the menu from overflowing the window.

        var top = options.y
        var left = options.x

        if (top + menu.height() - $(document).scrollTop() > $(window).height()) {
            top -= (top + menu.height()) - $(window).height() + 22;
        }
        if (left + menu.width() - $(document).scrollLeft() > $(window).width()) {
            left -= (left + menu.width()) - $(window).width() + 18;
        }
        menu.css({
            top: top + "px",
            left: left + "px"
        })
        $(".red-ui-menu.red-ui-menu-dropdown").hide();
        $(document).on("mousedown.red-ui-workspace-context-menu", function (evt) {
            if (menu && menu[0].contains(evt.target)) {
                return
            }
            disposeMenu()
        });
        menu.show();
        // set focus to first item so that pressing escape key closes the menu
        $("#red-ui-workspace-context-menu :first(ul) > a").trigger("focus")

    }
    // Allow escape key hook and other editor events to close context menu
    RED.keyboard.add("red-ui-workspace-context-menu", "escape", function () { RED.contextMenu.hide() })
    RED.events.on("editor:open", function () { RED.contextMenu.hide() });
    RED.events.on("search:open", function () { RED.contextMenu.hide() });
    RED.events.on("type-search:open", function () { RED.contextMenu.hide() });
    RED.events.on("actionList:open", function () { RED.contextMenu.hide() });
    RED.events.on("view:selection-changed", function () { RED.contextMenu.hide() });
    return {
        show: show,
        hide: disposeMenu
    }
})()
;/**
 * 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.
 **/
RED.actionList = (function() {

    var disabled = false;
    var dialog = null;
    var searchInput;
    var searchResults;
    var selected = -1;
    var visible = false;

    var filterTerm = "";
    var filterTerms = [];
    var previousActiveElement;

    function ensureSelectedIsVisible() {
        var selectedEntry = searchResults.find("li.selected");
        if (selectedEntry.length === 1) {
            var scrollWindow = searchResults.parent();
            var scrollHeight = scrollWindow.height();
            var scrollOffset = scrollWindow.scrollTop();
            var y = selectedEntry.position().top;
            var h = selectedEntry.height();
            if (y+h > scrollHeight) {
                scrollWindow.animate({scrollTop: '-='+(scrollHeight-(y+h)-10)},50);
            } else if (y<0) {
                scrollWindow.animate({scrollTop: '+='+(y-10)},50);
            }
        }
    }

    function createDialog() {
        dialog = $("<div>",{id:"red-ui-actionList",class:"red-ui-search"}).appendTo("#red-ui-main-container");
        var searchDiv = $("<div>",{class:"red-ui-search-container"}).appendTo(dialog);
        searchInput = $('<input type="text" data-i18n="[placeholder]keyboard.filterActions">').appendTo(searchDiv).searchBox({
            change: function() {
                filterTerm = $(this).val().trim().toLowerCase();
                filterTerms = filterTerm.split(" ");
                searchResults.editableList('filter');
                searchResults.find("li.selected").removeClass("selected");
                var children = searchResults.children(":visible");
                if (children.length) {
                    $(children[0]).addClass('selected');
                }
            }
        });

        searchInput.on('keydown',function(evt) {
            var selectedChild;
            if (evt.keyCode === 40) {
                // Down
                selectedChild = searchResults.find("li.selected");
                if (!selectedChild.length) {
                    var children = searchResults.children(":visible");
                    if (children.length) {
                        $(children[0]).addClass('selected');
                    }
                } else {
                    var nextChild = selectedChild.nextAll(":visible").first();
                    if (nextChild.length) {
                        selectedChild.removeClass('selected');
                        nextChild.addClass('selected');
                    }
                }
                ensureSelectedIsVisible();
                evt.preventDefault();
            } else if (evt.keyCode === 38) {
                // Up
                selectedChild = searchResults.find("li.selected");
                var nextChild = selectedChild.prevAll(":visible").first();
                if (nextChild.length) {
                    selectedChild.removeClass('selected');
                    nextChild.addClass('selected');
                }
                ensureSelectedIsVisible();
                evt.preventDefault();
            } else if (evt.keyCode === 13) {
                // Enter
                selectedChild = searchResults.find("li.selected");
                selectCommand(searchResults.editableList('getItem',selectedChild));
            }
        });
        searchInput.i18n();

        var searchResultsDiv = $("<div>",{class:"red-ui-search-results-container"}).appendTo(dialog);
        searchResults = $('<ol>',{style:"position: absolute;top: 5px;bottom: 5px;left: 5px;right: 5px;"}).appendTo(searchResultsDiv).editableList({
            addButton: false,
            addItem: function(container,i,action) {
                if (action.id === undefined) {
                    $('<div>',{class:"red-ui-search-empty"}).text(RED._('search.empty')).appendTo(container);
                } else {
                    var div = $('<a>',{href:'#',class:"red-ui-search-result"}).appendTo(container);
                    var contentDiv = $('<div>',{class:"red-ui-search-result-action"}).appendTo(div);


                    $('<div>').text(action.label).appendTo(contentDiv);
                    // $('<div>',{class:"red-ui-search-result-node-type"}).text(node.type).appendTo(contentDiv);
                    // $('<div>',{class:"red-ui-search-result-node-id"}).text(node.id).appendTo(contentDiv);
                    if (action.key) {
                        $('<div>',{class:"red-ui-search-result-action-key"}).html(RED.keyboard.formatKey(action.key)).appendTo(contentDiv);
                    }
                    div.on("click", function(evt) {
                        evt.preventDefault();
                        selectCommand(action);
                    });
                }
            },
            scrollOnAdd: false,
            filter: function(item) {
                if (filterTerm !== "") {
                    var pos=0;
                    for (var i=0;i<filterTerms.length;i++) {
                        var j = item._label.indexOf(filterTerms[i],pos);
                        if (j > -1) {
                            pos = j;
                        } else {
                            return false;
                        }
                    }
                    return true;
                }
                return true;
            }
        });

    }

    function selectCommand(command) {
        hide();
        if (command) {
            RED.actions.invoke(command.id);
        }
    }

    function show(v) {
        if (disabled) {
            return;
        }
        if (!visible) {
            previousActiveElement = document.activeElement;
            $("#red-ui-header-shade").show();
            $("#red-ui-editor-shade").show();
            $("#red-ui-palette-shade").show();
            $("#red-ui-sidebar-shade").show();
            $("#red-ui-sidebar-separator").hide();
            if (dialog === null) {
                createDialog();
            }
            dialog.slideDown(300);
            searchInput.searchBox('value',v);
            searchResults.editableList('empty');
            results = [];
            var actions = RED.actions.list();
            actions.sort(function(A,B) {
                var Akey = A.label;
                var Bkey = B.label;
                return Akey.localeCompare(Bkey);
            });
            actions.forEach(function(action) {
                action._label = action.label.toLowerCase();
                searchResults.editableList('addItem',action);
            });
            RED.events.emit("actionList:open");
            visible = true;
        }
        searchInput.trigger("focus");
        var children = searchResults.children(":visible");
        if (children.length) {
            $(children[0]).addClass('selected');
        }
    }

    function hide() {
        if (visible) {
            visible = false;
            $("#red-ui-header-shade").hide();
            $("#red-ui-editor-shade").hide();
            $("#red-ui-palette-shade").hide();
            $("#red-ui-sidebar-shade").hide();
            $("#red-ui-sidebar-separator").show();
            if (dialog !== null) {
                dialog.slideUp(200,function() {
                    searchInput.searchBox('value','');
                });
            }
            RED.events.emit("actionList:close");
            if (previousActiveElement) {
                $(previousActiveElement).trigger("focus");
                previousActiveElement = null;
            }
        }
    }

    function init() {
        if (RED.settings.theme("menu.menu-item-action-list", true)) {
            RED.actions.add("core:show-action-list",show);
        }

        RED.events.on("editor:open",function() { disabled = true; });
        RED.events.on("editor:close",function() { disabled = false; });
        RED.events.on("search:open",function() { disabled = true; });
        RED.events.on("search:close",function() { disabled = false; });
        RED.events.on("type-search:open",function() { disabled = true; });
        RED.events.on("type-search:close",function() { disabled = false; });

        RED.keyboard.add("red-ui-actionList","escape",function(){hide()});


        $("#red-ui-header-shade").on('mousedown',hide);
        $("#red-ui-editor-shade").on('mousedown',hide);
        $("#red-ui-palette-shade").on('mousedown',hide);
        $("#red-ui-sidebar-shade").on('mousedown',hide);
    }

    return {
        init: init,
        show: show,
        hide: hide
    };

})();
;RED.typeSearch = (function() {

    var shade;

    var disabled = false;
    var dialog = null;
    var searchInput;
    var searchResults;
    var searchResultsDiv;
    var selected = -1;
    var visible = false;

    var activeFilter = "";
    var addCallback;
    var cancelCallback;
    var moveCallback;

    var typesUsed = {};

    function search(val) {
        activeFilter = val.toLowerCase();
        var visible = searchResults.editableList('filter');
        searchResults.editableList('sort');
        setTimeout(function() {
            selected = 0;
            searchResults.children().removeClass('selected');
            searchResults.children(":visible:first").addClass('selected');
        },100);

    }

    function ensureSelectedIsVisible() {
        var selectedEntry = searchResults.find("li.selected");
        if (selectedEntry.length === 1) {
            var scrollWindow = searchResults.parent();
            var scrollHeight = scrollWindow.height();
            var scrollOffset = scrollWindow.scrollTop();
            var y = selectedEntry.position().top;
            var h = selectedEntry.height();
            if (y+h > scrollHeight) {
                scrollWindow.animate({scrollTop: '-='+(scrollHeight-(y+h)-10)},50);
            } else if (y<0) {
                scrollWindow.animate({scrollTop: '+='+(y-10)},50);
            }
        }
    }

    function moveDialog(dx,dy) {
        var pos = dialog.position();
        pos.top = (pos.top + dy)+"px";
        pos.left = (pos.left + dx)+"px";
        dialog.css(pos);
        moveCallback(dx,dy);

    }
    function createDialog() {
        dialog = $("<div>",{id:"red-ui-type-search",class:"red-ui-search red-ui-type-search"}).appendTo("#red-ui-main-container");
        var searchDiv = $("<div>",{class:"red-ui-search-container"}).appendTo(dialog);
        searchInput = $('<input type="text" id="red-ui-type-search-input">').attr("placeholder",RED._("search.addNode")).appendTo(searchDiv).searchBox({
            delay: 50,
            change: function() {
                search($(this).val());
            }
        });
        searchInput.on('keydown',function(evt) {
            var children = searchResults.children(":visible");
            if (evt.keyCode === 40 && evt.shiftKey) {
                evt.preventDefault();
                moveDialog(0,10);
            } else if (evt.keyCode === 38 && evt.shiftKey) {
                evt.preventDefault();
                moveDialog(0,-10);
            } else if (evt.keyCode === 39 && evt.shiftKey) {
                evt.preventDefault();
                moveDialog(10,0);
            } else if (evt.keyCode === 37 && evt.shiftKey) {
                evt.preventDefault();
                moveDialog(-10,0);
            } else if (children.length > 0) {
                if (evt.keyCode === 40) {
                    // Down
                    if (selected < children.length-1) {
                        if (selected > -1) {
                            $(children[selected]).removeClass('selected');
                        }
                        selected++;
                    }
                    $(children[selected]).addClass('selected');
                    ensureSelectedIsVisible();
                    evt.preventDefault();
                } else if (evt.keyCode === 38) {
                    if (selected > 0) {
                        if (selected < children.length) {
                            $(children[selected]).removeClass('selected');
                        }
                        selected--;
                    }
                    $(children[selected]).addClass('selected');
                    ensureSelectedIsVisible();
                    evt.preventDefault();
                } else if ((evt.metaKey || evt.ctrlKey) && evt.keyCode === 13 ) {
                    evt.preventDefault();
                    // (ctrl or cmd) and enter
                    var index = Math.max(0,selected);
                    if (index < children.length) {
                        var n = $(children[index]).find(".red-ui-editableList-item-content").data('data');
                        if (!/^_action_:/.test(n.type)) {
                            typesUsed[n.type] = Date.now();
                        }
                        if (n.def.outputs === 0) {
                            confirm(n);
                        } else {
                            addCallback(n.type,true);
                        }
                        $("#red-ui-type-search-input").val("").trigger("keyup");
                        setTimeout(function() {
                            $("#red-ui-type-search-input").focus();
                        },100);
                    }
                } else if (evt.keyCode === 13) {
                    evt.preventDefault();
                    // Enter
                    var index = Math.max(0,selected);
                    if (index < children.length) {
                        // TODO: dips into editableList impl details
                        confirm($(children[index]).find(".red-ui-editableList-item-content").data('data'));
                    }
                }
            } else {
                if (evt.keyCode === 13 ) {
                    // Stop losing focus if [Cmd]-Enter is pressed on an empty list
                    evt.stopPropagation();
                    evt.preventDefault();
                }
            }
        });

        searchResultsDiv = $("<div>",{class:"red-ui-search-results-container"}).appendTo(dialog);
        searchResults = $('<ol>',{style:"position: absolute;top: 0;bottom: 0;left: 0;right: 0;"}).appendTo(searchResultsDiv).editableList({
            addButton: false,
            filter: function(data) {
                if (activeFilter === "" ) {
                    return true;
                }
                if (data.recent || data.common) {
                    return false;
                }
                return (activeFilter==="")||(data.index.indexOf(activeFilter) > -1);
            },
            sort: function(A,B) {
                if (activeFilter === "") {
                    return A.i - B.i;
                }
                var Ai = A.index.indexOf(activeFilter);
                var Bi = B.index.indexOf(activeFilter);
                if (Ai === -1) {
                    return 1;
                }
                if (Bi === -1) {
                    return -1;
                }
                if (Ai === Bi) {
                    return sortTypeLabels(A,B);
                }
                return Ai-Bi;
            },
            addItem: function(container,i,object) {
                var def = object.def;
                object.index = object.type.toLowerCase();
                if (object.separator) {
                    container.addClass("red-ui-search-result-separator")
                }
                var div = $('<div>',{class:"red-ui-search-result"}).appendTo(container);

                var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div);
                if (object.type === "junction") {
                    nodeDiv.addClass("red-ui-palette-icon-junction");
                } else if (/^_action_:/.test(object.type)) {
                    nodeDiv.addClass("red-ui-palette-icon-junction")
                } else {
                    var colour = RED.utils.getNodeColor(object.type,def);
                    nodeDiv.css('backgroundColor',colour);
                }
                var icon_url = RED.utils.getNodeIcon(def);

                var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
                RED.utils.createIconElement(icon_url, iconContainer, false);

                if (/^subflow:/.test(object.type)) {
                    var sf = RED.nodes.subflow(object.type.substring(8));
                    if (sf.in.length > 0) {
                        $('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);
                    }
                    if (sf.out.length > 0) {
                        $('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv);
                    }
                } else if (!/^_action_:/.test(object.type) && object.type !== "junction") {
                    if (def.inputs > 0) {
                        $('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);
                    }
                    if (def.outputs > 0) {
                        $('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv);
                    }
                }

                var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);

                var label = object.label;
                object.index += "|"+label.toLowerCase();

                $('<div>',{class:"red-ui-search-result-node-label"}).text(label).appendTo(contentDiv);

                div.on("click", function(evt) {
                    evt.preventDefault();
                    confirm(object);
                });
            },
            scrollOnAdd: false
        });

    }
    function confirm(def) {
        hide();
        if (!/^_action_:/.test(def.type)) {
            typesUsed[def.type] = Date.now();
        }
        addCallback(def.type);
    }

    function handleMouseActivity(evt) {
        if (visible) {
            var t = $(evt.target);
            while (t.prop('nodeName').toLowerCase() !== 'body') {
                if (t.attr('id') === 'red-ui-type-search') {
                    return;
                }
                t = t.parent();
            }
            hide(true);
            if (cancelCallback) {
                cancelCallback();
            }
        }
    }
    function show(opts) {
        if (!visible) {
            if (dialog === null) {
                createDialog();
                RED.keyboard.add("red-ui-type-search","escape",function(){
                    hide();
                    if (cancelCallback) {
                        cancelCallback();
                    }
                });
            }
            visible = true;
        } else {
            dialog.hide();
            searchResultsDiv.hide();
        }
        $(document).off('mousedown.red-ui-type-search');
        $(document).off('mouseup.red-ui-type-search');
        $(document).off('click.red-ui-type-search');
        $(document).off('touchstart.red-ui-type-search');
        $(document).off('mousedown.red-ui-type-search');
        setTimeout(function() {
            $(document).on('mousedown.red-ui-type-search',handleMouseActivity);
            $(document).on('mouseup.red-ui-type-search',handleMouseActivity);
            $(document).on('click.red-ui-type-search',handleMouseActivity);
            $(document).on('touchstart.red-ui-type-search',handleMouseActivity);
        },200);

        refreshTypeList(opts);
        addCallback = opts.add;
        cancelCallback = opts.cancel;
        moveCallback = opts.move;
        RED.events.emit("type-search:open");
        //shade.show();
        if ($("#red-ui-main-container").height() - opts.y - 195 < 0) {
            opts.y = opts.y - 275;
        }
        const dialogWidth = dialog.width() || 300 // default is 300 (defined in class .red-ui-search)
        const workspaceWidth = $('#red-ui-workspace').width()
        if (workspaceWidth > dialogWidth && workspaceWidth - opts.x - dialogWidth < 0) {
            opts.x = opts.x - (dialogWidth - RED.view.node_width)
        }
        dialog.css({left:opts.x+"px",top:opts.y+"px"}).show();
        searchResultsDiv.slideDown(300);
        setTimeout(function() {
            searchResultsDiv.find(".red-ui-editableList-container").scrollTop(0);
            if (!opts.disableFocus) {
                searchInput.trigger("focus");
            }
        },200);
    }
    function hide(fast) {
        if (visible) {
            visible = false;
            if (dialog !== null) {
                searchResultsDiv.slideUp(fast?50:200,function() {
                    dialog.hide();
                    searchInput.searchBox('value','');
                });
                //shade.hide();
            }
            RED.events.emit("type-search:close");
            RED.view.focus();
            $(document).off('mousedown.red-ui-type-search');
            $(document).off('mouseup.red-ui-type-search');
            $(document).off('click.red-ui-type-search');
            $(document).off('touchstart.red-ui-type-search');
        }
    }
    function getTypeLabel(type, def) {
        var label = type;
        if (typeof def.paletteLabel !== "undefined") {
            try {
                label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
                label += " ("+type+")";
            } catch(err) {
                console.log("Definition error: "+type+".paletteLabel",err);
            }
        }
        return label;
    }
    function sortTypeLabels(a,b) {
        var al = a.label.toLowerCase();
        var bl = b.label.toLowerCase();
        if (al < bl) {
            return -1;
        } else if (al === bl) {
            return 0;
        } else {
            return 1;
        }
    }
    function applyFilter(filter,type,def) {
        if (!filter) {
            // No filter; allow everything
            return true
        }
        if (type === 'junction') {
            // Only allow Junction is there's no specific type filter
            return !filter.type
        }
        if (filter.type) {
            // Handle explicit type filter
            return filter.type === type
        }
        if (!def) {
            // No node definition available - allow it
            return true
        }
        // Check if the filter is for input/outputs and apply
        return (!filter.input || def.inputs > 0) &&
                (!filter.output || def.outputs > 0)
    }
    function refreshTypeList(opts) {
        var i;
        searchResults.editableList('empty');
        searchInput.searchBox('value','').focus();
        selected = -1;
        var common = [
            'inject','debug','function','change','switch','junction'
        ].filter(function(t) { return applyFilter(opts.filter,t,RED.nodes.getType(t)); });

        // if (opts.filter && opts.filter.input && opts.filter.output && !opts.filter.type) {
        //     if (opts.filter.spliceMultiple) {
        //         common.push('_action_:core:split-wires-with-junctions')
        //     }
        //     common.push('_action_:core:split-wire-with-link-nodes')
        // }

        var recentlyUsed = Object.keys(typesUsed);
        recentlyUsed.sort(function(a,b) {
            return typesUsed[b]-typesUsed[a];
        });
        recentlyUsed = recentlyUsed.filter(function(t) {
            return applyFilter(opts.filter,t,RED.nodes.getType(t)) && common.indexOf(t) === -1;
        });

        var items = [];
        RED.nodes.registry.getNodeTypes().forEach(function(t) {
            var def = RED.nodes.getType(t);
            if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
                items.push({type:t,def: def, label:getTypeLabel(t,def)});
            }
        });
        items.push({ type: 'junction', def: { inputs:1, outputs: 1, label: 'junction', type: 'junction'}, label: 'junction' })
        items.sort(sortTypeLabels);

        var commonCount = 0;
        var item;
        var index = 0;
        for(i=0;i<common.length;i++) {
            var itemDef = RED.nodes.getType(common[i]);
            if (common[i] === 'junction') {
                itemDef = { inputs:1, outputs: 1, label: 'junction', type: 'junction'}
            } else if (/^_action_:/.test(common[i]) ) {
                itemDef = { inputs:1, outputs: 1, label: common[i], type: common[i]}
            }
            if (itemDef) {
                item = {
                    type: common[i],
                    common: true,
                    def: itemDef,
                    i: index++
                };
                item.label = getTypeLabel(item.type,item.def);
                if (i === common.length-1) {
                    item.separator = true;
                }
                searchResults.editableList('addItem', item);
            }
        }
        for(i=0;i<Math.min(5,recentlyUsed.length);i++) {
            item = {
                type:recentlyUsed[i],
                def: RED.nodes.getType(recentlyUsed[i]),
                recent: true,
                i: index++
            };
            item.label = getTypeLabel(item.type,item.def);
            if (i === recentlyUsed.length-1) {
                item.separator = true;
            }
            searchResults.editableList('addItem', item);
        }
        for (i=0;i<items.length;i++) {
            if (applyFilter(opts.filter,items[i].type,items[i].def)) {
                items[i].i = index++;
                searchResults.editableList('addItem', items[i]);
            }
        }
        setTimeout(function() {
            selected = 0;
            searchResults.children(":first").addClass('selected');
        },100);
    }

    return {
        show: show,
        refresh: refreshTypeList,
        hide: hide
    };

})();
;/**
 * 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.
 **/

RED.subflow = (function() {

    var _subflowEditTemplate = '<script type="text/x-red" data-template-name="subflow">'+
        '<div class="form-row">'+
            '<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>'+
            '<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">'+
        '</div>'+
        '<div id="subflow-input-ui"></div>'+
        '</script>';

    var _subflowTemplateEditTemplate = '<script type="text/x-red" data-template-name="subflow-template">'+
        '<div class="form-row">'+
            '<label for="subflow-input-name" data-i18n="[append]common.label.name"><i class="fa fa-tag"></i>  </label>'+
            '<input type="text" id="subflow-input-name" data-i18n="[placeholder]common.label.name">'+
        '</div>'+
        '<div class="form-row">'+
            '<ul style="margin-bottom: 20px;" id="subflow-env-tabs"></ul>'+
        '</div>'+
        '<div id="subflow-env-tabs-content">'+
            '<div id="subflow-env-tab-edit">'+
                '<div class="form-row node-input-env-container-row" id="subflow-input-edit-ui">'+
                    '<ol id="node-input-env-container"></ol>'+
                    '<div class="node-input-env-locales-row"><i class="fa fa-language"></i> <select id="subflow-input-env-locale"></select></div>'+
                '</div>'+
            '</div>'+
            '<div id="subflow-env-tab-preview">'+
                '<div id="subflow-input-ui"/>'+
            '</div>'+
        '</div>'+
        '</script>';

    function findAvailableSubflowIOPosition(subflow,isInput) {
        const scrollPos = RED.view.scroll()
        const scaleFactor = RED.view.scale()
        var pos = { x: (scrollPos[0]/scaleFactor)+50, y: (scrollPos[1]/scaleFactor)+30 };
        if (!isInput) {
            pos.x += 110;
        }
        var ports = [].concat(subflow.out).concat(subflow.in);
        if (subflow.status) {
            ports.push(subflow.status);
        }
        ports.sort(function(A,B) {
            return A.x-B.x;
        });
        for (var i=0; i<ports.length; i++) {
            var port = ports[i];
            if (port.x == pos.x && port.y == pos.y) {
                pos.x += 55;
            }
        }
        return pos;
    }

    function addSubflowInput() {
        var subflow = RED.nodes.subflow(RED.workspaces.active());
        if (subflow.in.length === 1) {
            return;
        }
        var position = findAvailableSubflowIOPosition(subflow,true);
        var newInput = {
            type:"subflow",
            direction:"in",
            z:subflow.id,
            i:subflow.in.length,
            x:position.x,
            y:position.y,
            id:RED.nodes.id()
        };
        var oldInCount = subflow.in.length;
        subflow.in.push(newInput);
        subflow.dirty = true;
        var wasDirty = RED.nodes.dirty();
        var wasChanged = subflow.changed;
        subflow.changed = true;
        var result = refresh(true);
        var historyEvent = {
            t:'edit',
            node:subflow,
            dirty:wasDirty,
            changed:wasChanged,
            subflow: {
                inputCount: oldInCount,
                instances: result.instances
            }
        };
        RED.history.push(historyEvent);
        RED.view.select();
        RED.nodes.dirty(true);
        RED.view.redraw();
        $("#red-ui-subflow-input-add").addClass("active");
        $("#red-ui-subflow-input-remove").removeClass("active");
        RED.events.emit("subflows:change",subflow);
    }

    function removeSubflowInput() {
        var activeSubflow = RED.nodes.subflow(RED.workspaces.active());
        if (activeSubflow.in.length === 0) {
            return;
        }
        var removedInput = activeSubflow.in[0];
        var removedInputLinks = [];
        RED.nodes.eachLink(function(l) {
            if (l.source.type == "subflow" && l.source.z == activeSubflow.id && l.source.i == removedInput.i) {
                removedInputLinks.push(l);
            } else if (l.target.type == "subflow:"+activeSubflow.id) {
                removedInputLinks.push(l);
            }
        });
        removedInputLinks.forEach(function(l) { RED.nodes.removeLink(l)});
        activeSubflow.in = [];
        $("#red-ui-subflow-input-add").removeClass("active");
        $("#red-ui-subflow-input-remove").addClass("active");
        activeSubflow.changed = true;
        RED.events.emit("subflows:change",activeSubflow);
        return {subflowInputs: [ removedInput ], links:removedInputLinks};
    }

    function addSubflowOutput(id) {
        var subflow = RED.nodes.subflow(RED.workspaces.active());
        var position = findAvailableSubflowIOPosition(subflow,false);

        var newOutput = {
            type:"subflow",
            direction:"out",
            z:subflow.id,
            i:subflow.out.length,
            x:position.x,
            y:position.y,
            id:RED.nodes.id()
        };
        var oldOutCount = subflow.out.length;
        subflow.out.push(newOutput);
        subflow.dirty = true;
        var wasDirty = RED.nodes.dirty();
        var wasChanged = subflow.changed;
        subflow.changed = true;

        var result = refresh(true);

        var historyEvent = {
            t:'edit',
            node:subflow,
            dirty:wasDirty,
            changed:wasChanged,
            subflow: {
                outputCount: oldOutCount,
                instances: result.instances
            }
        };
        RED.history.push(historyEvent);
        RED.view.select();
        RED.nodes.dirty(true);
        RED.view.redraw();
        $("#red-ui-subflow-output .spinner-value").text(subflow.out.length);
        RED.events.emit("subflows:change",subflow);
    }

    function removeSubflowOutput(removedSubflowOutputs) {
        var activeSubflow = RED.nodes.subflow(RED.workspaces.active());
        if (activeSubflow.out.length === 0) {
            return;
        }
        if (typeof removedSubflowOutputs === "undefined") {
            removedSubflowOutputs = [activeSubflow.out[activeSubflow.out.length-1]];
        }
        var removedLinks = [];
        removedSubflowOutputs.sort(function(a,b) { return b.i-a.i});
        for (i=0;i<removedSubflowOutputs.length;i++) {
            var output = removedSubflowOutputs[i];
            activeSubflow.out.splice(output.i,1);
            var subflowRemovedLinks = [];
            var subflowMovedLinks = [];
            RED.nodes.eachLink(function(l) {
                if (l.target.type == "subflow" && l.target.z == activeSubflow.id && l.target.i == output.i) {
                    subflowRemovedLinks.push(l);
                }
                if (l.source.type == "subflow:"+activeSubflow.id) {
                    if (l.sourcePort == output.i) {
                        subflowRemovedLinks.push(l);
                    } else if (l.sourcePort > output.i) {
                        subflowMovedLinks.push(l);
                    }
                }
            });
            subflowRemovedLinks.forEach(function(l) { RED.nodes.removeLink(l)});
            subflowMovedLinks.forEach(function(l) { l.sourcePort--; });

            removedLinks = removedLinks.concat(subflowRemovedLinks);
            for (var j=output.i;j<activeSubflow.out.length;j++) {
                activeSubflow.out[j].i--;
                activeSubflow.out[j].dirty = true;
            }
        }
        activeSubflow.changed = true;
        RED.events.emit("subflows:change",activeSubflow);
        return {subflowOutputs: removedSubflowOutputs, links: removedLinks}
    }

    function addSubflowStatus() {
        var subflow = RED.nodes.subflow(RED.workspaces.active());
        if (subflow.status) {
            return;
        }
        var position = findAvailableSubflowIOPosition(subflow,false);
        var statusNode = {
            type:"subflow",
            direction:"status",
            z:subflow.id,
            x:position.x,
            y:position.y,
            id:RED.nodes.id()
        };
        subflow.status = statusNode;
        subflow.dirty = true;
        var wasDirty = RED.nodes.dirty();
        var wasChanged = subflow.changed;
        subflow.changed = true;
        var result = refresh(true);
        var historyEvent = {
            t:'edit',
            node:subflow,
            dirty:wasDirty,
            changed:wasChanged,
            subflow: { status: true }
        };
        RED.history.push(historyEvent);
        RED.view.select();
        RED.nodes.dirty(true);
        RED.view.redraw();
        RED.events.emit("subflows:change",subflow);
        $("#red-ui-subflow-status").prop("checked",!!subflow.status);
        $("#red-ui-subflow-status").parent().parent().toggleClass("active",!!subflow.status);
    }

    function removeSubflowStatus() {
        var subflow = RED.nodes.subflow(RED.workspaces.active());
        if (!subflow.status) {
            return;
        }
        var subflowRemovedLinks = [];
        RED.nodes.eachLink(function(l) {
            if (l.target.type == "subflow" && l.target.z == subflow.id && l.target.direction == "status") {
                subflowRemovedLinks.push(l);
            }
        });
        subflowRemovedLinks.forEach(function(l) { RED.nodes.removeLink(l)});
        delete subflow.status;

        $("#red-ui-subflow-status").prop("checked",!!subflow.status);
        $("#red-ui-subflow-status").parent().parent().toggleClass("active",!!subflow.status);

        return { links: subflowRemovedLinks }
    }

    function refresh(markChange) {
        var activeSubflow = RED.nodes.subflow(RED.workspaces.active());
        refreshToolbar(activeSubflow);
        var subflowInstances = [];
        if (activeSubflow) {
            RED.nodes.filterNodes({type:"subflow:"+activeSubflow.id}).forEach(function(n) {
                const parentFlow = RED.nodes.workspace(n.z)
                const wasLocked = parentFlow && parentFlow.locked
                if (wasLocked) {
                    parentFlow.locked = false
                }
                subflowInstances.push({
                    id: n.id,
                    changed: n.changed
                });
                if (markChange) {
                    n.changed = true;
                }
                n.inputs = activeSubflow.in.length;
                n.outputs = activeSubflow.out.length;
                n.resize = true;
                n.dirty = true;
                RED.editor.updateNodeProperties(n);
                if (wasLocked) {
                    parentFlow.locked = true
                }
            });
            RED.editor.validateNode(activeSubflow);
            return {
                instances: subflowInstances
            }
        }
    }

    function refreshToolbar(activeSubflow) {
        if (activeSubflow) {
            $("#red-ui-subflow-input-add").toggleClass("active", activeSubflow.in.length !== 0);
            $("#red-ui-subflow-input-remove").toggleClass("active",activeSubflow.in.length === 0);

            $("#red-ui-subflow-output .spinner-value").text(activeSubflow.out.length);

            $("#red-ui-subflow-status").prop("checked",!!activeSubflow.status);
            $("#red-ui-subflow-status").parent().parent().toggleClass("active",!!activeSubflow.status);

        }
    }

    function showWorkspaceToolbar(activeSubflow) {
        var toolbar = $("#red-ui-workspace-toolbar");
        toolbar.empty();

        // Edit properties
        $('<a class="button" id="red-ui-subflow-edit" href="#" data-i18n="[append]subflow.editSubflowProperties"><i class="fa fa-pencil"></i> </a>').appendTo(toolbar);

        // Inputs
        $('<span style="margin-left: 5px;" data-i18n="subflow.input"></span> '+
            '<div style="display: inline-block;" class="button-group">'+
            '<a id="red-ui-subflow-input-remove" class="button active" href="#">0</a>'+
            '<a id="red-ui-subflow-input-add" class="button" href="#">1</a>'+
            '</div>').appendTo(toolbar);

        // Outputs
        $('<span style="margin-left: 5px;" data-i18n="subflow.output"></span> <div id="red-ui-subflow-output" style="display: inline-block;" class="button-group spinner-group">'+
            '<a id="red-ui-subflow-output-remove" class="button" href="#"><i class="fa fa-minus"></i></a>'+
            '<div class="spinner-value">3</div>'+
            '<a id="red-ui-subflow-output-add" class="button" href="#"><i class="fa fa-plus"></i></a>'+
            '</div>').appendTo(toolbar);

        // Status
        $('<span class="button-group"><span class="button" style="padding:0"><label for="red-ui-subflow-status"><input id="red-ui-subflow-status" type="checkbox"> <span data-i18n="subflow.status"></span></label></span></span>').appendTo(toolbar);

        // $('<a class="button disabled" id="red-ui-subflow-add-input" href="#" data-i18n="[append]subflow.input"><i class="fa fa-plus"></i> </a>').appendTo(toolbar);
        // $('<a class="button" id="red-ui-subflow-add-output" href="#" data-i18n="[append]subflow.output"><i class="fa fa-plus"></i> </a>').appendTo(toolbar);

        // Delete
        $('<a class="button" id="red-ui-subflow-delete" href="#" data-i18n="[append]subflow.deleteSubflow"><i class="fa fa-trash"></i> </a>').appendTo(toolbar);

        toolbar.i18n();


        $("#red-ui-subflow-output-remove").on("click", function(event) {
            event.preventDefault();
            var wasDirty = RED.nodes.dirty();
            var wasChanged = activeSubflow.changed;
            var result = removeSubflowOutput();
            if (result) {
                var inst = refresh(true);
                RED.history.push({
                    t:'delete',
                    links:result.links,
                    subflowOutputs: result.subflowOutputs,
                    changed: wasChanged,
                    dirty:wasDirty,
                    subflow: {
                        instances: inst.instances
                    }
                });

                RED.view.select();
                RED.nodes.dirty(true);
                RED.view.redraw(true);
            }
        });

        $("#red-ui-subflow-output-add").on("click", function(event) {
            event.preventDefault();
            addSubflowOutput();
        });

        $("#red-ui-subflow-input-add").on("click", function(event) {
            event.preventDefault();
            addSubflowInput();
        });

        $("#red-ui-subflow-input-remove").on("click", function(event) {
            event.preventDefault();
            var wasDirty = RED.nodes.dirty();
            var wasChanged = activeSubflow.changed;
            activeSubflow.changed = true;
            var result = removeSubflowInput();
            if (result) {
                var inst = refresh(true);
                RED.history.push({
                    t:'delete',
                    links:result.links,
                    changed: wasChanged,
                    subflowInputs: result.subflowInputs,
                    dirty:wasDirty,
                    subflow: {
                        instances: inst.instances
                    }
                });
                RED.view.select();
                RED.nodes.dirty(true);
                RED.view.redraw(true);
            }
        });

        $("#red-ui-subflow-status").on("change", function(evt) {
            if (this.checked) {
                addSubflowStatus();
            } else {
                var currentStatus = activeSubflow.status;
                var wasChanged = activeSubflow.changed;
                var result = removeSubflowStatus();
                if (result) {
                    activeSubflow.changed = true;
                    var wasDirty = RED.nodes.dirty();
                    RED.history.push({
                        t:'delete',
                        links:result.links,
                        changed: wasChanged,
                        dirty:wasDirty,
                        subflow: {
                            id: activeSubflow.id,
                            status: currentStatus
                        }
                    });
                    RED.view.select();
                    RED.nodes.dirty(true);
                    RED.view.redraw();
                }
            }
        })

        $("#red-ui-subflow-edit").on("click", function(event) {
            RED.editor.editSubflow(RED.nodes.subflow(RED.workspaces.active()));
            event.preventDefault();
        });

        $("#red-ui-subflow-delete").on("click", function(event) {
            event.preventDefault();
            RED.subflow.delete(RED.workspaces.active())
        });

        refreshToolbar(activeSubflow);

        $("#red-ui-workspace-chart").css({"margin-top": "40px"});
        $("#red-ui-workspace-toolbar").show();
    }

    function hideWorkspaceToolbar() {
        $("#red-ui-workspace-toolbar").hide().empty();
        $("#red-ui-workspace-chart").css({"margin-top": "0"});
    }
    function deleteSubflow(id) {
        const subflow = RED.nodes.subflow(id || RED.workspaces.active());
        if (!subflow) {
            return
        }
        if (subflow.instances.length > 0) {
            if (subflow.instances.some(sf => { const ws = RED.nodes.workspace(sf.z); return ws?ws.locked:false })) {
                return
            }
            const msg = $('<div>')
            $('<p>').text(RED._("subflow.subflowInstances",{count: subflow.instances.length})).appendTo(msg);
            $('<p>').text(RED._("subflow.confirmDelete")).appendTo(msg);
            const confirmDeleteNotification = RED.notify(msg, {
                modal: true,
                fixed: true,
                buttons: [
                    {
                        text: RED._('common.label.cancel'),
                        click: function() {
                            confirmDeleteNotification.close();
                        }
                    },
                    {
                        text: RED._('workspace.confirmDelete'),
                        class: "primary",
                        click: function() {
                            confirmDeleteNotification.close();
                            completeDelete();
                        }
                    }
                ]
            });

            return;
        } else {
            completeDelete();
        }
        function completeDelete() {
            const startDirty = RED.nodes.dirty();
            const historyEvent = removeSubflow(subflow.id);
            historyEvent.t = 'delete';
            historyEvent.dirty = startDirty;
            RED.history.push(historyEvent);
        }
    }
    function removeSubflow(id, keepInstanceNodes) {
        // TODO:  A lot of this logic is common with RED.nodes.removeWorkspace
        var removedNodes = [];
        var removedLinks = [];
        var removedGroups = [];

        var activeSubflow = RED.nodes.subflow(id);

        RED.nodes.eachNode(function(n) {
            if (!keepInstanceNodes && n.type == "subflow:"+id) {
                removedNodes.push(n);
            }
            if (n.z == id) {
                removedNodes.push(n);
            }
        });
        RED.nodes.eachConfig(function(n) {
            if (n.z == id) {
                removedNodes.push(n);
            }
        });
        RED.nodes.groups(id).forEach(function(n) {
            removedGroups.push(n);
        })

        var removedJunctions = RED.nodes.junctions(id)
        for (var i=0;i<removedJunctions.length;i++) {
            var removedEntities = RED.nodes.removeJunction(removedJunctions[i])
            removedLinks = removedLinks.concat(removedEntities.links)
        }

        var removedConfigNodes = [];
        for (var i=0;i<removedNodes.length;i++) {
            var removedEntities = RED.nodes.remove(removedNodes[i].id);
            removedLinks = removedLinks.concat(removedEntities.links);
            removedConfigNodes = removedConfigNodes.concat(removedEntities.nodes);
        }
        // TODO: this whole delete logic should be in RED.nodes.removeSubflow..
        removedNodes = removedNodes.concat(removedConfigNodes);

        removedGroups = RED.nodes.groups(id).filter(function(g) { return !g.g; });
        for (i=0;i<removedGroups.length;i++) {
            removedGroups[i].nodes.forEach(function(n) {
                if (n.type === "group") {
                    removedGroups.push(n);
                }
            });
        }
        // Now remove them in the reverse order
        for (i=removedGroups.length-1; i>=0; i--) {
            RED.nodes.removeGroup(removedGroups[i]);
        }
        RED.nodes.removeSubflow(activeSubflow);
        RED.workspaces.remove(activeSubflow);
        RED.nodes.dirty(true);
        RED.view.redraw();

        return {
            nodes:removedNodes,
            links:removedLinks,
            groups: removedGroups,
            junctions: removedJunctions,
            subflows: [activeSubflow]
        }
    }

    function init() {
        RED.events.on("workspace:change",function(event) {
            var activeSubflow = RED.nodes.subflow(event.workspace);
            if (activeSubflow) {
                showWorkspaceToolbar(activeSubflow);
            } else {
                hideWorkspaceToolbar();
            }
        });
        RED.events.on("view:selection-changed",function(selection) {
            if (!selection.nodes || RED.workspaces.isLocked()) {
                RED.menu.setDisabled("menu-item-subflow-convert",true);
            } else {
                RED.menu.setDisabled("menu-item-subflow-convert",false);
            }
        });

        RED.actions.add("core:create-subflow",createSubflow);
        RED.actions.add("core:convert-to-subflow",convertToSubflow);

        $(_subflowEditTemplate).appendTo("#red-ui-editor-node-configs");
        $(_subflowTemplateEditTemplate).appendTo("#red-ui-editor-node-configs");

    }

    function createSubflow() {
        var lastIndex = 0;
        RED.nodes.eachSubflow(function(sf) {
           var m = (new RegExp("^Subflow (\\d+)$")).exec(sf.name);
           if (m) {
               lastIndex = Math.max(lastIndex,m[1]);
           }
        });

        var name = "Subflow "+(lastIndex+1);

        var subflowId = RED.nodes.id();
        var subflow = {
            type:"subflow",
            id:subflowId,
            name:name,
            info:"",
            in: [],
            out: []
        };
        RED.nodes.addSubflow(subflow);
        RED.history.push({
            t:'createSubflow',
            subflow: {
                subflow:subflow
            },
            dirty:RED.nodes.dirty()
        });
        RED.workspaces.show(subflowId);
        RED.nodes.dirty(true);
    }

    function snapToGrid(x) {
        if (RED.settings.get("editor").view['view-snap-grid']) {
            x = Math.round(x / RED.view.gridSize()) * RED.view.gridSize();
        }
        return x;
    }

    function nodeOrJunction(id) {
        var node = RED.nodes.node(id);
        if (node) {
            return node;
        }
        return RED.nodes.junction(id);
    }

    function convertToSubflow() {
        if (RED.workspaces.isLocked()) {
            return
        }
        var selection = RED.view.selection();
        if (!selection.nodes) {
            RED.notify(RED._("subflow.errors.noNodesSelected"),"error");
            return;
        }
        var i,n;
        var nodeList = new Set();
        var tmplist = selection.nodes.slice();
        var includedGroups = new Set();
        while(tmplist.length > 0) {
            n = tmplist.shift();
            if (n.type === "group") {
                includedGroups.add(n.id);
                tmplist = tmplist.concat(n.nodes);
            }
            nodeList.add(n);
        }

        nodeList = Array.from(nodeList);

        var containingGroup = nodeList[0].g;
        var nodesMovedFromGroup = [];

        for (i=0; i<nodeList.length;i++) {
            if (nodeList[i].g && !includedGroups.has(nodeList[i].g)) {
                if (containingGroup !== nodeList[i].g) {
                    RED.notify(RED._("subflow.errors.acrossMultipleGroups"), "error");
                    return;
                }
            }
        }
        if (containingGroup) {
            containingGroup = RED.nodes.group(containingGroup);
        }
        var nodes = {};
        var new_links = [];
        var removedLinks = [];

        var candidateInputs = [];
        var candidateOutputs = [];
        var candidateInputNodes = {};

        var boundingBox = [nodeList[0].x-(nodeList[0].w/2),
            nodeList[0].y-(nodeList[0].h/2),
            nodeList[0].x+(nodeList[0].w/2),
            nodeList[0].y+(nodeList[0].h/2)];

        for (i=0;i<nodeList.length;i++) {
            n = nodeList[i];
            nodes[n.id] = {n:n,outputs:{}};
            boundingBox = [
                Math.min(boundingBox[0],n.x-(n.w/2)),
                Math.min(boundingBox[1],n.y-(n.h/2)),
                Math.max(boundingBox[2],n.x+(n.w/2)),
                Math.max(boundingBox[3],n.y+(n.h/2))
            ]
        }
        var offsetX = snapToGrid(boundingBox[0] - 140);
        var offsetY = snapToGrid(boundingBox[1] - 60);

        var center = [
            snapToGrid((boundingBox[2]+boundingBox[0]) / 2),
            snapToGrid((boundingBox[3]+boundingBox[1]) / 2)
        ];

        RED.nodes.eachLink(function(link) {
            if (nodes[link.source.id] && nodes[link.target.id]) {
                // A link wholely within the selection
            }

            if (nodes[link.source.id] && !nodes[link.target.id]) {
                // An outbound link from the selection
                candidateOutputs.push(link);
                removedLinks.push(link);
            }
            if (!nodes[link.source.id] && nodes[link.target.id]) {
                // An inbound link
                candidateInputs.push(link);
                candidateInputNodes[link.target.id] = link.target;
                removedLinks.push(link);
            }
        });

        var outputs = {};
        candidateOutputs = candidateOutputs.filter(function(v) {
             if (outputs[v.source.id+":"+v.sourcePort]) {
                 outputs[v.source.id+":"+v.sourcePort].targets.push(v.target);
                 return false;
             }
             v.targets = [];
             v.targets.push(v.target);
             outputs[v.source.id+":"+v.sourcePort] = v;
             return true;
        });
        candidateOutputs.sort(function(a,b) { return a.source.y-b.source.y});

        if (Object.keys(candidateInputNodes).length > 1) {
             RED.notify(RED._("subflow.errors.multipleInputsToSelection"),"error");
             return;
        }

        var lastIndex = 0;
        RED.nodes.eachSubflow(function(sf) {
           var m = (new RegExp("^Subflow (\\d+)$")).exec(sf.name);
           if (m) {
               lastIndex = Math.max(lastIndex,m[1]);
           }
        });

        var name = "Subflow "+(lastIndex+1);

        var subflowId = RED.nodes.id();
        var subflow = {
            type:"subflow",
            id:subflowId,
            name:name,
            info:"",
            in: Object.keys(candidateInputNodes).map(function(v,i) { var index = i; return {
                type:"subflow",
                direction:"in",
                x:snapToGrid(candidateInputNodes[v].x-(candidateInputNodes[v].w/2)-80 - offsetX),
                y:snapToGrid(candidateInputNodes[v].y - offsetY),
                z:subflowId,
                i:index,
                id:RED.nodes.id(),
                wires:[{id:candidateInputNodes[v].id}]
            }}),
            out: candidateOutputs.map(function(v,i) { var index = i; return {
                type:"subflow",
                direction:"out",
                x:snapToGrid(v.source.x+(v.source.w/2)+80 - offsetX),
                y:snapToGrid(v.source.y - offsetY),
                z:subflowId,
                i:index,
                id:RED.nodes.id(),
                wires:[{id:v.source.id,port:v.sourcePort}]
            }})
        };

        RED.nodes.addSubflow(subflow);

        var subflowInstance = {
            id:RED.nodes.id(),
            type:"subflow:"+subflow.id,
            x: center[0],
            y: center[1],
            z: RED.workspaces.active(),
            inputs: subflow.in.length,
            outputs: subflow.out.length,
            h: Math.max(30/*node_height*/,(subflow.out.length||0) * 15),
            changed:true
        }
        subflowInstance._def = RED.nodes.getType(subflowInstance.type);
        RED.editor.validateNode(subflowInstance);
        subflowInstance = RED.nodes.add(subflowInstance);

        if (containingGroup) {
            RED.group.addToGroup(containingGroup, subflowInstance);
            nodeList.forEach(function(nl) {
                if (nl.g === containingGroup.id) {
                    delete nl.g;
                    var index = containingGroup.nodes.indexOf(nl);
                    containingGroup.nodes.splice(index,1);
                    nodesMovedFromGroup.push(nl);
                }
            })
            containingGroup.dirty = true;
        }


        candidateInputs.forEach(function(l) {
            var link = {source:l.source, sourcePort:l.sourcePort, target: subflowInstance};
            new_links.push(link);
            RED.nodes.addLink(link);
        });

        candidateOutputs.forEach(function(output,i) {
            output.targets.forEach(function(target) {
                var link = {source:subflowInstance, sourcePort:i, target: target};
                new_links.push(link);
                RED.nodes.addLink(link);
            });
        });

        subflow.in.forEach(function(input) {
            input.wires.forEach(function(wire) {
                var link = {source: input, sourcePort: 0, target: nodeOrJunction(wire.id) }
                new_links.push(link);
                RED.nodes.addLink(link);
            });
        });

        subflow.out.forEach(function(output,i) {
            output.wires.forEach(function(wire) {
                var link = {source: nodeOrJunction(wire.id), sourcePort: wire.port , target: output }
                new_links.push(link);
                RED.nodes.addLink(link);
            });
        });

        for (i=0;i<removedLinks.length;i++) {
            RED.nodes.removeLink(removedLinks[i]);
        }

        for (i=0;i<nodeList.length;i++) {
            n = nodeList[i];
            if (/^link /.test(n.type)) {
                n.links = n.links.filter(function(id) {
                    var isLocalLink = nodes.hasOwnProperty(id);
                    if (!isLocalLink) {
                        var otherNode = nodeOrJunction(id);
                        if (otherNode && otherNode.links) {
                            var i = otherNode.links.indexOf(n.id);
                            if (i > -1) {
                                otherNode.links.splice(i,1);
                            }
                        }
                    }
                    return isLocalLink;
                });
            }
            n.x -= offsetX;
            n.y -= offsetY;
            RED.nodes.moveNodeToTab(n, subflow.id);
        }

        var historyEvent = {
            t:'createSubflow',
            nodes:[subflowInstance.id],
            links:new_links,
            subflow: {
                subflow: subflow,
                offsetX: offsetX,
                offsetY: offsetY
            },

            activeWorkspace: RED.workspaces.active(),
            removedLinks: removedLinks,

            dirty:RED.nodes.dirty()
        }
        if (containingGroup) {
            historyEvent = {
                t:'multi',
                events: [ historyEvent ]
            }
            historyEvent.events.push({
                t:'addToGroup',
                group: containingGroup,
                nodes: [subflowInstance]
            })
            historyEvent.events.push({
                t:'removeFromGroup',
                group: containingGroup,
                nodes: nodesMovedFromGroup,
                reparent: false
            })
        }
        RED.history.push(historyEvent);
        RED.editor.validateNode(subflow);
        RED.nodes.dirty(true);
        RED.view.updateActive();
        RED.view.select(null);
        RED.view.focus();
    }


    /**
     * Build the edit dialog for a subflow template (creating/modifying a subflow template)
     * @param {Object} uiContainer - the jQuery container for the environment variable list
     * @param {Object} node - the subflow template node
     */
    function buildEnvControl(uiContainer,node) {
        var tabs = RED.tabs.create({
            id: "subflow-env-tabs",
            onchange: function(tab) {
                if (tab.id === "subflow-env-tab-preview") {
                    var inputContainer = $("#subflow-input-ui");
                    var list = uiContainer.editableList("items");
                    var exportedEnv = exportEnvList(list, true);
                    buildEnvUI(inputContainer, exportedEnv, node);
                }
                $("#subflow-env-tabs-content").children().hide();
                $("#" + tab.id).show();
            }
        });
        tabs.addTab({
            id: "subflow-env-tab-edit",
            label: RED._("editor-tab.envProperties")
        });
        tabs.addTab({
            id: "subflow-env-tab-preview",
            label:  RED._("editor-tab.preview")
        });

        var localesList = RED.settings.theme("languages")
            .map(function(lc) { var name = RED._("languages."+lc); return {text: (name ? name : lc), val: lc}; })
            .sort(function(a, b) { return a.text.localeCompare(b.text) });
        RED.popover.tooltip($(".node-input-env-locales-row i"),RED._("editor.locale"))
        var locales = $("#subflow-input-env-locale")
        localesList.forEach(function(item) {
            var opt = {
                value: item.val
            };
            if (item.val === "en-US") { // make en-US default selected
                opt.selected = "";
            }
            $("<option/>", opt).text(item.text).appendTo(locales);
        });
        var locale = RED.i18n.lang();
        locales.val(locale);

        locales.on("change", function() {
            RED.editor.envVarList.setLocale($(this).val(), $("#node-input-env-container"));
        });
        RED.editor.envVarList.setLocale(locale);
    }

    /**
     * Build a UI row for a subflow instance environment variable
     * Also used to build the UI row for subflow template preview
     * @param {JQuery} row - A form row element
     * @param {Object} tenv - A template environment variable
     * @param {String} tenv.name - The name of the environment variable
     * @param {String} tenv.type - The type of the environment variable
     * @param {String} tenv.value - The value set for this environment variable
     * @param {Object} tenv.parent - The parent environment variable
     * @param {String} tenv.parent.value - The value set for the parent environment variable
     * @param {String} tenv.parent.type - The type of the parent environment variable
     * @param {Object} tenv.ui - The UI configuration for the environment variable
     * @param {String} tenv.ui.icon - The icon for the environment variable
     * @param {Object} tenv.ui.label - The label for the environment variable
     * @param {String} tenv.ui.type - The type of the UI control for the environment variable
     * @param {Object} node - The subflow instance node
     */
    function buildEnvUIRow(row, tenv, node) {
        if(RED.subflow.debug) { console.log("buildEnvUIRow", tenv) }
        const ui = tenv.ui || {}
        ui.label = ui.label||{};
        if ((tenv.type === "cred" || (tenv.parent && tenv.parent.type === "cred")) && !ui.type) {
            ui.type = "cred";
            ui.opts = {};
        } else if (tenv.type === "conf-types") {
            ui.type = "conf-types"
            ui.opts = { types: ['conf-types'] }
        } else if (!ui.type) {
            ui.type = "input";
            ui.opts = { types: RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST }
        } else {
            if (!ui.opts) {
                ui.opts = (ui.type === "select") ? {opts:[]} : {};
            }
        }

        var labels = ui.label || {};
        var locale = RED.i18n.lang();
        var labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"]||tenv.name, locale);
        var label = $('<label>').appendTo(row);
        $('<span>&nbsp;</span>').appendTo(row);
        var labelContainer = $('<span></span>').appendTo(label);
        if (ui.icon) {
            var newPath = RED.utils.separateIconPath(ui.icon);
            if (newPath) {
                $("<i class='fa "+newPath.file +"'/>").appendTo(labelContainer);
            }
        }
        if (ui.type !== "checkbox") {
            var css = ui.icon ? {"padding-left":"5px"} : {};
            $('<span>').css(css).text(labelText).appendTo(label);
            if (ui.type === 'none') {
                label.width('100%');
            }
        }
        var input;
        var val = {
            value: "",
            type: "str"
        };
        if (tenv.parent) {
            val.value = tenv.parent.value;
            val.type = tenv.parent.type;
        }
        if (tenv.hasOwnProperty('value')) {
            val.value = tenv.value;
        }
        if (tenv.hasOwnProperty('type')) {
            val.type = tenv.type;
        }
        const elId = getSubflowEnvPropertyName(tenv.name)
        switch(ui.type) {
            case "input":
                input = $('<input type="text">').css('width','70%').attr('id', elId).appendTo(row);
                if (ui.opts.types && ui.opts.types.length > 0) {
                    var inputType = val.type;
                    if (ui.opts.types.indexOf(inputType) === -1) {
                        inputType = ui.opts.types[0]
                    }
                    input.typedInput({
                        types: ui.opts.types,
                        default: inputType
                    })
                    input.typedInput('value',val.value)
                    if (inputType === 'cred') {
                        if (node.credentials) {
                            if (node.credentials[tenv.name]) {
                                input.typedInput('value', node.credentials[tenv.name]);
                            } else if (node.credentials['has_'+tenv.name]) {
                                input.typedInput('value', "__PWRD__")
                            } else {
                                input.typedInput('value', "");
                            }
                        }
                    }
                } else {
                    input.val(val.value)
                }
                break;
            case "select":
                input = $('<select>').css('width','70%').attr('id', elId).appendTo(row);
                if (ui.opts.opts) {
                    ui.opts.opts.forEach(function(o) {
                        $('<option>').val(o.v).text(RED.editor.envVarList.lookupLabel(o.l, o.l['en-US']||o.v, locale)).appendTo(input);
                    })
                }
                input.val(val.value);
                break;
            case "checkbox":
                label.css("cursor","default");
                var cblabel = $('<label>').css('width','70%').appendTo(row);
                input = $('<input type="checkbox">').attr('id', elId).css({
                    marginTop: 0,
                    width: 'auto',
                    height: '34px'
                }).appendTo(cblabel);
                labelContainer.css({"padding-left":"5px"}).appendTo(cblabel);
                $('<span>').css({"padding-left":"5px"}).text(labelText).appendTo(cblabel);
                var boolVal = false;
                if (val.type === 'bool') {
                    boolVal = val.value === 'true'
                } else if (val.type === 'num') {
                    boolVal = val.value !== "0"
                } else {
                    boolVal = val.value !== ""
                }
                input.prop("checked",boolVal);
                break;
            case "spinner":
                input = $('<input>').css('width','70%').attr('id', elId).appendTo(row);
                var spinnerOpts = {};
                if (ui.opts.hasOwnProperty('min')) {
                    spinnerOpts.min = ui.opts.min;
                }
                if (ui.opts.hasOwnProperty('max')) {
                    spinnerOpts.max = ui.opts.max;
                }
                input.spinner(spinnerOpts).parent().width('70%');
                input.val(val.value);
                break;
            case "cred":
                input = $('<input type="password">').css('width','70%').attr('id', elId).appendTo(row);
                if (node.credentials) {
                    if (node.credentials[tenv.name]) {
                        input.val(node.credentials[tenv.name]);
                    } else if (node.credentials['has_'+tenv.name]) {
                        input.val("__PWRD__")
                    } else {
                        input.val("");
                    }
                } else {
                    input.val("");
                }
                input.typedInput({
                    types: ['cred'],
                    default: 'cred'
                })
                break;
            case "conf-types":
                // let clsId = 'config-node-input-' + val.type + '-' + val.value + '-' + Math.floor(Math.random() * 100000);
                // clsId = clsId.replace(/\W/g, '-');
                // input = $('<input>').css('width','70%').addClass(clsId).attr('id', elId).appendTo(row);
                input = $('<input>').css('width','70%').attr('id', elId).appendTo(row);
                const _type = tenv.parent?.type || tenv.type;
                RED.editor.prepareConfigNodeSelect(node, tenv.name, _type, 'node-input-subflow-env', null, tenv);
                break;
        }
    }

    /**
     * Build the edit form for a subflow instance
     * Also used to build the preview form in the subflow template edit dialog
     * @param uiContainer - container for UI
     * @param envList - env var definitions of template
     */
    function buildEnvUI(uiContainer, envList, node) {
        if(RED.subflow.debug) { console.log("buildEnvUI",envList) }
        uiContainer.empty();
        for (var i = 0; i < envList.length; i++) {
            var tenv = envList[i];
            if (tenv.ui && tenv.ui.type === 'hide') {
                continue;
            }
            var row = $("<div/>", { class: "form-row" }).appendTo(uiContainer);
            buildEnvUIRow(row, tenv, node);
        }
    }
    // buildEnvUI

    function exportEnvList(list, all) {
        if (list) {
            var env = [];
            list.each(function(i) {
                var entry = $(this);
                var item = entry.data('data');
                var name = (item.parent?item.name:item.nameField.val()).trim();
                if ((name !== "") ||
                    (item.ui && (item.ui.type === "none"))) {
                    var valueInput = item.valueField;
                    var value = valueInput.typedInput("value");
                    var type = valueInput.typedInput("type");
                    if (all || !item.parent || (item.parent.value !== value || item.parent.type !== type)) {
                        var envItem = {
                            name: name,
                            type: type,
                            value: value,
                        };
                        if (item.ui) {
                            var ui = {
                                icon: item.ui.icon,
                                label: $.extend(true,{},item.ui.label),
                                type: item.ui.type,
                                opts: $.extend(true,{},item.ui.opts)
                            }
                            // Check to see if this is the default ui definition.
                            // Delete any defaults to keep it compact
                            // {
                            //     icon: "",
                            //     label: {},
                            //     type: "input",
                            //     opts: {types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST}
                            // }
                            if (!ui.icon) {
                                delete ui.icon;
                            }
                            if ($.isEmptyObject(ui.label)) {
                                delete ui.label;
                            }
                            switch (ui.type) {
                                case "input":
                                    if (JSON.stringify(ui.opts) === JSON.stringify({types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST})) {
                                        // This is the default input config. Delete it as it will
                                        // be applied automatically
                                        delete ui.type;
                                        delete ui.opts;
                                    }
                                    break;
                                case "cred":
                                    if (envItem.type === "cred") {
                                        delete ui.type;
                                    }
                                    delete ui.opts;
                                    break;
                                case "select":
                                    if (ui.opts && $.isEmptyObject(ui.opts.opts)) {
                                        // This is the default select config.
                                        // Delete it as it will be applied automatically
                                        delete ui.opts;
                                    }
                                    break;
                                case "spinner":
                                    if ($.isEmptyObject(ui.opts)) {
                                        // This is the default spinner config.
                                        // Delete as it will be applied automatically
                                        delete ui.opts
                                    }
                                    break;
                                case "conf-types":
                                    delete ui.opts;
                                    break;
                                default:
                                    delete ui.opts;
                            }
                            if (!$.isEmptyObject(ui)) {
                                envItem.ui = ui;
                            }
                        }
                        env.push(envItem);
                    }
                }
            });
            return env;
        }
        return null;
    }

    function getSubflowInstanceParentEnv(node) {
        var parentEnv = {};
        var envList = [];
        if (/^subflow:/.test(node.type)) {
            var subflowDef = RED.nodes.subflow(node.type.substring(8));
            if (subflowDef.env) {
                subflowDef.env.forEach(function(env, i) {
                    var item = {
                        index: i,
                        name:env.name,
                        parent: {
                            type: env.type,
                            value: env.value
                        },
                        ui: $.extend(true,{},env.ui)
                    }
                    envList.push(item);
                    parentEnv[env.name] = item;
                })
            }
            if (node.env) {
                for (var i = 0; i < node.env.length; i++) {
                    var env = node.env[i];
                    if (parentEnv.hasOwnProperty(env.name)) {
                        parentEnv[env.name].type = env.type;
                        parentEnv[env.name].value = env.value;
                    } else {
                        // envList.push({
                        //     name: env.name,
                        //     type: env.type,
                        //     value: env.value,
                        // });
                    }
                }
            }
        } else if (node._def.subflowModule) {
            var keys = Object.keys(node._def.defaults);
            keys.forEach(function(name) {
                if (name !== 'name') {
                    var prop = node._def.defaults[name];
                    var nodeProp = node[name];
                    var nodePropType;
                    var nodePropValue = nodeProp;
                    if (prop.ui && prop.ui.type === "cred") {
                        nodePropType = "cred";
                    } else if (prop.ui && prop.ui.type === "conf-types") {
                        nodePropType = prop.value.type
                    } else {
                        switch(typeof nodeProp) {
                            case "string": nodePropType = "str"; break;
                            case "number": nodePropType = "num"; break;
                            case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break;
                            default:
                                if (nodeProp) {
                                    nodePropType = nodeProp.type;
                                    nodePropValue = nodeProp.value;
                                } else {
                                    nodePropType = 'str'
                                }
                        }
                    }
                    var item = {
                        name: name,
                        type: nodePropType,
                        value: nodePropValue,
                        parent: {
                            type: prop.type,
                            value: prop.value
                        },
                        ui: $.extend(true,{},prop.ui)
                    }
                    envList.push(item);
                }
            })
        }
        return envList;
    }

    function exportSubflowInstanceEnv(node) {
        if(RED.subflow.debug) { console.log("exportSubflowInstanceEnv",node) }
        var env = [];
        // First, get the values for the SubflowTemplate defined properties
        //  - these are the ones with custom UI elements
        var parentEnv = getSubflowInstanceParentEnv(node);
        parentEnv.forEach(function(data) {
            var item;
            var ui = data.ui || {};
            if (!ui.type) {
                if (data.parent && data.parent.type === "cred") {
                    ui.type = "cred";
                } else {
                    ui.type = "input";
                    ui.opts = {types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST}
                }
            } else {
                ui.opts = ui.opts || {};
            }
            var input = $("#"+getSubflowEnvPropertyName(data.name));
            if (input.length || ui.type === "cred") {
                item = { name: data.name };
                switch(ui.type) {
                    case "input":
                        if (ui.opts.types && ui.opts.types.length > 0) {
                            item.value = input.typedInput('value');
                            item.type = input.typedInput('type');
                        } else {
                            item.value = input.val();
                            item.type = 'str';
                        }
                        break;
                    case "cred":
                        item.value = input.typedInput('value');
                        item.type = 'cred';
                        break;
                    case "spinner":
                        item.value = input.val();
                        item.type = 'num';
                        break;
                    case "select":
                        item.value = input.val();
                        item.type = 'str';
                        break;
                    case "checkbox":
                        item.type = 'bool';
                        item.value = ""+input.prop("checked");
                        break;
                    case "conf-types":
                        item.value = input.val() === "_ADD_" ? "" : input.val();
                        item.type = "conf-type"
                }
                if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {
                    env.push(item);
                }
            }
        })
        return env;
    }

    function getSubflowEnvPropertyName(name) {
        return 'node-input-subflow-env-'+name.replace(/[^a-z0-9-_]/ig,"_");
    }

    
    /**
     * Build the subflow edit form
     * Called by subflow.oneditprepare for both instances and templates
     * @param {"subflow"|"subflow-template"} type - the type of subflow being edited
     * @param {Object} node - the node being edited
     */
    function buildEditForm(type,node) {
        if(RED.subflow.debug) { console.log("buildEditForm",type,node) }
        if (type === "subflow-template") {
            // This is the tabbed UI that offers the env list - with UI options
            // plus the preview tab
            buildEnvControl($('#node-input-env-container'), node);
            RED.editor.envVarList.create($('#node-input-env-container'), node);
        } else  if (type === "subflow") {
            // This is the rendered version of the subflow env var list
            buildEnvUI($("#subflow-input-ui"), getSubflowInstanceParentEnv(node), node);
        }
    }

    return {
        init: init,
        createSubflow: createSubflow,
        convertToSubflow: convertToSubflow,
        // removeSubflow: Internal function to remove subflow
        removeSubflow: removeSubflow,
        // delete: Prompt user for confirmation
        delete: deleteSubflow,
        refresh: refresh,
        removeInput: removeSubflowInput,
        removeOutput: removeSubflowOutput,
        removeStatus: removeSubflowStatus,

        buildEditForm: buildEditForm,

        exportSubflowTemplateEnv: exportEnvList,
        exportSubflowInstanceEnv: exportSubflowInstanceEnv
    }
})();
;/**
 * 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.
 **/

RED.group = (function() {

    var _groupEditTemplate = '<script type="text/x-red" data-template-name="group">'+
        '<div class="form-row">'+
            '<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>'+
            '<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">'+
        '</div>'+

        // '<div class="node-input-group-style-tools"><span class="button-group"><button class="red-ui-button red-ui-button-small">Use default style</button><button class="red-ui-button red-ui-button-small">Set as default style</button></span></div>'+

        '<div class="form-row" id="node-input-row-style-stroke">'+
            '<label data-i18n="editor:common.label.style"></label>'+
            '<label style="width: 70px;margin-right:10px" for="node-input-style-stroke" data-i18n="editor:common.label.line"></label>'+
        '</div>'+
        '<div class="form-row" style="padding-left: 100px;" id="node-input-row-style-fill">'+
            '<label style="width: 70px;margin-right: 10px "  for="node-input-style-fill" data-i18n="editor:common.label.fill"></label>'+
        '</div>'+
        '<div class="form-row">'+
            '<label for="node-input-style-label" data-i18n="editor:common.label.label"></label>'+
            '<input type="checkbox" id="node-input-style-label"/>'+
        '</div>'+
        '<div class="form-row" id="node-input-row-style-label-options">'+
            '<div style="margin-left: 100px; display: inline-block">'+
                '<div class="form-row">'+
                    '<span style="display: inline-block; min-width: 140px"  id="node-input-row-style-label-color">'+
                        '<label style="width: 70px;margin-right: 10px" for="node-input-style-fill" data-i18n="editor:common.label.color"></label>'+
                    '</span>'+
                '</div>'+
                '<div class="form-row">'+
                    '<span style="display: inline-block; min-width: 140px;" id="node-input-row-style-label-position">'+
                        '<label style="width: 70px;margin-right: 10px " for="node-input-style-label-position" data-i18n="editor:common.label.position"></label>'+
                    '</span>'+
                '</div>'+
            '</div>'+
        '</div>'+

        '</script>';

    var colorPalette = [
        "#ff0000",
        "#ffC000",
        "#ffff00",
        "#92d04f",
        "#0070c0",
        "#001f60",
        "#6f2fa0",
        "#000000",
        "#777777"
    ]
    var colorSteps = 3;
    var colorCount = colorPalette.length;
    for (var i=0,len=colorPalette.length*colorSteps;i<len;i++) {
        var ci = i%colorCount;
        var j = Math.floor(i/colorCount)+1;
        var c = colorPalette[ci];
        var r = parseInt(c.substring(1, 3), 16);
        var g = parseInt(c.substring(3, 5), 16);
        var b = parseInt(c.substring(5, 7), 16);
        var dr = (255-r)/(colorSteps+((ci===colorCount-1) ?0:1));
        var dg = (255-g)/(colorSteps+((ci===colorCount-1) ?0:1));
        var db = (255-b)/(colorSteps+((ci===colorCount-1) ?0:1));
        r = Math.min(255,Math.floor(r+j*dr));
        g = Math.min(255,Math.floor(g+j*dg));
        b = Math.min(255,Math.floor(b+j*db));
        var s = ((r<<16) + (g<<8) + b).toString(16);
        colorPalette.push('#'+'000000'.slice(0, 6-s.length)+s);
    }

    var defaultGroupStyle = {
        label: true,
        "label-position": "nw"
    };


    var groupDef = {
        defaults:{
            name:{value:""},
            style:{value:{label:true}},
            nodes:{value:[]},
            env: {value:[]},
        },
        category: "config",
        oneditprepare: function() {
            var style = this.style || {};
            RED.editor.colorPicker.create({
                id:"node-input-style-stroke",
                value: style.stroke || defaultGroupStyle.stroke || "#a4a4a4",
                defaultValue: "#a4a4a4",
                palette: colorPalette,
                cellPerRow: colorCount,
                cellWidth: 16,
                cellHeight: 16,
                cellMargin: 3,
                none: true,
                opacity: style.hasOwnProperty('stroke-opacity')?style['stroke-opacity']:(defaultGroupStyle.hasOwnProperty('stroke-opacity')?defaultGroupStyle['stroke-opacity']:1.0)
            }).appendTo("#node-input-row-style-stroke");
            RED.editor.colorPicker.create({
                id:"node-input-style-fill",
                value: style.fill || defaultGroupStyle.fill ||"none",
                defaultValue: "none",
                palette: colorPalette,
                cellPerRow: colorCount,
                cellWidth: 16,
                cellHeight: 16,
                cellMargin: 3,
                none: true,
                opacity: style.hasOwnProperty('fill-opacity')?style['fill-opacity']:(defaultGroupStyle.hasOwnProperty('fill-opacity')?defaultGroupStyle['fill-opacity']:1.0)
            }).appendTo("#node-input-row-style-fill");

            createLayoutPicker({
                id:"node-input-style-label-position",
                value:style["label-position"] || "nw"
            }).appendTo("#node-input-row-style-label-position");

            RED.editor.colorPicker.create({
                id:"node-input-style-color",
                value: style.color || defaultGroupStyle.color ||"#a4a4a4",
                defaultValue: "#a4a4a4",
                palette: colorPalette,
                cellPerRow: colorCount,
                cellWidth: 16,
                cellHeight: 16,
                cellMargin: 3
            }).appendTo("#node-input-row-style-label-color");

            $("#node-input-style-label").toggleButton({
                enabledLabel: RED._("editor.show"),
                disabledLabel: RED._("editor.show"),
            })

            $("#node-input-style-label").on("change", function(evt) {
                $("#node-input-row-style-label-options").toggle($(this).prop("checked"));
            })
            $("#node-input-style-label").prop("checked", this.style.label)
            $("#node-input-style-label").trigger("change");
        },
        oneditresize: function(size) {
        },
        oneditsave: function() {
            this.style.stroke = $("#node-input-style-stroke").val();
            this.style.fill = $("#node-input-style-fill").val();
            this.style["stroke-opacity"] = $("#node-input-style-stroke-opacity").val();
            this.style["fill-opacity"] = $("#node-input-style-fill-opacity").val();
            this.style.label = $("#node-input-style-label").prop("checked");
            if (this.style.label) {
                this.style["label-position"] = $("#node-input-style-label-position").val();
                this.style.color = $("#node-input-style-color").val();
            } else {
                delete this.style["label-position"];
                delete this.style.color;
            }

            var node = this;
            ['stroke','fill','stroke-opacity','fill-opacity','color','label-position'].forEach(function(prop) {
                if (node.style[prop] === defaultGroupStyle[prop]) {
                    delete node.style[prop]
                }
            })

            this.resize = true;
        },
        set:{
            module: "node-red"
        }
    }

    function init() {

        RED.events.on("view:selection-changed",function(selection) {
            var activateGroup = !!selection.nodes;
            var activateUngroup = false;
            var activateMerge = false;
            var activateRemove = false;
            var singleGroupSelected = false;
            var locked = RED.workspaces.isLocked()

            if (activateGroup) {
                singleGroupSelected = selection.nodes.length === 1 && selection.nodes[0].type === 'group';
                selection.nodes.forEach(function (n) {
                    if (n.type === "group") {
                        activateUngroup = true;
                    }
                    if (!!n.g) {
                        activateRemove = true;
                    }
                });
                if (activateUngroup) {
                    activateMerge = (selection.nodes.length > 1);
                }
            }
            RED.menu.setDisabled("menu-item-group-group", locked || !activateGroup);
            RED.menu.setDisabled("menu-item-group-ungroup", locked || !activateUngroup);
            RED.menu.setDisabled("menu-item-group-merge", locked || !activateMerge);
            RED.menu.setDisabled("menu-item-group-remove", locked || !activateRemove);
            RED.menu.setDisabled("menu-item-edit-copy-group-style", !singleGroupSelected);
            RED.menu.setDisabled("menu-item-edit-paste-group-style", locked || !activateUngroup);
        });

        RED.actions.add("core:group-selection", function() { groupSelection() })
        RED.actions.add("core:ungroup-selection", function() { ungroupSelection() })
        RED.actions.add("core:merge-selection-to-group", function() { mergeSelection() })
        RED.actions.add("core:remove-selection-from-group", function() { removeSelection() })
        RED.actions.add("core:copy-group-style", function() { copyGroupStyle() });
        RED.actions.add("core:paste-group-style", function() { pasteGroupStyle() });

        $(_groupEditTemplate).appendTo("#red-ui-editor-node-configs");

        var groupStyleDiv = $("<div>",{
            class:"red-ui-flow-group-body",
            style: "position: absolute; top: -1000px;"
        }).appendTo(document.body);
        var groupStyle = getComputedStyle(groupStyleDiv[0]);
        defaultGroupStyle = {
            stroke: convertColorToHex(groupStyle.stroke),
            "stroke-opacity": groupStyle.strokeOpacity,
            fill: convertColorToHex(groupStyle.fill),
            "fill-opacity": groupStyle.fillOpacity,
            label: true,
            "label-position": "nw"
        }
        groupStyleDiv.remove();
        groupStyleDiv = $("<div>",{
            class:"red-ui-flow-group-label",
            style: "position: absolute; top: -1000px;"
        }).appendTo(document.body);
        groupStyle = getComputedStyle(groupStyleDiv[0]);
        defaultGroupStyle.color = convertColorToHex(groupStyle.fill);
        groupStyleDiv.remove();
    }

    function convertColorToHex(c) {
        var m = /^rgb\((\d+), (\d+), (\d+)\)$/.exec(c);
        if (m) {
            var s = ((parseInt(m[1])<<16) + (parseInt(m[2])<<8) + parseInt(m[3])).toString(16)
            return '#'+'000000'.slice(0, 6-s.length)+s;
        }
        return c;
    }


    var groupStyleClipboard;

    function copyGroupStyle() {
        if (RED.view.state() !== RED.state.DEFAULT) { return }
        var selection = RED.view.selection();
        if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].type === 'group') {
            groupStyleClipboard = JSON.parse(JSON.stringify(selection.nodes[0].style));
            RED.notify(RED._("clipboard.groupStyleCopied"),{id:"clipboard"})
            RED.menu.setDisabled("menu-item-edit-paste-group-style", false)
        }
    }
    function pasteGroupStyle() {
        if (RED.workspaces.isLocked()) { return }
        if (RED.view.state() !== RED.state.DEFAULT) { return }
        if (groupStyleClipboard) {
            var selection = RED.view.selection();
            if (selection.nodes) {
                var historyEvent = {
                    t:'multi',
                    events:[],
                    dirty: RED.nodes.dirty()
                }
                selection.nodes.forEach(function(n) {
                    if (n.type === 'group') {
                        historyEvent.events.push({
                            t: "edit",
                            node: n,
                            changes: {
                                style: JSON.parse(JSON.stringify(n.style))
                            },
                            dirty: RED.nodes.dirty()
                        });
                        n.style = JSON.parse(JSON.stringify(groupStyleClipboard));
                        n.dirty = true;

                    }
                })
                if (historyEvent.events.length > 0) {
                    RED.history.push(historyEvent);
                    RED.nodes.dirty(true);
                    RED.view.redraw();
                }
            }
        }
    }

    function groupSelection() {
        if (RED.workspaces.isLocked()) { return }
        if (RED.view.state() !== RED.state.DEFAULT) { return }
        var selection = RED.view.selection();
        if (selection.nodes) {
            var group = createGroup(selection.nodes);
            if (group) {
                var historyEvent = {
                    t:"createGroup",
                    groups: [ group ],
                    dirty: RED.nodes.dirty()
                }
                RED.history.push(historyEvent);
                RED.view.select({nodes:[group]});
                RED.nodes.dirty(true);
                RED.view.focus();
            }
        }
    }
    function ungroupSelection() {
        if (RED.workspaces.isLocked()) { return }
        if (RED.view.state() !== RED.state.DEFAULT) { return }
        var selection = RED.view.selection();
        if (selection.nodes) {
            var newSelection = [];
            let groups = selection.nodes.filter(function(n) { return n.type === "group" });

            var historyEvent = {
                t:"ungroup",
                groups: [ ],
                dirty: RED.nodes.dirty()
            }
            groups.forEach(function(g) {
                newSelection = newSelection.concat(ungroup(g))
                historyEvent.groups.push(g);
            })
            RED.history.push(historyEvent);
            RED.view.select({nodes:newSelection})
            RED.nodes.dirty(true);
            RED.view.focus();
        }
    }

    function ungroup(g) {
        if (RED.workspaces.isLocked()) { return }
        var nodes = [];
        var parentGroup = RED.nodes.group(g.g);
        g.nodes.forEach(function(n) {
            nodes.push(n);
            if (parentGroup) {
                // Move nodes to parent group
                n.g = parentGroup.id;
                parentGroup.nodes.push(n);
                parentGroup.dirty = true;
                n.dirty = true;
            } else {
                delete n.g;
            }
            if (n.type === 'group') {
                RED.events.emit("groups:change",n)
            } else if (n.type !== 'junction') {
                RED.events.emit("nodes:change",n)
            } else {
                RED.events.emit("junctions:change",n)
            }
        })
        RED.nodes.removeGroup(g);
        return nodes;
    }

    function mergeSelection() {
        if (RED.workspaces.isLocked()) { return }
        if (RED.view.state() !== RED.state.DEFAULT) { return }
        var selection = RED.view.selection();
        if (selection.nodes) {
            var nodes = [];

            var historyEvent = {
                t: "multi",
                events: []
            }
            var ungroupHistoryEvent = {
                t: "ungroup",
                groups: []
            }


            var n;
            var parentGroup;
            // First pass, check they are all in the same parent
            // TODO: DRY mergeSelection,removeSelection,...
            for (var i=0; i<selection.nodes.length; i++) {
                n = selection.nodes[i];
                if (i === 0) {
                    parentGroup = n.g;
                } else if (n.g !== parentGroup) {
                    RED.notify(RED._("group.errors.cannotCreateDiffGroups"),"error");
                    return;
                }
            }
            var existingGroup;
            var mergedEnv = {}
            // Second pass, ungroup any groups in the selection and add their contents
            // to the selection
            for (var i=0; i<selection.nodes.length; i++) {
                n = selection.nodes[i];
                if (n.type === "group") {
                    if (!existingGroup) {
                        existingGroup = n;
                    }
                    if (n.env && n.env.length > 0) {
                        n.env.forEach(env => {
                            mergedEnv[env.name] = env
                        })
                    }
                    ungroupHistoryEvent.groups.push(n);
                    nodes = nodes.concat(ungroup(n));
                } else {
                    nodes.push(n);
                }
                n.dirty = true;
            }
            if (ungroupHistoryEvent.groups.length > 0) {
                historyEvent.events.push(ungroupHistoryEvent);
            }
            // Finally, create the new group
            var group = createGroup(nodes);
            if (group) {
                if (existingGroup) {
                    group.style = existingGroup.style;
                    group.name = existingGroup.name;
                }
                group.env = Object.values(mergedEnv)
                RED.view.select({nodes:[group]})
            }
            historyEvent.events.push({
                t:"createGroup",
                groups: [ group ],
                dirty: RED.nodes.dirty()
            });
            RED.history.push(historyEvent);
            RED.nodes.dirty(true);
            RED.view.focus();
        }
    }

    function removeSelection() {
        if (RED.workspaces.isLocked()) { return }
        if (RED.view.state() !== RED.state.DEFAULT) { return }
        var selection = RED.view.selection();
        if (selection.nodes) {
            var nodes = [];
            var n;
            var parentGroup = RED.nodes.group(selection.nodes[0].g);
            if (parentGroup) {
                try {
                    removeFromGroup(parentGroup,selection.nodes,true);
                    var historyEvent = {
                        t: "removeFromGroup",
                        dirty: RED.nodes.dirty(),
                        group: parentGroup,
                        nodes: selection.nodes
                    }
                    RED.history.push(historyEvent);
                    RED.nodes.dirty(true);
                } catch(err) {
                    RED.notify(err,"error");
                    return;
                }
            }
            RED.view.select({nodes:selection.nodes})
            RED.view.focus();
        }
    }
    function createGroup(nodes) {
        if (RED.workspaces.isLocked()) { return }
        if (nodes.length === 0) {
            return;
        }
        const existingGroup = nodes[0].g
        for (let i = 0; i < nodes.length; i++) {
            const n = nodes[i]
            if (n.type === 'subflow') {
                RED.notify(RED._("group.errors.cannotAddSubflowPorts"),"error");
                return;
            }
            if (n.g !== existingGroup) {
                console.warn("Cannot add nooes with different z properties")
                return
            }
        }
        // nodes is an array
        // each node must be on the same tab (z)
        var group = {
            id: RED.nodes.id(),
            type: 'group',
            nodes: [],
            style: JSON.parse(JSON.stringify(defaultGroupStyle)),
            x: Number.POSITIVE_INFINITY,
            y: Number.POSITIVE_INFINITY,
            w: 0,
            h: 0,
            _def: RED.group.def,
            changed: true
        }

        group.z = nodes[0].z;
        group = RED.nodes.addGroup(group);

        if (existingGroup) {
            addToGroup(RED.nodes.group(existingGroup), group)
        }

        try {
            addToGroup(group,nodes);
        } catch(err) {
            RED.notify(err,"error");
            return;
        }
        return group;
    }
    function addToGroup(group,nodes) {
        if (!Array.isArray(nodes)) {
            nodes = [nodes];
        }
        var i,n,z;
        var g;
        // First pass - validate we can safely add these nodes to the group
        for (i=0;i<nodes.length;i++) {
            n = nodes[i]
            if (!n.z) {
                throw new Error("Cannot add node without a z property to a group")
            }
            if (!z) {
                z = n.z;
            } else if (z !== n.z) {
                throw new Error("Cannot add nodes with different z properties")
            }
            if (n.g) {
                // This is already in a group.
                //  - check they are all in the same group
                if (!g) {
                    if (i!==0) {
                        // TODO: this might be ok when merging groups
                        throw new Error(RED._("group.errors.cannotCreateDiffGroups"))
                    }
                    g = n.g
                }
            }
            if (g !== n.g) {
                throw new Error(RED._("group.errors.cannotCreateDiffGroups"))
            }
        }
        // The nodes are already in a group - so we need to remove them first
        if (g) {
            g = RED.nodes.group(g);
            g.dirty = true;
        }
        // Second pass - add them to the group
        for (i=0;i<nodes.length;i++) {
            n = nodes[i];
            if (n.type !== "subflow") {
                if (g && n.g === g.id) {
                    var ni = g.nodes.indexOf(n);
                    if (ni > -1) {
                        g.nodes.splice(ni,1)
                    }
                }
                n.g = group.id;
                n.dirty = true;
                group.nodes.push(n);
                group.x = Math.min(group.x,n.x-n.w/2-25-((n._def.button && n._def.align!=="right")?20:0));
                group.y = Math.min(group.y,n.y-n.h/2-25);
                group.w = Math.max(group.w,n.x+n.w/2+25+((n._def.button && n._def.align=="right")?20:0) - group.x);
                group.h = Math.max(group.h,n.y+n.h/2+25-group.y);
                if (n.type === 'group') {
                    RED.events.emit("groups:change",n)
                } else if (n.type !== 'junction') {
                    RED.events.emit("nodes:change",n)
                } else {
                    RED.events.emit("junctions:change",n)
                }
            }
        }
        if (g) {
            RED.events.emit("groups:change",group)
        }
        markDirty(group);
    }
    function removeFromGroup(group, nodes, reparent) {
        if (RED.workspaces.isLocked()) { return }
        if (!Array.isArray(nodes)) {
            nodes = [nodes];
        }
        var n;
        // First pass, check they are all in the same parent
        // TODO: DRY mergeSelection,removeSelection,...
        for (var i=0; i<nodes.length; i++) {
            if (nodes[i].g !== group.id) {
                return;
            }
        }
        var parentGroup = RED.nodes.group(group.g);
        for (var i=0; i<nodes.length; i++) {
            n = nodes[i];
            n.dirty = true;
            var index = group.nodes.indexOf(n);
            group.nodes.splice(index,1);
            if (reparent && parentGroup) {
                n.g = group.g
                parentGroup.nodes.push(n);
            } else {
                delete n.g;
            }
            if (n.type === 'group') {
                RED.events.emit("groups:change",n)
            } else if (n.type !== 'junction') {
                RED.events.emit("nodes:change",n)
            } else {
                RED.events.emit("junctions:change",n)
            }
        }
        markDirty(group);
    }

    function getNodes(group,recursive,excludeGroup) {
        var nodes = [];
        group.nodes.forEach(function(n) {
            if (n.type !== 'group' || !excludeGroup) {
                nodes.push(n);
            }
            if (recursive && n.type === 'group') {
                nodes = nodes.concat(getNodes(n,recursive,excludeGroup))
            }
        })
        return nodes;
    }

    function groupContains(group,item) {
        if (item.g === group.id) {
            return true;
        }
        for (var i=0;i<group.nodes.length;i++) {
            if (group.nodes[i].type === "group") {
                if (groupContains(group.nodes[i],item)) {
                    return true;
                }
            }
        }
        return false;
    }
    function getRootGroup(group) {
        if (!group.g) {
            return group;
        }
        return getRootGroup(RED.nodes.group(group.g))
    }

    function createLayoutPicker(options) {

        var container = $("<div>",{style:"display:inline-block"});
        var layoutHiddenInput = $("<input/>", { id: options.id, type: "hidden", value: options.value }).appendTo(container);

        var layoutButton = $('<button type="button" class="red-ui-button red-ui-editor-node-appearance-button">').appendTo(container);
        $('<i class="fa fa-caret-down"></i>').appendTo(layoutButton);

        var layoutDispContainer = $('<div>',{class:"red-ui-search-result-node"}).appendTo(layoutButton);
        var layoutDisp = $('<div>',{class:"red-ui-group-layout-picker-cell-text red-ui-group-layout-text-pos-"}).appendTo(layoutDispContainer);

        var refreshDisplay = function() {
            var val = layoutHiddenInput.val();
            layoutDisp.removeClass().addClass("red-ui-group-layout-picker-cell-text red-ui-group-layout-text-pos-"+val)
        }
        layoutButton.on("click", function(e) {
            var picker = $("<div/>", {
                class: "red-ui-group-layout-picker"
            }).css({
                width: "126px"
            });

            var row = null;

            row = $("<div/>").appendTo(picker);
            var currentButton;
            for (var y=0;y<2;y++) { //red-ui-group-layout-text-pos
                var yComponent= "ns"[y];
                row = $("<div/>").appendTo(picker);
                for (var x=0;x<3;x++) {
                    var xComponent = ["w","","e"][x];
                    var val = yComponent+xComponent;
                    var button = $("<button/>", { class:"red-ui-search-result-node red-ui-button","data-pos":val }).appendTo(row);
                    button.on("click",  function (e) {
                        e.preventDefault();
                        layoutHiddenInput.val($(this).data("pos"));
                        layoutPanel.hide()
                        refreshDisplay();
                    });
                    $('<div>',{class:"red-ui-group-layout-picker-cell-text red-ui-group-layout-text-pos-"+val}).appendTo(button);
                    if (val === layoutHiddenInput.val()) {
                        currentButton = button;
                    }
                }
            }
            refreshDisplay();
            var layoutPanel = RED.popover.panel(picker);
            layoutPanel.show({
                target: layoutButton,
                onclose: function() {
                    layoutButton.focus();
                }
            });
            if (currentButton) {
                currentButton.focus();
            }
        })

        refreshDisplay();

        return container;

    }

    function markDirty(group) {
        group.dirty = true;
        while(group) {
            group.dirty = true;
            group = RED.nodes.group(group.g);
        }
    }


    return {
        def: groupDef,
        init: init,
        createGroup: createGroup,
        ungroup: ungroup,
        addToGroup: addToGroup,
        removeFromGroup: removeFromGroup,
        getNodes: getNodes,
        contains: groupContains,
        markDirty: markDirty
    }
})();
;/**
 * 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.
 **/

RED.userSettings = (function() {

    var trayWidth = 700;
    var settingsVisible = false;

    var panes = [];

    function addPane(options) {
        panes.push(options);
    }

    function show(initialTab) {
        if (settingsVisible) {
            return;
        }
        if (!RED.user.hasPermission("settings.write")) {
            RED.notify(RED._("user.errors.settings"),"error");
            return;
        }
        settingsVisible = true;

        var trayOptions = {
            title: RED._("menu.label.userSettings"),
            buttons: [
                {
                    id: "node-dialog-ok",
                    text: RED._("common.label.close"),
                    class: "primary",
                    click: function() {
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                trayWidth = dimensions.width;
            },
            open: function(tray) {
                var trayBody = tray.find('.red-ui-tray-body');
                var settingsContent = $('<div></div>').appendTo(trayBody);
                var tabContainer = $('<div></div>',{class:"red-ui-settings-tabs-container"}).appendTo(settingsContent);

                $('<ul></ul>',{id:"user-settings-tabs"}).appendTo(tabContainer);
                var settingsTabs = RED.tabs.create({
                    id: "user-settings-tabs",
                    vertical: true,
                    onchange: function(tab) {
                        setTimeout(function() {
                            tabContents.children().hide();
                            $("#" + tab.id).show();
                            if (tab.pane.focus) {
                                tab.pane.focus();
                            }
                        },50);
                    }
                });
                var tabContents = $('<div></div>',{class:"red-ui-settings-tabs-content"}).appendTo(settingsContent);

                panes.forEach(function(pane) {
                    settingsTabs.addTab({
                        id: "red-ui-settings-tab-"+pane.id,
                        label: pane.title,
                        pane: pane
                    });
                    pane.get().hide().appendTo(tabContents);
                });
                settingsContent.i18n();
                settingsTabs.activateTab("red-ui-settings-tab-"+(initialTab||'view'))
                $("#red-ui-sidebar-shade").show();
            },
            close: function() {
                settingsVisible = false;
                panes.forEach(function(pane) {
                    if (pane.close) {
                        pane.close();
                    }
                });
                $("#red-ui-sidebar-shade").hide();

            },
            show: function() {}
        }
        if (trayWidth !== null) {
            trayOptions.width = trayWidth;
        }
        RED.tray.show(trayOptions);
    }

    function localeToName(lc) {
        var name = RED._("languages."+lc);
        return {text: (name ? name : lc), val: lc};
    }

    function compText(a, b) {
        return a.text.localeCompare(b.text);
    }

    var viewSettings = [
        {
            options: [
                {setting:"editor-language",local: true, label:"menu.label.view.language",options:function(done){ done([{val:'',text:RED._('menu.label.view.browserDefault')}].concat(RED.settings.theme("languages").map(localeToName).sort(compText))) }},
            ]
        },
        // {
        //     options: [
        //         {setting:"theme", label:"Theme",options:function(done){ done([{val:'',text:'default'}].concat(RED.settings.theme("themes"))) }},
        //     ]
        // },
        {
            title: "menu.label.view.view",
            options: [
                {setting:"view-store-zoom",label:"menu.label.view.storeZoom", default: false, toggle:true, onchange: function(val) { if (!val) { RED.settings.removeLocal("zoom-level")}}},
                {setting:"view-store-position",label:"menu.label.view.storePosition", default: false, toggle:true, onchange: function(val) { if (!val) { RED.settings.removeLocal("scroll-positions")}}},
            ]
        },
        {
            title: "menu.label.view.grid",
            options: [
                {setting:"view-show-grid",oldSetting:"menu-menu-item-view-show-grid",label:"menu.label.view.showGrid", default: true, toggle:true,onchange:"core:toggle-show-grid"},
                {setting:"view-snap-grid",oldSetting:"menu-menu-item-view-snap-grid",label:"menu.label.view.snapGrid", default: true, toggle:true,onchange:"core:toggle-snap-grid"},
                {setting:"view-grid-size",label:"menu.label.view.gridSize",type:"number",default: 20, onchange:RED.view.gridSize}
            ]
        },
        {
            title: "menu.label.nodes",
            options: [
                {setting:"view-node-status",oldSetting:"menu-menu-item-status",label:"menu.label.displayStatus",default: true, toggle:true,onchange:"core:toggle-status"},
                {setting:"view-node-show-label",label:"menu.label.showNodeLabelDefault",default: true, toggle:true}
            ]
        },
        {
            title: "menu.label.other",
            options: [
                {setting:"view-show-tips",oldSettings:"menu-menu-item-show-tips",label:"menu.label.showTips",toggle:true,default:true,onchange:"core:toggle-show-tips"},
                {setting:"view-show-welcome-tours",label:"menu.label.showWelcomeTours",toggle:true,default:true}
            ]
        }
    ];

    var allSettings = {};

    function createViewPane() {

        var pane = $('<div id="red-ui-settings-tab-view" class="red-ui-help"></div>');

        var currentEditorSettings = RED.settings.get('editor') || {};
        currentEditorSettings.view = currentEditorSettings.view || {};

        viewSettings.forEach(function(section) {
            if (section.title) {
                $('<h3></h3>').text(RED._(section.title)).appendTo(pane);
            }
            section.options.forEach(function(opt) {
                var initialState;
                if (opt.local) {
                    initialState = localStorage.getItem(opt.setting);
                } else {
                    initialState = currentEditorSettings.view[opt.setting];
                }
                var row = $('<div class="red-ui-settings-row"></div>').appendTo(pane);
                var input;
                if (opt.toggle) {
                    input = $('<label for="user-settings-'+opt.setting+'"><input id="user-settings-'+opt.setting+'" type="checkbox"> '+RED._(opt.label)+'</label>').appendTo(row).find("input");
                    input.prop('checked',initialState);
                } else if (opt.options) {
                    $('<label for="user-settings-'+opt.setting+'">'+RED._(opt.label)+'</label>').appendTo(row);
                    var select = $('<select id="user-settings-'+opt.setting+'"></select>').appendTo(row);
                    if (typeof opt.options === 'function') {
                        opt.options(function(options) {
                            options.forEach(function(opt) {
                                var val = opt;
                                var text = opt;
                                if (typeof opt !== 'string') {
                                    val = opt.val;
                                    text = opt.text;
                                }
                                $('<option>').val(val).text(text).appendTo(select);
                            })
                        })
                        select.val(initialState)
                    } else {
                        // TODO: support other option types
                    }
                } else {
                    $('<label for="user-settings-'+opt.setting+'">'+RED._(opt.label)+'</label>').appendTo(row);
                    $('<input id="user-settings-'+opt.setting+'" type="'+(opt.type||"text")+'">').appendTo(row).val(initialState);
                }
            });
        })
        return pane;
    }

    function setSelected(id, value) {
        var opt = allSettings[id];
        if (opt.local) {
            localStorage.setItem(opt.setting,value);
        } else {
            var currentEditorSettings = RED.settings.get('editor') || {};
            currentEditorSettings.view = currentEditorSettings.view || {};
            currentEditorSettings.view[opt.setting] = value;
            RED.settings.set('editor', currentEditorSettings);
            var callback = opt.onchange;
            if (typeof callback === 'string') {
                callback = RED.actions.get(callback);
            }
            if (callback) {
                callback.call(opt,value);
            }
        }
    }
    function toggle(id) {
        var opt = allSettings[id];
        var currentEditorSettings = RED.settings.get('editor') || {};
        currentEditorSettings.view = currentEditorSettings.view || {};
        setSelected(id,!currentEditorSettings.view[opt.setting]);
    }


    function init() {
        RED.actions.add("core:show-user-settings",show);
        RED.actions.add("core:show-help", function() { show('keyboard')});

        addPane({
            id:'view',
            title: RED._("menu.label.view.view"),
            get: createViewPane,
            close: function() {
                viewSettings.forEach(function(section) {
                    section.options.forEach(function(opt) {
                        var input = $("#user-settings-"+opt.setting);
                        if (opt.toggle) {
                            setSelected(opt.setting,input.prop('checked'));
                        } else {
                            setSelected(opt.setting,input.val());
                        }
                    });
                })
            }
        })

        var currentEditorSettings = RED.settings.get('editor') || {};
        currentEditorSettings.view = currentEditorSettings.view || {};
        var editorSettingsChanged = false;
        viewSettings.forEach(function(section) {
            section.options.forEach(function(opt) {
                if (opt.local) {
                    allSettings[opt.setting] = opt;
                    return;
                }
                if (opt.oldSetting) {
                    var oldValue = RED.settings.get(opt.oldSetting);
                    if (oldValue !== undefined && oldValue !== null) {
                        currentEditorSettings.view[opt.setting] = oldValue;
                        editorSettingsChanged = true;
                        RED.settings.remove(opt.oldSetting);
                    }
                }
                allSettings[opt.setting] = opt;
                var value = currentEditorSettings.view[opt.setting];
                if ((value === null || value === undefined) && opt.hasOwnProperty('default')) {
                    value = opt.default;
                    currentEditorSettings.view[opt.setting] = value;
                    editorSettingsChanged = true;
                }

                if (opt.onchange) {
                    var callback = opt.onchange;
                    if (typeof callback === 'string') {
                        callback = RED.actions.get(callback);
                    }
                    if (callback) {
                        callback.call(opt,value);
                    }
                }
            });
        });
        if (editorSettingsChanged) {
            RED.settings.set('editor',currentEditorSettings);
        }

    }
    return {
        init: init,
        toggle: toggle,
        show: show,
        add: addPane
    };
})();
;/**
 * 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.
 **/
RED.projects = (function() {

    var dialog;
    var dialogBody;

    var activeProject;
    function reportUnexpectedError(error) {
        var notification;
        if (error.code === 'git_missing_user') {
            notification = RED.notify("<p>"+RED._("projects.errors.no-username-email")+"</p>",{
                fixed: true,
                type:'error',
                buttons: [
                    {
                        text: RED._("common.label.cancel"),
                        click: function() {
                            notification.close();
                        }
                    },
                    {
                        text: RED._("projects.config-git"),
                        click: function() {
                            RED.userSettings.show('gitconfig');
                            notification.close();
                        }
                    }
                ]
            })
        } else {
            console.log(error);
            notification = RED.notify("<p>"+RED._("projects.errors.unexpected")+":</p><p>"+error.message+"</p><small>"+RED._("projects.errors.code")+": "+error.code+"</small>",{
                fixed: true,
                modal: true,
                type: 'error',
                buttons: [
                    {
                        text: RED._("common.label.close"),
                        click: function() {
                            notification.close();
                        }
                    }
                ]
            })
        }
    }
    var screens = {};
    function initScreens() {
        var migrateProjectHeader = $('<div class="red-ui-projects-dialog-screen-start-hero"></div>');
        $('<span><i class="fa fa-files-o fa-2x"></i> &nbsp; &nbsp; <i class="fa fa-long-arrow-right fa-2x"></i> &nbsp; &nbsp; <i class="fa fa-archive fa-2x"></i></span>').appendTo(migrateProjectHeader)
        $('<hr>').appendTo(migrateProjectHeader);

        var createProjectOptions = {};

        screens = {
            'welcome': {
                content: function(options) {

                    var container = $('<div class="red-ui-projects-dialog-screen-start"></div>');

                    migrateProjectHeader.appendTo(container);

                    var body = $('<div class="red-ui-projects-dialog-screen-start-body"></div>').appendTo(container);
                    $('<p>').text(RED._("projects.welcome.hello")).appendTo(body);
                    $('<p>').text(RED._("projects.welcome.desc0")).appendTo(body);
                    $('<p>').text(RED._("projects.welcome.desc1")).appendTo(body);
                    $('<p>').text(RED._("projects.welcome.desc2")).appendTo(body);

                    var row = $('<div style="text-align: center"></div>').appendTo(body);
                    var createAsEmpty = $('<button data-type="empty" class="red-ui-button red-ui-projects-dialog-button red-ui-projects-dialog-screen-create-type"><i class="fa fa-archive fa-2x"></i><i style="position: absolute;" class="fa fa-asterisk"></i><br/>'+RED._("projects.welcome.create")+'</button>').appendTo(row);
                    var createAsClone = $('<button data-type="clone" class="red-ui-button red-ui-projects-dialog-button red-ui-projects-dialog-screen-create-type"><i class="fa fa-archive fa-2x"></i><i style="position: absolute;" class="fa fa-git"></i><br/>'+RED._("projects.welcome.clone")+'</button>').appendTo(row);

                    createAsEmpty.on("click", function(e) {
                        e.preventDefault();
                        createProjectOptions = {
                            action: "create"
                        }
                        show('git-config');
                    })

                    createAsClone.on("click", function(e) {
                        e.preventDefault();
                        createProjectOptions = {
                            action: "clone"
                        }
                        show('git-config');
                    })

                    return container;
                },
                buttons: [
                    {
                        // id: "red-ui-clipboard-dialog-cancel",
                        text: RED._("projects.welcome.openExistingProject"),
                        class: "secondary",
                        click: function() {
                            createProjectOptions = {
                                action: "open"
                            }
                            show('git-config');
                        }
                    },

                    {
                        // id: "red-ui-clipboard-dialog-cancel",
                        text: RED._("projects.welcome.not-right-now"),
                        click: function() {
                            createProjectOptions = {};
                            $( this ).dialog( "close" );
                        }
                    }
                ]
            },
            'git-config': (function() {
                var gitUsernameInput;
                var gitEmailInput;
                return {
                    content: function(options) {
                        var isGlobalConfig = false;
                        var existingGitSettings = RED.settings.get('git');
                        if (existingGitSettings && existingGitSettings.user) {
                            existingGitSettings = existingGitSettings.user;
                        } else if (RED.settings.git && RED.settings.git.globalUser) {
                            isGlobalConfig = true;
                            existingGitSettings = RED.settings.git.globalUser;
                        }

                        var validateForm = function() {
                            var name = gitUsernameInput.val().trim();
                            var email = gitEmailInput.val().trim();
                            var valid = name.length > 0 && email.length > 0;
                            $("#red-ui-projects-dialog-git-config").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid);

                        }

                        var container = $('<div class="red-ui-projects-dialog-screen-start"></div>');
                        migrateProjectHeader.appendTo(container);
                        var body = $('<div class="red-ui-projects-dialog-screen-start-body"></div>').appendTo(container);

                        $('<p>').text(RED._("projects.git-config.setup")).appendTo(body);
                        $('<p>').text(RED._("projects.git-config.desc0")).appendTo(body);
                        $('<p>').text(RED._("projects.git-config.desc1")).appendTo(body);

                        if (isGlobalConfig) {
                            $('<p>').text(RED._("projects.git-config.desc2")).appendTo(body);
                        }
                        $('<p>').text(RED._("projects.git-config.desc3")).appendTo(body);

                        var row = $('<div class="form-row"></div>').appendTo(body);
                        $('<label for="">'+RED._("projects.git-config.username")+'</label>').appendTo(row);
                        gitUsernameInput = $('<input type="text">').val((existingGitSettings&&existingGitSettings.name)||"").appendTo(row);
                        // $('<div style="position:relative;"></div>').text("This does not need to be your real name").appendTo(row);
                        gitUsernameInput.on("change keyup paste",validateForm);

                        row = $('<div class="form-row"></div>').appendTo(body);
                        $('<label for="">'+RED._("projects.git-config.email")+'</label>').appendTo(row);
                        gitEmailInput = $('<input type="text">').val((existingGitSettings&&existingGitSettings.email)||"").appendTo(row);
                        gitEmailInput.on("change keyup paste",validateForm);
                        // $('<div style="position:relative;"></div>').text("Something something email").appendTo(row);
                        setTimeout(function() {
                            gitUsernameInput.trigger("focus");
                            validateForm();
                        },50);
                        return container;
                    },
                    buttons: [
                        {
                            // id: "red-ui-clipboard-dialog-cancel",
                            text: RED._("common.label.back"),
                            click: function() {
                                show('welcome');
                            }
                        },
                        {
                            id: "red-ui-projects-dialog-git-config",
                            text: RED._("common.label.next"),
                            class: "primary",
                            click: function() {
                                var currentGitSettings = RED.settings.get('git') || {};
                                currentGitSettings.user = currentGitSettings.user || {};
                                currentGitSettings.user.name = gitUsernameInput.val();
                                currentGitSettings.user.email = gitEmailInput.val();
                                RED.settings.set('git', currentGitSettings);
                                if (createProjectOptions.action === "create") {
                                    show('project-details');
                                } else if (createProjectOptions.action === "clone") {
                                    show('clone-project');
                                } else if (createProjectOptions.action === "open") {
                                    show('create',{screen:'open'})
                                }
                            }
                        }
                    ]
                };
            })(),
            'project-details': (function() {
                var projectNameInput;
                var projectSummaryInput;
                return {
                    content: function(options) {
                        var projectList = null;
                        var projectNameValid;

                        var pendingFormValidation = false;
                        $.getJSON("projects", function(data) {
                            projectList = {};
                            data.projects.forEach(function(p) {
                                projectList[p] = true;
                                if (pendingFormValidation) {
                                    pendingFormValidation = false;
                                    validateForm();
                                }
                            })
                        });
                        var container = $('<div class="red-ui-projects-dialog-screen-start"></div>');
                        migrateProjectHeader.appendTo(container);
                        var body = $('<div class="red-ui-projects-dialog-screen-start-body"></div>').appendTo(container);

                        $('<p>').text(RED._("projects.project-details.create")).appendTo(body);
                        $('<p>').text(RED._("projects.project-details.desc0")).appendTo(body);
                        $('<p>').text(RED._("projects.project-details.desc1")).appendTo(body);
                        $('<p>').text(RED._("projects.project-details.desc2")).appendTo(body);

                        var validateForm = function() {
                            var projectName = projectNameInput.val();
                            var valid = true;
                            if (projectNameInputChanged) {
                                if (projectList === null) {
                                    pendingFormValidation = true;
                                    return;
                                }
                                projectNameStatus.empty();
                                if (!/^[a-zA-Z0-9\-_]+$/.test(projectName) || projectList[projectName]) {
                                    projectNameInput.addClass("input-error");
                                    $('<i style="margin-top: 8px;" class="fa fa-exclamation-triangle"></i>').appendTo(projectNameStatus);
                                    projectNameValid = false;
                                    valid = false;
                                    if (projectList[projectName]) {
                                        projectNameSublabel.text(RED._("projects.project-details.already-exists"));
                                    } else {
                                        projectNameSublabel.text(RED._("projects.project-details.must-contain"));
                                    }
                                } else {
                                    projectNameInput.removeClass("input-error");
                                    $('<i style="margin-top: 8px;" class="fa fa-check"></i>').appendTo(projectNameStatus);
                                    projectNameSublabel.text(RED._("projects.project-details.must-contain"));
                                    projectNameValid = true;
                                }
                                projectNameLastChecked = projectName;
                            }
                            valid = projectNameValid;
                            $("#red-ui-projects-dialog-create-name").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid);
                        }

                        var row = $('<div class="form-row"></div>').appendTo(body);
                        $('<label for="red-ui-projects-dialog-screen-create-project-name">'+RED._("projects.project-details.project-name")+'</label>').appendTo(row);

                        var subrow = $('<div style="position:relative;"></div>').appendTo(row);
                        projectNameInput = $('<input id="red-ui-projects-dialog-screen-create-project-name" type="text"></input>').val(createProjectOptions.name||"").appendTo(subrow);
                        var projectNameStatus = $('<div class="red-ui-projects-dialog-screen-input-status"></div>').appendTo(subrow);

                        var projectNameInputChanged = false;
                        var projectNameLastChecked = "";
                        var projectNameValid;
                        var checkProjectName;
                        var autoInsertedName = "";


                        projectNameInput.on("change keyup paste",function() {
                            projectNameInputChanged = (projectNameInput.val() !== projectNameLastChecked);
                            if (checkProjectName) {
                                clearTimeout(checkProjectName);
                            } else if (projectNameInputChanged) {
                                projectNameStatus.empty();
                                $('<img src="red/images/spin.svg"/>').appendTo(projectNameStatus);
                                if (projectNameInput.val() === '') {
                                    validateForm();
                                    return;
                                }
                            }
                            checkProjectName = setTimeout(function() {
                                validateForm();
                                checkProjectName = null;
                            },300)
                        });
                        projectNameSublabel = $('<label class="red-ui-projects-edit-form-sublabel"><small>'+RED._("projects.project-details.must-contain")+'</small></label>').appendTo(row).find("small");

                        // Empty Project
                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-empty"></div>').appendTo(body);
                        $('<label for="red-ui-projects-dialog-screen-create-project-desc">'+RED._("projects.project-details.desc")+'</label>').appendTo(row);
                        projectSummaryInput = $('<input id="red-ui-projects-dialog-screen-create-project-desc" type="text">').val(createProjectOptions.summary||"").appendTo(row);
                        $('<label class="red-ui-projects-edit-form-sublabel"><small>'+RED._("projects.project-details.opt")+'</small></label>').appendTo(row);

                        setTimeout(function() {
                            projectNameInput.trigger("focus");
                            projectNameInput.trigger("change");
                        },50);
                        return container;
                    },
                    buttons: function(options) {
                        return [
                            {
                                text: RED._("common.label.back"),
                                click: function() {
                                    show('git-config');
                                }
                            },
                            {
                                id: "red-ui-projects-dialog-create-name",
                                disabled: true,
                                text: RED._("common.label.next"),
                                class: "primary disabled",
                                click: function() {
                                    createProjectOptions.name = projectNameInput.val();
                                    createProjectOptions.summary = projectSummaryInput.val();
                                    show('default-files', options);
                                }
                            }
                        ]
                    }
                };
            })(),
            'clone-project': (function() {
                var projectNameInput;
                var projectSummaryInput;
                var projectFlowFileInput;
                var projectSecretInput;
                var projectSecretSelect;
                var copyProject;
                var projectRepoInput;
                var projectCloneSecret;
                var emptyProjectCredentialInput;
                var projectRepoUserInput;
                var projectRepoPasswordInput;
                var projectNameSublabel;
                var projectRepoSSHKeySelect;
                var projectRepoPassphrase;
                var projectRepoRemoteName
                var projectRepoBranch;
                var selectedProject;

                return {
                    content: function(options) {
                        var container = $('<div class="red-ui-projects-dialog-screen-start"></div>');
                        migrateProjectHeader.appendTo(container);
                        var body = $('<div class="red-ui-projects-dialog-screen-start-body"></div>').appendTo(container);
                        $('<p>').text(RED._("projects.clone-project.clone")).appendTo(body);
                        $('<p>').text(RED._("projects.clone-project.desc0")).appendTo(body);

                        var projectList = null;
                        var pendingFormValidation = false;
                        $.getJSON("projects", function(data) {
                            projectList = {};
                            data.projects.forEach(function(p) {
                                projectList[p] = true;
                                if (pendingFormValidation) {
                                    pendingFormValidation = false;
                                    validateForm();
                                }
                            })
                        });


                        var validateForm = function() {
                            var projectName = projectNameInput.val();
                            var valid = true;
                            if (projectNameInputChanged) {
                                if (projectList === null) {
                                    pendingFormValidation = true;
                                    return;
                                }
                                projectNameStatus.empty();
                                if (!/^[a-zA-Z0-9\-_]+$/.test(projectName) || projectList[projectName]) {
                                    projectNameInput.addClass("input-error");
                                    $('<i style="margin-top: 8px;" class="fa fa-exclamation-triangle"></i>').appendTo(projectNameStatus);
                                    projectNameValid = false;
                                    valid = false;
                                    if (projectList[projectName]) {
                                        projectNameSublabel.text(RED._("projects.clone-project.already-exists"));
                                    } else {
                                        projectNameSublabel.text(RED._("projects.clone-project.must-contain"));
                                    }
                                } else {
                                    projectNameInput.removeClass("input-error");
                                    $('<i style="margin-top: 8px;" class="fa fa-check"></i>').appendTo(projectNameStatus);
                                    projectNameSublabel.text(RED._("projects.clone-project.must-contain"));
                                    projectNameValid = true;
                                }
                                projectNameLastChecked = projectName;
                            }
                            valid = projectNameValid;

                            var repo = projectRepoInput.val();

                            // var validRepo = /^(?:file|git|ssh|https?|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?[\w\.@:\/~_-]+(?:\/?|\#[\d\w\.\-_]+?)$/.test(repo);
                            var validRepo = repo.length > 0 && !/\s/.test(repo);
                            if (/^https?:\/\/[^/]+@/i.test(repo)) {
                                $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.clone-project.no-info-in-url"));
                                validRepo = false;
                            }
                            if (!validRepo) {
                                if (projectRepoChanged) {
                                    projectRepoInput.addClass("input-error");
                                }
                                valid = false;
                            } else {
                                projectRepoInput.removeClass("input-error");
                            }
                            if (/^https?:\/\//.test(repo)) {
                                $(".red-ui-projects-dialog-screen-create-row-creds").show();
                                $(".red-ui-projects-dialog-screen-create-row-sshkey").hide();
                            } else if (/^(?:ssh|[\S]+?@[\S]+?):(?:\/\/)?/.test(repo)) {
                                $(".red-ui-projects-dialog-screen-create-row-creds").hide();
                                $(".red-ui-projects-dialog-screen-create-row-sshkey").show();
                                // if ( !getSelectedSSHKey(projectRepoSSHKeySelect) ) {
                                //     valid = false;
                                // }
                            } else {
                                $(".red-ui-projects-dialog-screen-create-row-creds").hide();
                                $(".red-ui-projects-dialog-screen-create-row-sshkey").hide();
                            }

                            $("#red-ui-projects-dialog-clone-project").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid);
                        }

                        var row;

                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-empty red-ui-projects-dialog-screen-create-row-clone"></div>').appendTo(body);
                        $('<label for="red-ui-projects-dialog-screen-create-project-name">'+RED._("projects.clone-project.project-name")+'</label>').appendTo(row);

                        var subrow = $('<div style="position:relative;"></div>').appendTo(row);
                        projectNameInput = $('<input id="red-ui-projects-dialog-screen-create-project-name" type="text"></input>').appendTo(subrow);
                        var projectNameStatus = $('<div class="red-ui-projects-dialog-screen-input-status"></div>').appendTo(subrow);

                        var projectNameInputChanged = false;
                        var projectNameLastChecked = "";
                        var projectNameValid;
                        var checkProjectName;
                        var autoInsertedName = "";


                        projectNameInput.on("change keyup paste",function() {
                            projectNameInputChanged = (projectNameInput.val() !== projectNameLastChecked);
                            if (checkProjectName) {
                                clearTimeout(checkProjectName);
                            } else if (projectNameInputChanged) {
                                projectNameStatus.empty();
                                $('<img src="red/images/spin.svg"/>').appendTo(projectNameStatus);
                                if (projectNameInput.val() === '') {
                                    validateForm();
                                    return;
                                }
                            }
                            checkProjectName = setTimeout(function() {
                                validateForm();
                                checkProjectName = null;
                            },300)
                        });
                        projectNameSublabel = $('<label class="red-ui-projects-edit-form-sublabel"><small>'+RED._("projects.clone-project.must-contain")+'</small></label>').appendTo(row).find("small");

                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-clone"></div>').appendTo(body);
                        $('<label for="red-ui-projects-dialog-screen-create-project-repo">'+RED._("projects.clone-project.git-url")+'</label>').appendTo(row);
                        projectRepoInput = $('<input id="red-ui-projects-dialog-screen-create-project-repo" type="text" placeholder="https://git.example.com/path/my-project.git"></input>').appendTo(row);
                        $('<label id="red-ui-projects-dialog-screen-create-project-repo-label" class="red-ui-projects-edit-form-sublabel"><small>'+RED._("projects.clone-project.protocols")+'</small></label>').appendTo(row);
                        var projectRepoChanged = false;
                        var lastProjectRepo = "";
                        projectRepoInput.on("change keyup paste",function() {
                            projectRepoChanged = true;
                            var repo = $(this).val();
                            if (lastProjectRepo !== repo) {
                                $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.clone-project.protocols"));
                            }
                            lastProjectRepo = repo;

                            var m = /\/([^/]+?)(?:\.git)?$/.exec(repo);
                            if (m) {
                                var projectName = projectNameInput.val();
                                if (projectName === "" || projectName === autoInsertedName) {
                                    autoInsertedName = m[1];
                                    projectNameInput.val(autoInsertedName);
                                    projectNameInput.trigger("change");
                                }
                            }
                            validateForm();
                        });

                        var cloneAuthRows = $('<div class="red-ui-projects-dialog-screen-create-row"></div>').appendTo(body);
                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row-auth-error"></div>').hide().appendTo(cloneAuthRows);
                        $('<div><i class="fa fa-warning"></i> '+RED._("projects.clone-project.auth-failed")+'</div>').appendTo(row);

                        // Repo credentials - username/password ----------------
                        row = $('<div class="hide form-row red-ui-projects-dialog-screen-create-row-creds"></div>').hide().appendTo(cloneAuthRows);

                        var subrow = $('<div style="width: calc(50% - 10px); display:inline-block;"></div>').appendTo(row);
                        $('<label for="red-ui-projects-dialog-screen-create-project-repo-user">'+RED._("projects.clone-project.username")+'</label>').appendTo(subrow);
                        projectRepoUserInput = $('<input id="red-ui-projects-dialog-screen-create-project-repo-user" type="text"></input>').appendTo(subrow);

                        subrow = $('<div style="width: calc(50% - 10px); margin-left: 20px; display:inline-block;"></div>').appendTo(row);
                        $('<label for="red-ui-projects-dialog-screen-create-project-repo-pass">'+RED._("projects.clone-project.passwd")+'</label>').appendTo(subrow);
                        projectRepoPasswordInput = $('<input style="width:100%" id="red-ui-projects-dialog-screen-create-project-repo-pass" type="password"></input>').appendTo(subrow);
                        projectRepoPasswordInput.typedInput({type:"cred"});
                        // -----------------------------------------------------

                        // Repo credentials - key/passphrase -------------------
                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-sshkey"></div>').hide().appendTo(cloneAuthRows);
                        subrow = $('<div style="width: calc(50% - 10px); display:inline-block;"></div>').appendTo(row);
                        $('<label for="red-ui-projects-dialog-screen-create-project-repo-passphrase">'+RED._("projects.clone-project.ssh-key")+'</label>').appendTo(subrow);
                        projectRepoSSHKeySelect = $("<select>",{style:"width: 100%"}).appendTo(subrow);

                        $.getJSON("settings/user/keys", function(data) {
                            var count = 0;
                            data.keys.forEach(function(key) {
                                projectRepoSSHKeySelect.append($("<option></option>").val(key.name).text(key.name));
                                count++;
                            });
                            if (count === 0) {
                                projectRepoSSHKeySelect.addClass("input-error");
                                projectRepoSSHKeySelect.attr("disabled",true);
                                sshwarningRow.show();
                            } else {
                                projectRepoSSHKeySelect.removeClass("input-error");
                                projectRepoSSHKeySelect.attr("disabled",false);
                                sshwarningRow.hide();
                            }
                        });
                        subrow = $('<div style="width: calc(50% - 10px); margin-left: 20px; display:inline-block;"></div>').appendTo(row);
                        $('<label for="red-ui-projects-dialog-screen-create-project-repo-passphrase">'+RED._("projects.clone-project.passphrase")+'</label>').appendTo(subrow);
                        projectRepoPassphrase = $('<input id="red-ui-projects-dialog-screen-create-project-repo-passphrase" type="password"></input>').appendTo(subrow);
                        projectRepoPassphrase.typedInput({type:"cred"});
                        subrow = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-sshkey"></div>').appendTo(cloneAuthRows);
                        var sshwarningRow = $('<div class="red-ui-projects-dialog-screen-create-row-auth-error-no-keys"></div>').hide().appendTo(subrow);
                        $('<div class="form-row"><i class="fa fa-warning"></i> '+RED._("projects.clone-project.ssh-key-desc")+'</div>').appendTo(sshwarningRow);
                        subrow = $('<div style="text-align: center">').appendTo(sshwarningRow);
                        $('<button type="button" class="red-ui-button red-ui-projects-dialog-button">'+RED._("projects.clone-project.ssh-key-add")+'</button>').appendTo(subrow).on("click", function(e) {
                            e.preventDefault();
                            dialog.dialog( "close" );
                            RED.userSettings.show('gitconfig');
                            setTimeout(function() {
                                $("#user-settings-gitconfig-add-key").trigger("click");
                            },500);
                        });
                        // -----------------------------------------------------


                        // Secret - clone
                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-clone"></div>').appendTo(body);
                        $('<label>'+RED._("projects.clone-project.credential-key")+'</label>').appendTo(row);
                        projectSecretInput = $('<input style="width: 100%" type="password"></input>').appendTo(row);
                        projectSecretInput.typedInput({type:"cred"});


                        return container;
                    },
                    buttons: function(options) {
                        return [
                            {
                                text: RED._("common.label.back"),
                                click: function() {
                                    show('git-config');
                                }
                            },
                            {
                                id: "red-ui-projects-dialog-clone-project",
                                disabled: true,
                                text: RED._("common.label.clone"),
                                class: "primary disabled",
                                click: function() {
                                    var projectType = $(".red-ui-projects-dialog-screen-create-type.selected").data('type');
                                    var projectData = {
                                        name: projectNameInput.val(),
                                    }
                                    projectData.credentialSecret = projectSecretInput.val();
                                    var repoUrl = projectRepoInput.val();
                                    var metaData = {};
                                    if (/^(?:ssh|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?/.test(repoUrl)) {
                                        var selected = projectRepoSSHKeySelect.val();//false;//getSelectedSSHKey(projectRepoSSHKeySelect);
                                        if ( selected ) {
                                            projectData.git = {
                                                remotes: {
                                                    'origin': {
                                                        url: repoUrl,
                                                        keyFile: selected,
                                                        passphrase: projectRepoPassphrase.val()
                                                    }
                                                }
                                            };
                                        }
                                        else {
                                            console.log(RED._("projects.clone-project.cant-get-ssh-key"));
                                            return;
                                        }
                                    }
                                    else {
                                        projectData.git = {
                                            remotes: {
                                                'origin': {
                                                    url: repoUrl,
                                                    username: projectRepoUserInput.val(),
                                                    password: projectRepoPasswordInput.val()
                                                }
                                            }
                                        };
                                    }

                                    $(".red-ui-projects-dialog-screen-create-row-auth-error").hide();
                                    $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.clone-project.protocols"));

                                    projectRepoUserInput.removeClass("input-error");
                                    projectRepoPasswordInput.removeClass("input-error");
                                    projectRepoSSHKeySelect.removeClass("input-error");
                                    projectRepoPassphrase.removeClass("input-error");

                                    RED.deploy.setDeployInflight(true);
                                    RED.projects.settings.switchProject(projectData.name);

                                    sendRequest({
                                        url: "projects",
                                        type: "POST",
                                        handleAuthFail: false,
                                        responses: {
                                            200: function(data) {
                                                dialog.dialog( "close" );
                                            },
                                            400: {
                                                'project_exists': function(error) {
                                                    console.log(RED._("projects.clone-project.already-exists2"));
                                                },
                                                'git_error': function(error) {
                                                    console.log(RED._("projects.clone-project.git-error"),error);
                                                },
                                                'git_connection_failed': function(error) {
                                                    projectRepoInput.addClass("input-error");
                                                    $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.clone-project.connection-failed"));
                                                },
                                                'git_not_a_repository': function(error) {
                                                    projectRepoInput.addClass("input-error");
                                                    $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.clone-project.not-git-repo"));
                                                },
                                                'git_repository_not_found': function(error) {
                                                    projectRepoInput.addClass("input-error");
                                                    $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.clone-project.repo-not-found"));
                                                },
                                                'git_auth_failed': function(error) {
                                                    $(".red-ui-projects-dialog-screen-create-row-auth-error").show();

                                                    projectRepoUserInput.addClass("input-error");
                                                    projectRepoPasswordInput.addClass("input-error");
                                                    // getRepoAuthDetails(req);
                                                    projectRepoSSHKeySelect.addClass("input-error");
                                                    projectRepoPassphrase.addClass("input-error");
                                                },
                                                'missing_flow_file': function(error) {
                                                    // This is handled via a runtime notification.
                                                    dialog.dialog("close");
                                                },
                                                'missing_package_file': function(error) {
                                                    // This is handled via a runtime notification.
                                                    dialog.dialog("close");
                                                },
                                                'project_empty': function(error) {
                                                    // This is handled via a runtime notification.
                                                    dialog.dialog("close");
                                                },
                                                'credentials_load_failed': function(error) {
                                                    // This is handled via a runtime notification.
                                                    dialog.dialog("close");
                                                },
                                                '*': function(error) {
                                                    reportUnexpectedError(error);
                                                    $( dialog ).dialog( "close" );
                                                }
                                            }
                                        }
                                    },projectData).then(function() {
                                        RED.menu.setDisabled('menu-item-projects-open',false);
                                        RED.menu.setDisabled('menu-item-projects-settings',false);
                                        RED.events.emit("project:change", {name:name});
                                    }).always(function() {
                                        setTimeout(function() {
                                            RED.deploy.setDeployInflight(false);
                                        },500);
                                    })

                                }
                            }
                        ]
                    }
                }
            })(),
            'default-files': (function() {
                var projectFlowFileInput;
                var projectCredentialFileInput;
                return {
                    content: function(options) {
                        var container = $('<div class="red-ui-projects-dialog-screen-start"></div>');
                        migrateProjectHeader.appendTo(container);
                        var body = $('<div class="red-ui-projects-dialog-screen-start-body"></div>').appendTo(container);

                        $('<p>').text(RED._("projects.default-files.create")).appendTo(body);
                        $('<p>').text(RED._("projects.default-files.desc0")).appendTo(body);
                        $('<p>').text(RED._("projects.default-files.desc1")).appendTo(body);
                        if (!options.existingProject && RED.settings.files) {
                            $('<p>').text(RED._("projects.default-files.desc2")).appendTo(body);
                        }

                        var validateForm = function() {
                            var valid = true;
                            var flowFile = projectFlowFileInput.val();
                            if (flowFile === "" || !/\.json$/.test(flowFile)) {
                                valid = false;
                                if (!projectFlowFileInput.hasClass("input-error")) {
                                    projectFlowFileInput.addClass("input-error");
                                    projectFlowFileInput.next().empty().append('<i style="margin-top: 8px;" class="fa fa-exclamation-triangle"></i>');
                                }
                                projectCredentialFileInput.text("");
                                if (!projectCredentialFileInput.hasClass("input-error")) {
                                    projectCredentialFileInput.addClass("input-error");
                                    projectCredentialFileInput.next().empty().append('<i style="margin-top: 8px;" class="fa fa-exclamation-triangle"></i>');
                                }
                            } else {
                                if (projectFlowFileInput.hasClass("input-error")) {
                                    projectFlowFileInput.removeClass("input-error");
                                    projectFlowFileInput.next().empty();
                                }
                                if (projectCredentialFileInput.hasClass("input-error")) {
                                    projectCredentialFileInput.removeClass("input-error");
                                    projectCredentialFileInput.next().empty();
                                }
                                projectCredentialFileInput.text(flowFile.substring(0,flowFile.length-5)+"_cred.json");
                            }
                            $("#red-ui-projects-dialog-create-default-files").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid);
                        }
                        var row = $('<div class="form-row"></div>').appendTo(body);
                        $('<label for="red-ui-projects-dialog-screen-create-project-file">'+RED._("projects.default-files.flow-file")+'</label>').appendTo(row);
                        var subrow = $('<div style="position:relative;"></div>').appendTo(row);
                        var defaultFlowFile = (createProjectOptions.files &&createProjectOptions.files.flow) || (RED.settings.files && RED.settings.files.flow) || "flows.json";
                        projectFlowFileInput = $('<input id="red-ui-projects-dialog-screen-create-project-file" type="text">').val(defaultFlowFile)
                            .on("change keyup paste",validateForm)
                            .appendTo(subrow);
                        $('<div class="red-ui-projects-dialog-screen-input-status"></div>').appendTo(subrow);
                        $('<label class="red-ui-projects-edit-form-sublabel"><small>*.json</small></label>').appendTo(row);

                        var defaultCredentialsFile = (createProjectOptions.files &&createProjectOptions.files.credentials) || (RED.settings.files && RED.settings.files.credentials) || "flows_cred.json";
                        row = $('<div class="form-row"></div>').appendTo(body);
                        $('<label for="red-ui-projects-dialog-screen-create-project-credfile">'+RED._("projects.default-files.credentials-file")+'</label>').appendTo(row);
                        subrow = $('<div style="position:relative;"></div>').appendTo(row);
                        projectCredentialFileInput = $('<div style="width: 100%" class="uneditable-input" id="red-ui-projects-dialog-screen-create-project-credentials">').text(defaultCredentialsFile)
                            .appendTo(subrow);
                        $('<div class="red-ui-projects-dialog-screen-input-status"></div>').appendTo(subrow);

                        setTimeout(function() {
                            projectFlowFileInput.trigger("focus");
                            validateForm();
                        },50);

                        return container;
                    },
                    buttons: function(options) {
                        return [
                            {
                                // id: "red-ui-clipboard-dialog-cancel",
                                text: RED._(options.existingProject ? "common.label.cancel": "common.label.back"),
                                click: function() {
                                    if (options.existingProject) {
                                        $(this).dialog('close');
                                    } else {
                                        show('project-details',options);
                                    }
                                }
                            },
                            {
                                id: "red-ui-projects-dialog-create-default-files",
                                text: RED._("common.label.next"),
                                class: "primary",
                                click: function() {
                                    createProjectOptions.files = {
                                        flow: projectFlowFileInput.val(),
                                        credentials: projectCredentialFileInput.text()
                                    }
                                    if (!options.existingProject) {
                                        createProjectOptions.migrateFiles = true;
                                    }
                                    show('encryption-config',options);
                                }
                            }
                        ]
                    }
                }
            })(),
            'encryption-config': (function() {
                var emptyProjectCredentialInput;
                return {
                    content: function(options) {

                        var container = $('<div class="red-ui-projects-dialog-screen-start"></div>');
                        migrateProjectHeader.appendTo(container);
                        var body = $('<div class="red-ui-projects-dialog-screen-start-body"></div>').appendTo(container);

                        $('<p>').text(RED._("projects.encryption-config.setup")).appendTo(body);
                        if (options.existingProject) {
                            $('<p>').text(RED._("projects.encryption-config.desc0")).appendTo(body);
                            $('<p>').text(RED._("projects.encryption-config.desc1")).appendTo(body);
                        } else {
                            if (RED.settings.flowEncryptionType === 'disabled') {
                                $('<p>').text(RED._("projects.encryption-config.desc2")).appendTo(body);
                                $('<p>').text(RED._("projects.encryption-config.desc3")).appendTo(body);
                                $('<p>').text(RED._("projects.encryption-config.desc4")).appendTo(body);
                            } else {
                                if (RED.settings.flowEncryptionType === 'user') {
                                    $('<p>').text(RED._("projects.encryption-config.desc5")).appendTo(body);
                                } else if (RED.settings.flowEncryptionType === 'system') {
                                    $('<p>').text(RED._("projects.encryption-config.desc6")).appendTo(body);
                                }
                                $('<p>').text(RED._("projects.encryption-config.desc7")).appendTo(body);
                            }
                        }

                        // var row = $('<div class="form-row"></div>').appendTo(body);
                        // $('<label for="">Username</label>').appendTo(row);
                        // var gitUsernameInput = $('<input type="text">').val(currentGitSettings.user.name||"").appendTo(row);
                        // // $('<div style="position:relative;"></div>').text("This does not need to be your real name").appendTo(row);
                        //
                        // row = $('<div class="form-row"></div>').appendTo(body);
                        // $('<label for="">Email</label>').appendTo(row);
                        // var gitEmailInput = $('<input type="text">').val(currentGitSettings.user.email||"").appendTo(row);
                        // // $('<div style="position:relative;"></div>').text("Something something email").appendTo(row);

                        var validateForm = function() {
                            var valid = true;
                            var encryptionState = $("input[name=projects-encryption-type]:checked").val();
                            if (encryptionState === 'enabled') {
                                var encryptionKeyType = $("input[name=projects-encryption-key]:checked").val();
                                if (encryptionKeyType === 'custom') {
                                    valid = valid && emptyProjectCredentialInput.val()!=='';
                                }
                            }
                            $("#red-ui-projects-dialog-create-encryption").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid);
                        }


                        var row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-empty"></div>').appendTo(body);
                        $('<label>'+RED._("projects.encryption-config.credentials")+'</label>').appendTo(row);

                        var credentialsBox = $('<div class="red-ui-projects-dialog-credentials-box">').appendTo(row);
                        var credentialsRightBox = $('<div class="red-ui-projects-dialog-credentials-box-right">').appendTo(credentialsBox);
                        var credentialsLeftBox = $('<div class="red-ui-projects-dialog-credentials-box-left">').appendTo(credentialsBox);

                        var credentialsEnabledBox = $('<div class="form-row red-ui-projects-dialog-credentials-box-enabled"></div>').appendTo(credentialsLeftBox);
                        $('<label class="red-ui-projects-edit-form-inline-label"><input type="radio" name="projects-encryption-type" value="enabled"> <i class="fa fa-lock"></i> <span>'+RED._("projects.encryption-config.enable")+'</span></label>').appendTo(credentialsEnabledBox);
                        var credentialsDisabledBox = $('<div class="form-row red-ui-projects-dialog-credentials-box-disabled"></div>').appendTo(credentialsLeftBox);
                        $('<label class="red-ui-projects-edit-form-inline-label"><input type="radio" name="projects-encryption-type" value="disabled"> <i class="fa fa-unlock"></i> <span>'+RED._("projects.encryption-config.disable")+'</span></label>').appendTo(credentialsDisabledBox);

                        credentialsLeftBox.find("input[name=projects-encryption-type]").on("click", function(e) {
                            var val = $(this).val();
                            var toEnable;
                            var toDisable;
                            if (val === 'enabled') {
                                toEnable = credentialsEnabledBox;
                                toDisable = credentialsDisabledBox;
                                $(".projects-encryption-enabled-row").show();
                                $(".projects-encryption-disabled-row").hide();
                                if ($("input[name=projects-encryption-key]:checked").val() === 'custom') {
                                    emptyProjectCredentialInput.trigger("focus");
                                }

                            } else {
                                toDisable = credentialsEnabledBox;
                                toEnable = credentialsDisabledBox;
                                $(".projects-encryption-enabled-row").hide();
                                $(".projects-encryption-disabled-row").show();
                            }

                            toEnable.removeClass("disabled");
                            toDisable.addClass("disabled");
                            validateForm();
                        })

                        row = $('<div class="form-row projects-encryption-enabled-row"></div>').appendTo(credentialsRightBox);
                        $('<label class="red-ui-projects-edit-form-inline-label '+((RED.settings.flowEncryptionType !== 'user')?'disabled':'')+'" style="margin-left: 5px"><input '+((RED.settings.flowEncryptionType !== 'user')?RED._("projects.encryption-config.disabled"):'')+' type="radio" style="vertical-align: middle; margin-top:0; margin-right: 10px;" value="default" name="projects-encryption-key"> <span style="vertical-align: middle;">'+RED._("projects.encryption-config.copy")+'</span></label>').appendTo(row);
                        row = $('<div class="form-row projects-encryption-enabled-row"></div>').appendTo(credentialsRightBox);
                        $('<label class="red-ui-projects-edit-form-inline-label" style="margin-left: 5px"><input type="radio" style="vertical-align: middle; margin-top:0; margin-right: 10px;" value="custom" name="projects-encryption-key"> <span style="vertical-align: middle;">'+RED._("projects.encryption-config.use-custom")+'</span></label>').appendTo(row);
                        row = $('<div class="projects-encryption-enabled-row"></div>').appendTo(credentialsRightBox);
                        emptyProjectCredentialInput = $('<input disabled type="password" style="margin-left: 25px; width: calc(100% - 30px);"></input>').appendTo(row);
                        emptyProjectCredentialInput.typedInput({type:"cred"});
                        emptyProjectCredentialInput.on("change keyup paste", validateForm);

                        row = $('<div class="form-row projects-encryption-disabled-row"></div>').hide().appendTo(credentialsRightBox);
                        $('<div class="" style="padding: 5px 20px;"><i class="fa fa-warning"></i> '+RED._("projects.encryption-config.desc8")+'</div>').appendTo(row);

                        credentialsRightBox.find("input[name=projects-encryption-key]").on("click", function() {
                            var val = $(this).val();
                            emptyProjectCredentialInput.attr("disabled",val === 'default');
                            if (val === "custom") {
                                emptyProjectCredentialInput.trigger("focus");
                            }
                            validateForm();
                        });

                        setTimeout(function() {
                            credentialsLeftBox.find("input[name=projects-encryption-type][value=enabled]").trigger("click");
                            if (RED.settings.flowEncryptionType !== 'user') {
                                credentialsRightBox.find("input[name=projects-encryption-key][value=custom]").trigger("click");
                            } else {
                                credentialsRightBox.find("input[name=projects-encryption-key][value=default]").trigger("click");
                            }
                            validateForm();
                        },100);

                        return container;
                    },
                    buttons: function(options) {
                            return [
                            {
                                // id: "red-ui-clipboard-dialog-cancel",
                                text: RED._("common.label.back"),
                                click: function() {
                                    show('default-files',options);
                                }
                            },
                            {
                                id: "red-ui-projects-dialog-create-encryption",
                                text: RED._(options.existingProject?"projects.encryption-config.create-project-files":"projects.encryption-config.create-project"),
                                class: "primary disabled",
                                disabled: true,
                                click: function() {
                                    var encryptionState = $("input[name=projects-encryption-type]:checked").val();
                                    if (encryptionState === 'enabled') {
                                        var encryptionKeyType = $("input[name=projects-encryption-key]:checked").val();
                                        if (encryptionKeyType === 'custom') {
                                            createProjectOptions.credentialSecret = emptyProjectCredentialInput.val();
                                        } else {
                                            // If 'use existing', leave createProjectOptions.credentialSecret blank
                                            // - that will trigger it to use the existing key
                                            // TODO: this option should be disabled if encryption is disabled
                                        }
                                    } else {
                                        // Disabled encryption by explicitly setting credSec to false
                                        createProjectOptions.credentialSecret = false;
                                    }
                                    RED.deploy.setDeployInflight(true);
                                    RED.projects.settings.switchProject(createProjectOptions.name);

                                    var method = "POST";
                                    var url = "projects";

                                    if (options.existingProject) {
                                        createProjectOptions.initialise = true;
                                        method = "PUT";
                                        url = "projects/"+activeProject.name;
                                    }
                                    var self = this;
                                    sendRequest({
                                        url: url,
                                        type: method,
                                        requireCleanWorkspace: true,
                                        handleAuthFail: false,
                                        responses: {
                                            200: function(data) {
                                                createProjectOptions = {};
                                                if (options.existingProject) {
                                                    $( self ).dialog( "close" );
                                                } else {
                                                    show('create-success');
                                                    RED.menu.setDisabled('menu-item-projects-open',false);
                                                    RED.menu.setDisabled('menu-item-projects-settings',false);
                                                }
                                            },
                                            400: {
                                                'project_exists': function(error) {
                                                    console.log(RED._("projects.encryption-config.already-exists"));
                                                },
                                                'git_error': function(error) {
                                                    console.log(RED._("projects.encryption-config.git-error"),error);
                                                },
                                                'git_connection_failed': function(error) {
                                                    projectRepoInput.addClass("input-error");
                                                },
                                                'git_auth_failed': function(error) {
                                                    projectRepoUserInput.addClass("input-error");
                                                    projectRepoPasswordInput.addClass("input-error");
                                                    // getRepoAuthDetails(req);
                                                    console.log(RED._("projects.encryption-config.git-auth-error"),error);
                                                },
                                                '*': function(error) {
                                                    reportUnexpectedError(error);
                                                    $( dialog ).dialog( "close" );
                                                }
                                            }
                                        }
                                    },createProjectOptions).always(function() {
                                        setTimeout(function() {
                                            RED.deploy.setDeployInflight(false);
                                        },500);
                                    })
                                }
                            }
                        ];
                    }
                }
            })(),
            'create-success': {
                content: function(options) {

                    var container = $('<div class="red-ui-projects-dialog-screen-start"></div>');
                    migrateProjectHeader.appendTo(container);
                    var body = $('<div class="red-ui-projects-dialog-screen-start-body"></div>').appendTo(container);

                    $('<p>').text(RED._("projects.create-success.success")).appendTo(body);
                    $('<p>').text(RED._("projects.create-success.desc0")).appendTo(body);
                    $('<p>').text(RED._("projects.create-success.desc1")).appendTo(body);
                    $('<p>').text(RED._("projects.create-success.desc2")).appendTo(body);

                    return container;
                },
                buttons: [
                    {
                        text: RED._("common.label.done"),
                        click: function() {
                            $( this ).dialog( "close" );
                        }
                    }
                ]
            },
            'create': (function() {
                var projectNameInput;
                var projectSummaryInput;
                var projectFlowFileInput;
                var projectSecretInput;
                var projectSecretSelect;
                var copyProject;
                var projectRepoInput;
                var projectCloneSecret;
                var emptyProjectCredentialInput;
                var projectRepoUserInput;
                var projectRepoPasswordInput;
                var projectNameSublabel;
                var projectRepoSSHKeySelect;
                var projectRepoPassphrase;
                var projectRepoRemoteName
                var projectRepoBranch;
                var selectedProject;

                return {
                    title: RED._("projects.create.projects"),
                    content: function(options) {
                        var projectList = null;
                        selectedProject = null;
                        var pendingFormValidation = false;
                        $.getJSON("projects", function(data) {
                            projectList = {};
                            data.projects.forEach(function(p) {
                                projectList[p] = true;
                                if (pendingFormValidation) {
                                    pendingFormValidation = false;
                                    validateForm();
                                }
                            })
                        });

                        var container = $('<div class="red-ui-projects-dialog-screen-create"></div>');
                        var row;

                        var validateForm = function() {
                            var projectName = projectNameInput.val();
                            var valid = true;
                            if (projectNameInputChanged) {
                                if (projectList === null) {
                                    pendingFormValidation = true;
                                    return;
                                }
                                projectNameStatus.empty();
                                if (!/^[a-zA-Z0-9\-_]+$/.test(projectName) || projectList[projectName]) {
                                    projectNameInput.addClass("input-error");
                                    $('<i style="margin-top: 8px;" class="fa fa-exclamation-triangle"></i>').appendTo(projectNameStatus);
                                    projectNameValid = false;
                                    valid = false;
                                    if (projectList[projectName]) {
                                        projectNameSublabel.text(RED._("projects.create.already-exists"));
                                    } else {
                                        projectNameSublabel.text(RED._("projects.create.must-contain"));
                                    }
                                } else {
                                    projectNameInput.removeClass("input-error");
                                    $('<i style="margin-top: 8px;" class="fa fa-check"></i>').appendTo(projectNameStatus);
                                    projectNameSublabel.text(RED._("projects.create.must-contain"));
                                    projectNameValid = true;
                                }
                                projectNameLastChecked = projectName;
                            }
                            valid = projectNameValid;

                            var projectType = $(".red-ui-projects-dialog-screen-create-type.selected").data('type');
                            if (projectType === 'copy') {
                                if (!copyProject) {
                                    valid = false;
                                }
                            } else if (projectType === 'clone') {
                                var repo = projectRepoInput.val();

                                // var validRepo = /^(?:file|git|ssh|https?|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?[\w\.@:\/~_-]+(?:\/?|\#[\d\w\.\-_]+?)$/.test(repo);
                                var validRepo = repo.length > 0 && !/\s/.test(repo);
                                if (/^https?:\/\/[^/]+@/i.test(repo)) {
                                    $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.create.no-info-in-url"));
                                    validRepo = false;
                                }
                                if (!validRepo) {
                                    if (projectRepoChanged) {
                                        projectRepoInput.addClass("input-error");
                                    }
                                    valid = false;
                                } else {
                                    projectRepoInput.removeClass("input-error");
                                }
                                if (/^https?:\/\//.test(repo)) {
                                    $(".red-ui-projects-dialog-screen-create-row-creds").show();
                                    $(".red-ui-projects-dialog-screen-create-row-sshkey").hide();
                                } else if (/^(?:ssh|[\S]+?@[\S]+?):(?:\/\/)?/.test(repo)) {
                                    $(".red-ui-projects-dialog-screen-create-row-creds").hide();
                                    $(".red-ui-projects-dialog-screen-create-row-sshkey").show();
                                    // if ( !getSelectedSSHKey(projectRepoSSHKeySelect) ) {
                                    //     valid = false;
                                    // }
                                } else {
                                    $(".red-ui-projects-dialog-screen-create-row-creds").hide();
                                    $(".red-ui-projects-dialog-screen-create-row-sshkey").hide();
                                }


                            } else if (projectType === 'empty') {
                                var flowFile = projectFlowFileInput.val();
                                if (flowFile === "" || !/\.json$/.test(flowFile)) {
                                    valid = false;
                                    if (!projectFlowFileInput.hasClass("input-error")) {
                                        projectFlowFileInput.addClass("input-error");
                                        projectFlowFileInput.next().empty().append('<i style="margin-top: 8px;" class="fa fa-exclamation-triangle"></i>');
                                    }
                                } else {
                                    if (projectFlowFileInput.hasClass("input-error")) {
                                        projectFlowFileInput.removeClass("input-error");
                                        projectFlowFileInput.next().empty();
                                    }
                                }

                                var encryptionState = $("input[name=projects-encryption-type]:checked").val();
                                if (encryptionState === 'enabled') {
                                    var encryptionKeyType = $("input[name=projects-encryption-key]:checked").val();
                                    if (encryptionKeyType === 'custom') {
                                        valid = valid && emptyProjectCredentialInput.val()!==''
                                    }
                                }
                            } else if (projectType === 'open') {
                                valid = !!selectedProject;
                            }

                            $("#red-ui-projects-dialog-create").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid);
                        }

                        row = $('<div class="form-row button-group"></div>').appendTo(container);

                        var openProject = $('<button type="button" data-type="open" class="red-ui-button red-ui-projects-dialog-button red-ui-projects-dialog-screen-create-type toggle"><i class="fa fa-archive fa-2x"></i><i style="position: absolute;" class="fa fa-folder-open"></i><br/>'+RED._("projects.create.open")+'</button>').appendTo(row);
                        var createAsEmpty = $('<button type="button" data-type="empty" class="red-ui-button red-ui-projects-dialog-button red-ui-projects-dialog-screen-create-type toggle"><i class="fa fa-archive fa-2x"></i><i style="position: absolute;" class="fa fa-asterisk"></i><br/>'+RED._("projects.create.create")+'</button>').appendTo(row);
                        // var createAsCopy = $('<button type="button" data-type="copy" class="red-ui-button red-ui-projects-dialog-button red-ui-projects-dialog-screen-create-type toggle"><i class="fa fa-archive fa-2x"></i><i class="fa fa-long-arrow-right fa-2x"></i><i class="fa fa-archive fa-2x"></i><br/>Copy existing</button>').appendTo(row);
                        var createAsClone = $('<button type="button" data-type="clone" class="red-ui-button red-ui-projects-dialog-button red-ui-projects-dialog-screen-create-type toggle"><i class="fa fa-archive fa-2x"></i><i style="position: absolute;" class="fa fa-git"></i><br/>'+RED._("projects.create.clone")+'</button>').appendTo(row);
                        // var createAsClone = $('<button type="button" data-type="clone" class="red-ui-button red-ui-projects-dialog-button red-ui-projects-dialog-screen-create-type toggle"><i class="fa fa-git fa-2x"></i><i class="fa fa-arrows-h fa-2x"></i><i class="fa fa-archive fa-2x"></i><br/>Clone Repository</button>').appendTo(row);
                        row.find(".red-ui-projects-dialog-screen-create-type").on("click", function(evt) {
                            evt.preventDefault();
                            container.find(".red-ui-projects-dialog-screen-create-type").removeClass('selected');
                            $(this).addClass('selected');
                            container.find(".red-ui-projects-dialog-screen-create-row").hide();
                            container.find(".red-ui-projects-dialog-screen-create-row-"+$(this).data('type')).show();
                            validateForm();
                            projectNameInput.trigger("focus");
                            switch ($(this).data('type')) {
                                case "open": $("#red-ui-projects-dialog-create").text(RED._("projects.create.open")); break;
                                case "empty": $("#red-ui-projects-dialog-create").text(RED._("projects.create.create")); break;
                                case "clone": $("#red-ui-projects-dialog-create").text(RED._("projects.create.clone")); break;
                            }
                        })

                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-open"></div>').hide().appendTo(container);
                        createProjectList({
                            canSelectActive: false,
                            dblclick: function(project) {
                                selectedProject = project;
                                $("#red-ui-projects-dialog-create").trigger("click");
                            },
                            select: function(project) {
                                selectedProject = project;
                                validateForm();
                            },
                            delete: function(project) {
                                if (projectList) {
                                    delete projectList[project.name];
                                }
                                selectedProject = null;

                                validateForm();
                            }
                        }).appendTo(row);

                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-open"></div>').hide().appendTo(container);
                        $('<span style="display: flex; align-items: center;"><input style="padding:0; margin: 0 5px 0 0" checked type="checkbox" id="red-ui-projects-dialog-screen-clear-context"> <label for="red-ui-projects-dialog-screen-clear-context" style="padding:0; margin: 0"> <span data-i18n="projects.create.clearContext"></span></label></span>').appendTo(row).i18n();

                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-empty red-ui-projects-dialog-screen-create-row-clone"></div>').appendTo(container);
                        $('<label for="red-ui-projects-dialog-screen-create-project-name">'+RED._("projects.create.project-name")+'</label>').appendTo(row);

                        var subrow = $('<div style="position:relative;"></div>').appendTo(row);
                        projectNameInput = $('<input id="red-ui-projects-dialog-screen-create-project-name" type="text"></input>').appendTo(subrow);
                        var projectNameStatus = $('<div class="red-ui-projects-dialog-screen-input-status"></div>').appendTo(subrow);

                        var projectNameInputChanged = false;
                        var projectNameLastChecked = "";
                        var projectNameValid;
                        var checkProjectName;
                        var autoInsertedName = "";


                        projectNameInput.on("change keyup paste",function() {
                            projectNameInputChanged = (projectNameInput.val() !== projectNameLastChecked);
                            if (checkProjectName) {
                                clearTimeout(checkProjectName);
                            } else if (projectNameInputChanged) {
                                projectNameStatus.empty();
                                $('<img src="red/images/spin.svg"/>').appendTo(projectNameStatus);
                                if (projectNameInput.val() === '') {
                                    validateForm();
                                    return;
                                }
                            }
                            checkProjectName = setTimeout(function() {
                                validateForm();
                                checkProjectName = null;
                            },300)
                        });
                        projectNameSublabel = $('<label class="red-ui-projects-edit-form-sublabel"><small>'+RED._("projects.create.must-contain")+'</small></label>').appendTo(row).find("small");

                        // Empty Project
                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-empty"></div>').appendTo(container);
                        $('<label for="red-ui-projects-dialog-screen-create-project-desc">'+RED._("projects.create.desc")+'</label>').appendTo(row);
                        projectSummaryInput = $('<input id="red-ui-projects-dialog-screen-create-project-desc" type="text">').appendTo(row);
                        $('<label class="red-ui-projects-edit-form-sublabel"><small>'+RED._("projects.create.opt")+'</small></label>').appendTo(row);

                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-empty"></div>').appendTo(container);
                        $('<label for="red-ui-projects-dialog-screen-create-project-file">'+RED._("projects.create.flow-file")+'</label>').appendTo(row);
                        subrow = $('<div style="position:relative;"></div>').appendTo(row);
                        projectFlowFileInput = $('<input id="red-ui-projects-dialog-screen-create-project-file" type="text">').val("flows.json")
                            .on("change keyup paste",validateForm)
                            .appendTo(subrow);
                        $('<div class="red-ui-projects-dialog-screen-input-status"></div>').appendTo(subrow);
                        $('<label class="red-ui-projects-edit-form-sublabel"><small>*.json</small></label>').appendTo(row);

                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-empty"></div>').appendTo(container);
                        $('<label>'+RED._("projects.create.credentials")+'</label>').appendTo(row);

                        var credentialsBox = $('<div class="red-ui-projects-dialog-credentials-box">').appendTo(row);
                        var credentialsRightBox = $('<div class="red-ui-projects-dialog-credentials-box-right">').appendTo(credentialsBox);
                        var credentialsLeftBox = $('<div class="red-ui-projects-dialog-credentials-box-left">').appendTo(credentialsBox);

                        var credentialsEnabledBox = $('<div class="form-row red-ui-projects-dialog-credentials-box-enabled"></div>').appendTo(credentialsLeftBox);
                        $('<label class="red-ui-projects-edit-form-inline-label"><input type="radio" name="projects-encryption-type" value="enabled" checked> <i class="fa fa-lock"></i> <span>'+RED._("projects.encryption-config.enable")+'</span></label>').appendTo(credentialsEnabledBox);
                        var credentialsDisabledBox = $('<div class="form-row red-ui-projects-dialog-credentials-box-disabled disabled"></div>').appendTo(credentialsLeftBox);
                        $('<label class="red-ui-projects-edit-form-inline-label"><input type="radio" name="projects-encryption-type" value="disabled"> <i class="fa fa-unlock"></i> <span>'+RED._("projects.encryption-config.disable")+'</span></label>').appendTo(credentialsDisabledBox);

                        credentialsLeftBox.find("input[name=projects-encryption-type]").on("click", function(e) {
                            var val = $(this).val();
                            var toEnable;
                            var toDisable;
                            if (val === 'enabled') {
                                toEnable = credentialsEnabledBox;
                                toDisable = credentialsDisabledBox;
                                $(".projects-encryption-enabled-row").show();
                                $(".projects-encryption-disabled-row").hide();
                                if ($("input[name=projects-encryption-key]:checked").val() === 'custom') {
                                    emptyProjectCredentialInput.trigger("focus");
                                }
                            } else {
                                toDisable = credentialsEnabledBox;
                                toEnable = credentialsDisabledBox;
                                $(".projects-encryption-enabled-row").hide();
                                $(".projects-encryption-disabled-row").show();

                            }
                            toEnable.removeClass("disabled");
                            toDisable.addClass("disabled");
                            validateForm();
                        })

                        row = $('<div class="form-row projects-encryption-enabled-row"></div>').appendTo(credentialsRightBox);
                        $('<label class="red-ui-projects-edit-form-inline-label">'+RED._("projects.create.encryption-key")+'</label>').appendTo(row);
                        // row = $('<div class="projects-encryption-enabled-row"></div>').appendTo(credentialsRightBox);
                        emptyProjectCredentialInput = $('<input type="password"></input>').appendTo(row);
                        emptyProjectCredentialInput.typedInput({type:"cred"});
                        emptyProjectCredentialInput.on("change keyup paste", validateForm);
                        $('<label class="red-ui-projects-edit-form-sublabel"><small>'+RED._("projects.create.desc0")+'</small></label>').appendTo(row);


                        row = $('<div class="form-row projects-encryption-disabled-row"></div>').hide().appendTo(credentialsRightBox);
                        $('<div class="" style="padding: 5px 20px;"><i class="fa fa-warning"></i> '+RED._("projects.create.desc1")+'</div>').appendTo(row);

                        credentialsRightBox.find("input[name=projects-encryption-key]").on("click", function() {
                            var val = $(this).val();
                            emptyProjectCredentialInput.attr("disabled",val === 'default');
                            if (val === "custom") {
                                emptyProjectCredentialInput.trigger("focus");
                            }
                            validateForm();
                        })

                        // Clone Project
                        row = $('<div class="hide form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-clone"></div>').appendTo(container);
                        $('<label for="red-ui-projects-dialog-screen-create-project-repo">'+RED._("projects.create.git-url")+'</label>').appendTo(row);
                        projectRepoInput = $('<input id="red-ui-projects-dialog-screen-create-project-repo" type="text" placeholder="https://git.example.com/path/my-project.git"></input>').appendTo(row);
                        $('<label id="red-ui-projects-dialog-screen-create-project-repo-label" class="red-ui-projects-edit-form-sublabel"><small>'+RED._("projects.create.protocols")+'</small></label>').appendTo(row);

                        var projectRepoChanged = false;
                        var lastProjectRepo = "";
                        projectRepoInput.on("change keyup paste",function() {
                            projectRepoChanged = true;
                            var repo = $(this).val();
                            if (lastProjectRepo !== repo) {
                                $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.create.protocols"));
                            }
                            lastProjectRepo = repo;

                            var m = /\/([^/]+?)(?:\.git)?$/.exec(repo);
                            if (m) {
                                var projectName = projectNameInput.val();
                                if (projectName === "" || projectName === autoInsertedName) {
                                    autoInsertedName = m[1];
                                    projectNameInput.val(autoInsertedName);
                                    projectNameInput.trigger("change");
                                }
                            }
                            validateForm();
                        });


                        var cloneAuthRows = $('<div class="hide red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-clone"></div>').hide().appendTo(container);
                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row-auth-error"></div>').hide().appendTo(cloneAuthRows);
                        $('<div><i class="fa fa-warning"></i> '+RED._("projects.create.auth-failed")+'</div>').appendTo(row);

                        // Repo credentials - username/password ----------------
                        row = $('<div class="hide form-row red-ui-projects-dialog-screen-create-row-creds"></div>').hide().appendTo(cloneAuthRows);

                        var subrow = $('<div style="width: calc(50% - 10px); display:inline-block;"></div>').appendTo(row);
                        $('<label for="red-ui-projects-dialog-screen-create-project-repo-user">'+RED._("projects.create.username")+'</label>').appendTo(subrow);
                        projectRepoUserInput = $('<input id="red-ui-projects-dialog-screen-create-project-repo-user" type="text"></input>').appendTo(subrow);

                        subrow = $('<div style="width: calc(50% - 10px); margin-left: 20px; display:inline-block;"></div>').appendTo(row);
                        $('<label for="red-ui-projects-dialog-screen-create-project-repo-pass">'+RED._("projects.create.password")+'</label>').appendTo(subrow);
                        projectRepoPasswordInput = $('<input style="width:100%" id="red-ui-projects-dialog-screen-create-project-repo-pass" type="password"></input>').appendTo(subrow);
                        projectRepoPasswordInput.typedInput({type:"cred"});
                        // -----------------------------------------------------

                        // Repo credentials - key/passphrase -------------------
                        row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-sshkey"></div>').hide().appendTo(cloneAuthRows);
                        subrow = $('<div style="width: calc(50% - 10px); display:inline-block;"></div>').appendTo(row);
                        $('<label for="red-ui-projects-dialog-screen-create-project-repo-passphrase">'+RED._("projects.create.ssh-key")+'</label>').appendTo(subrow);
                        projectRepoSSHKeySelect = $("<select>",{style:"width: 100%"}).appendTo(subrow);

                        $.getJSON("settings/user/keys", function(data) {
                            var count = 0;
                            data.keys.forEach(function(key) {
                                projectRepoSSHKeySelect.append($("<option></option>").val(key.name).text(key.name));
                                count++;
                            });
                            if (count === 0) {
                                projectRepoSSHKeySelect.addClass("input-error");
                                projectRepoSSHKeySelect.attr("disabled",true);
                                sshwarningRow.show();
                            } else {
                                projectRepoSSHKeySelect.removeClass("input-error");
                                projectRepoSSHKeySelect.attr("disabled",false);
                                sshwarningRow.hide();
                            }
                        });
                        subrow = $('<div style="width: calc(50% - 10px); margin-left: 20px; display:inline-block;"></div>').appendTo(row);
                        $('<label for="red-ui-projects-dialog-screen-create-project-repo-passphrase">'+RED._("projects.create.passphrase")+'</label>').appendTo(subrow);
                        projectRepoPassphrase = $('<input id="red-ui-projects-dialog-screen-create-project-repo-passphrase" type="password"></input>').appendTo(subrow);
                        projectRepoPassphrase.typedInput({type:"cred"});

                        subrow = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-sshkey"></div>').appendTo(cloneAuthRows);
                        var sshwarningRow = $('<div class="red-ui-projects-dialog-screen-create-row-auth-error-no-keys"></div>').hide().appendTo(subrow);
                        $('<div class="form-row"><i class="fa fa-warning"></i> '+RED._("projects.create.desc2")+'</div>').appendTo(sshwarningRow);
                        subrow = $('<div style="text-align: center">').appendTo(sshwarningRow);
                        $('<button type="button" class="red-ui-button red-ui-projects-dialog-button">'+RED._("projects.create.add-ssh-key")+'</button>').appendTo(subrow).on("click", function(e) {
                            e.preventDefault();
                            $('#red-ui-projects-dialog-cancel').trigger("click");
                            RED.userSettings.show('gitconfig');
                            setTimeout(function() {
                                $("#user-settings-gitconfig-add-key").trigger("click");
                            },500);
                        });
                        // -----------------------------------------------------


                        // Secret - clone
                        row = $('<div class="hide form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-clone"></div>').appendTo(container);
                        $('<label>'+RED._("projects.create.credentials-encryption-key")+'</label>').appendTo(row);
                        projectSecretInput = $('<input style="width:100%" type="password"></input>').appendTo(row);
                        projectSecretInput.typedInput({type:"cred"});

                        switch(options.screen||"empty") {
                            case "empty": createAsEmpty.trigger("click"); break;
                            case "open":  openProject.trigger("click"); break;
                            case "clone": createAsClone.trigger("click"); break;
                        }

                        setTimeout(function() {
                            if ((options.screen||"empty") !== "open") {
                                projectNameInput.trigger("focus");
                            } else {
                                $("#red-ui-projects-dialog-project-list-search").trigger("focus");
                            }
                        },50);
                        return container;
                    },
                    buttons: function(options) {
                        var initialLabel;
                        switch (options.screen||"empty") {
                            case "open": initialLabel = RED._("projects.create.open"); break;
                            case "empty": initialLabel = RED._("projects.create.create"); break;
                            case "clone": initialLabel = RED._("projects.create.clone"); break;
                        }
                        return [
                            {
                                id: "red-ui-projects-dialog-cancel",
                                text: RED._("common.label.cancel"),
                                click: function() {
                                    $( this ).dialog( "close" );
                                }
                            },
                            {
                                id: "red-ui-projects-dialog-create",
                                text: initialLabel,
                                class: "primary disabled",
                                disabled: true,
                                click: function() {
                                    var projectType = $(".red-ui-projects-dialog-screen-create-type.selected").data('type');
                                    var projectData = {
                                        name: projectNameInput.val(),
                                    }
                                    if (projectType === 'empty') {
                                        projectData.summary = projectSummaryInput.val();
                                        projectData.files = {
                                            flow: projectFlowFileInput.val()
                                        };
                                        var encryptionState = $("input[name=projects-encryption-type]:checked").val();
                                        if (encryptionState === 'enabled') {
                                            projectData.credentialSecret = emptyProjectCredentialInput.val();
                                        } else {
                                            // Disabled encryption by explicitly setting credSec to false
                                            projectData.credentialSecret = false;
                                        }


                                    } else if (projectType === 'copy') {
                                        projectData.copy = copyProject.name;
                                    } else if (projectType === 'clone') {
                                        projectData.credentialSecret = projectSecretInput.val();
                                        var repoUrl = projectRepoInput.val();
                                        var metaData = {};
                                        if (/^(?:ssh|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?/.test(repoUrl)) {
                                            var selected = projectRepoSSHKeySelect.val();//false;//getSelectedSSHKey(projectRepoSSHKeySelect);
                                            if ( selected ) {
                                                projectData.git = {
                                                    remotes: {
                                                        'origin': {
                                                            url: repoUrl,
                                                            keyFile: selected,
                                                            passphrase: projectRepoPassphrase.val()
                                                        }
                                                    }
                                                };
                                            }
                                            else {
                                                console.log(RED._("projects.create.cant-get-ssh-key-path"));
                                                return;
                                            }
                                        }
                                        else {
                                            projectData.git = {
                                                remotes: {
                                                    'origin': {
                                                        url: repoUrl,
                                                        username: projectRepoUserInput.val(),
                                                        password: projectRepoPasswordInput.val()
                                                    }
                                                }
                                            };
                                        }
                                    } else if (projectType === 'open') {
                                        var clearContext = $("#red-ui-projects-dialog-screen-clear-context").prop("checked")
                                        return switchProject(selectedProject.name, clearContext, function(err,data) {
                                            if (err) {
                                                if (err.code !== 'credentials_load_failed') {
                                                    console.log(RED._("projects.create.unexpected_error"),err)
                                                }
                                            }
                                        })
                                    }

                                    $(".red-ui-projects-dialog-screen-create-row-auth-error").hide();
                                    $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.create.protocols"));

                                    projectRepoUserInput.removeClass("input-error");
                                    projectRepoPasswordInput.removeClass("input-error");
                                    projectRepoSSHKeySelect.removeClass("input-error");
                                    projectRepoPassphrase.removeClass("input-error");

                                    RED.deploy.setDeployInflight(true);
                                    RED.projects.settings.switchProject(projectData.name);

                                    sendRequest({
                                        url: "projects",
                                        type: "POST",
                                        handleAuthFail: false,
                                        responses: {
                                            200: function(data) {
                                                dialog.dialog( "close" );
                                            },
                                            400: {
                                                'project_exists': function(error) {
                                                    console.log(RED._("projects.create.already-exists-2"));
                                                },
                                                'git_error': function(error) {
                                                    console.log(RED._("projects.create.git-error"),error);
                                                },
                                                'git_connection_failed': function(error) {
                                                    projectRepoInput.addClass("input-error");
                                                    $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.create.con-failed"));
                                                },
                                                'git_not_a_repository': function(error) {
                                                    projectRepoInput.addClass("input-error");
                                                    $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.create.not-git"));
                                                },
                                                'git_repository_not_found': function(error) {
                                                    projectRepoInput.addClass("input-error");
                                                    $("#red-ui-projects-dialog-screen-create-project-repo-label small").text(RED._("projects.create.no-resource"));
                                                },
                                                'git_auth_failed': function(error) {
                                                    $(".red-ui-projects-dialog-screen-create-row-auth-error").show();

                                                    projectRepoUserInput.addClass("input-error");
                                                    projectRepoPasswordInput.addClass("input-error");
                                                    // getRepoAuthDetails(req);
                                                    projectRepoSSHKeySelect.addClass("input-error");
                                                    projectRepoPassphrase.addClass("input-error");
                                                },
                                                'missing_flow_file': function(error) {
                                                    // This is handled via a runtime notification.
                                                    dialog.dialog("close");
                                                },
                                                'missing_package_file': function(error) {
                                                    // This is handled via a runtime notification.
                                                    dialog.dialog("close");
                                                },
                                                'project_empty': function(error) {
                                                    // This is handled via a runtime notification.
                                                    dialog.dialog("close");
                                                },
                                                'credentials_load_failed': function(error) {
                                                    // This is handled via a runtime notification.
                                                    dialog.dialog("close");
                                                },
                                                '*': function(error) {
                                                    reportUnexpectedError(error);
                                                    $( dialog ).dialog( "close" );
                                                }
                                            }
                                        }
                                    },projectData).then(function() {
                                        RED.events.emit("project:change", {name:name});
                                    }).always(function() {
                                        setTimeout(function() {
                                            RED.deploy.setDeployInflight(false);
                                        },500);
                                    })
                                }
                            }
                        ]
                    }
                }
            })()
        }
    }

    function switchProject(name,clearContext,done) {
        RED.deploy.setDeployInflight(true);
        RED.projects.settings.switchProject(name);
        sendRequest({
            url: "projects/"+name,
            type: "PUT",
            responses: {
                200: function(data) {
                    done(null,data);
                },
                400: {
                    'credentials_load_failed': function(data) {
                        dialog.dialog( "close" );
                        RED.events.emit("project:change", {name:name});
                        done(null,data);
                    },
                    '*': done
                },
            }
        },{active:true, clearContext:clearContext}).then(function() {
            dialog.dialog( "close" );
            RED.events.emit("project:change", {name:name});
        }).always(function() {
            setTimeout(function() {
                RED.deploy.setDeployInflight(false);
            },500);
        })
    }

    function deleteProject(row,name,done) {
        var cover = $('<div class="red-ui-projects-dialog-project-list-entry-delete-confirm"></div>').on("click", function(evt) { evt.stopPropagation(); }).appendTo(row);
        $('<span>').text(RED._("projects.delete.confirm")).appendTo(cover);
        $('<button type="button" class="red-ui-button red-ui-projects-dialog-button">'+RED._("common.label.cancel")+'</button>')
            .appendTo(cover)
            .on("click", function(e) {
                e.stopPropagation();
                cover.remove();
                done(true);
            });
        $('<button type="button" class="red-ui-button red-ui-projects-dialog-button primary">'+RED._("common.label.delete")+'</button>')
            .appendTo(cover)
            .on("click", function(e) {
                e.stopPropagation();
                cover.remove();
                sendRequest({
                    url: "projects/"+name,
                    type: "DELETE",
                    responses: {
                        200: function(data) {
                            done(false);
                        },
                        400: {
                            'unexpected_error': function(error) {
                                cover.remove();
                                done(true);
                            }
                        }
                    }
                });
            });

        setTimeout(function() {
            cover.css("left",0);
        },50);
        //
    }

    function show(s,options) {
        if (!dialog) {
            RED.projects.init();
        }
        var screen = screens[s];
        var container = screen.content(options||{});

        dialogBody.empty();
        var buttons = screen.buttons;
        if (typeof buttons === 'function') {
            buttons = buttons(options||{});
        }



        dialog.dialog('option','buttons',buttons);
        dialogBody.append(container);


        var dialogHeight = 590;
        var winHeight = $(window).height();
        if (winHeight < 750) {
            dialogHeight = 590 - (750 - winHeight);
        }
        $(".red-ui-projects-dialog-box").height(dialogHeight);
        $(".red-ui-projects-dialog-project-list-inner-container").height(Math.max(500,dialogHeight) - 210);
        dialog.dialog('option','title',screen.title||"");
        dialog.dialog("open");
    }

    function createProjectList(options) {
        options = options||{};
        var height = options.height || "200px";
        var container = $('<div></div>',{class:"red-ui-projects-dialog-project-list-container" });
        var filterTerm = "";

        var searchDiv = $("<div>",{class:"red-ui-search-container"}).appendTo(container);
        var searchInput = $('<input id="red-ui-projects-dialog-project-list-search" type="text" placeholder="'+RED._("projects.create-project-list.search")+'">').appendTo(searchDiv).searchBox({
            //data-i18n="[placeholder]menu.label.searchInput"
            delay: 200,
            change: function() {
                filterTerm = $(this).val().toLowerCase();
                list.editableList('filter');
                if (selectedListItem && !selectedListItem.is(":visible")) {
                    selectedListItem.children().children().removeClass('selected');
                    selectedListItem = list.children(":visible").first();
                    selectedListItem.children().children().addClass('selected');
                    if (options.select) {
                        options.select(selectedListItem.children().data('data'));
                    }
                } else {
                    selectedListItem = list.children(":visible").first();
                    selectedListItem.children().children().addClass('selected');
                    if (options.select) {
                        options.select(selectedListItem.children().data('data'));
                    }
                }
                ensureSelectedIsVisible();
            }
        });
        var selectedListItem;

        searchInput.on('keydown',function(evt) {
            if (evt.keyCode === 40) {
                evt.preventDefault();
                // Down
                var next = selectedListItem;
                if (selectedListItem) {
                    do {
                        next = next.next();
                    } while(next.length !== 0 && !next.is(":visible"));
                    if (next.length === 0) {
                        return;
                    }
                    selectedListItem.children().children().removeClass('selected');
                } else {
                    next = list.children(":visible").first();
                }
                selectedListItem = next;
                selectedListItem.children().children().addClass('selected');
                if (options.select) {
                    options.select(selectedListItem.children().data('data'));
                }
                ensureSelectedIsVisible();
            } else if (evt.keyCode === 38) {
                evt.preventDefault();
                // Up
                var prev = selectedListItem;
                if (selectedListItem) {
                    do {
                        prev = prev.prev();
                    } while(prev.length !== 0 && !prev.is(":visible"));
                    if (prev.length === 0) {
                        return;
                    }
                    selectedListItem.children().children().removeClass('selected');
                } else {
                    prev = list.children(":visible").first();
                }
                selectedListItem = prev;
                selectedListItem.children().children().addClass('selected');
                if (options.select) {
                    options.select(selectedListItem.children().data('data'));
                }
                ensureSelectedIsVisible();
            } else if (evt.keyCode === 13) {
                evt.preventDefault();
                // Enter
                if (selectedListItem) {
                    if (options.dblclick) {
                        options.dblclick(selectedListItem.children().data('data'));
                    }
                }
            }
        });

        searchInput.i18n();

        var ensureSelectedIsVisible = function() {
            var selectedEntry = list.find(".red-ui-projects-dialog-project-list-entry.selected").parent().parent();
            if (selectedEntry.length === 1) {
                var scrollWindow = listContainer;
                var scrollHeight = scrollWindow.height();
                var scrollOffset = scrollWindow.scrollTop();
                var y = selectedEntry.position().top;
                var h = selectedEntry.height();
                if (y+h > scrollHeight) {
                    scrollWindow.animate({scrollTop: '-='+(scrollHeight-y-h)},50);
                } else if (y<0) {
                    scrollWindow.animate({scrollTop: '+='+y},50);
                }
            }
        }

        var listContainer = $('<div></div>',{class:"red-ui-projects-dialog-project-list-inner-container" }).appendTo(container);

        var list = $('<ol>',{class:"red-ui-projects-dialog-project-list"}).appendTo(listContainer).editableList({
            addButton: false,
            height:"auto",
            scrollOnAdd: false,
            addItem: function(row,index,entry) {
                var header = $('<div></div>',{class:"red-ui-projects-dialog-project-list-entry"}).appendTo(row);
                $('<span class="red-ui-projects-dialog-project-list-entry-icon"><i class="fa fa-archive"></i></span>').appendTo(header);
                $('<span class="red-ui-projects-dialog-project-list-entry-name" style=""></span>').text(entry.name).appendTo(header);
                if (activeProject && activeProject.name === entry.name) {
                    header.addClass("projects-list-entry-current");
                    $('<span class="red-ui-projects-dialog-project-list-entry-current">'+RED._("projects.create-project-list.current")+'</span>').appendTo(header);
                    if (options.canSelectActive === false) {
                        // active project cannot be selected; so skip the rest
                        return
                    }
                }

                header.addClass("selectable");

                var tools = $('<div class="red-ui-projects-dialog-project-list-entry-tools"></div>').appendTo(header);
                $('<button type="button" class="red-ui-button red-ui-projects-dialog-button red-ui-button-small" style="float: right;"><i class="fa fa-trash"></i></button>')
                    .appendTo(tools)
                    .on("click", function(e) {
                        e.stopPropagation();
                        e.preventDefault();
                        deleteProject(row,entry.name, function(cancelled) {
                            if (!cancelled) {
                                row.fadeOut(300,function() {
                                    list.editableList('removeItem',entry);
                                    if (options.delete) {
                                        options.delete(entry);
                                    }
                                });
                            }
                        })
                    });


                row.on("click", function(evt) {
                    $('.red-ui-projects-dialog-project-list-entry').removeClass('selected');
                    header.addClass('selected');
                    selectedListItem = row.parent();
                    if (options.select) {
                        options.select(entry);
                    }
                    ensureSelectedIsVisible();
                    searchInput.trigger("focus");
                })
                if (options.dblclick) {
                    row.on("dblclick", function(evt) {
                        evt.preventDefault();
                        options.dblclick(entry);
                    })
                }
            },
            filter: function(data) {
                if (filterTerm === "") { return true; }
                return data.name.toLowerCase().indexOf(filterTerm) !== -1;
            }
        });
        $.getJSON("projects", function(data) {
            data.projects.forEach(function(project) {
                list.editableList('addItem',{name:project});
            });
        })
        return container;
    }



    function requireCleanWorkspace(done) {
        if (RED.nodes.dirty()) {
            var message = RED._("projects.require-clean.confirm");
            var cleanNotification = RED.notify(message,{
                type:"info",
                fixed: true,
                modal: true,
                buttons: [
                    {
                        //id: "node-dialog-delete",
                        //class: 'leftButton',
                        text: RED._("common.label.cancel"),
                        click: function() {
                            cleanNotification.close();
                            done(true);
                        }
                    },{
                        text: RED._("common.label.cont"),
                        click: function() {
                            cleanNotification.close();
                            done(false);
                        }
                    }
                ]
            });

        }
    }

    function sendRequest(options,body) {
        // dialogBody.hide();
        // console.log(options.url,body);
        if (options.requireCleanWorkspace && RED.nodes.dirty()) {
            var thenCallback;
            var alwaysCallback;
            requireCleanWorkspace(function(cancelled) {
                if (cancelled) {
                    if (options.cancel) {
                        options.cancel();
                        if (alwaysCallback) {
                            alwaysCallback();
                        }
                    }
                } else {
                    delete options.requireCleanWorkspace;
                    sendRequest(options,body).then(function() {
                        if (thenCallback) {
                            thenCallback();
                        }
                    }).always(function() {
                        if (alwaysCallback) {
                            alwaysCallback();
                        }

                    })
                }
            })
            // What follows is a very hacky Promise-like api thats good enough
            // for our needs.
            return {
                then: function(done) {
                    thenCallback = done;
                    return { always: function(done) { alwaysCallback = done; }}
                 },
                always: function(done) { alwaysCallback = done; }
            }
        }

        var start = Date.now();
        // TODO: this is specific to the dialog-based requests
        $(".red-ui-component-spinner").show();
        $("#red-ui-projects-dialog").parent().find(".ui-dialog-buttonset").children().css("visibility","hidden")
        if (body) {
            options.data = JSON.stringify(body);
            options.contentType = "application/json; charset=utf-8";
        }
        var resultCallback;
        var resultCallbackArgs;
        return $.ajax(options).done(function(data,textStatus,xhr) {
            if (options.responses && options.responses[200]) {
                resultCallback = options.responses[200];
                resultCallbackArgs = data;
            }
        }).fail(function(xhr,textStatus,err) {
            var responses;

            if (options.responses && options.responses[xhr.status]) {
                responses = options.responses[xhr.status];
                if (typeof responses === 'function') {
                    resultCallback = responses;
                    resultCallbackArgs = {error:responses.statusText};
                    return;
                } else if (options.handleAuthFail !== false && (xhr.responseJSON.code === 'git_auth_failed' || xhr.responseJSON.code === 'git_host_key_verification_failed')) {
                    if (xhr.responseJSON.code === 'git_auth_failed') {
                        var url = activeProject.git.remotes[xhr.responseJSON.remote||options.remote||'origin'].fetch;

                        var message = $('<div>'+
                        '<div class="form-row">'+RED._("projects.send-req.auth-req")+':</div>'+
                        '<div class="form-row"><div style="margin-left: 20px;">'+url+'</div></div>'+
                        '</div>');

                        var isSSH = false;
                        if (/^https?:\/\//.test(url)) {
                            $('<div class="form-row"><label for="projects-user-auth-username">'+RED._("projects.send-req.username")+'</label><input id="projects-user-auth-username" type="text"></input></div>'+
                            '<div class="form-row"><label for="projects-user-auth-password">'+RED._("projects.send-req.password")+'</label><input id="projects-user-auth-password" type="password"></input></div>').appendTo(message);
                            message.find("#projects-user-auth-password").typedInput({type:"cred"})
                        } else if (/^(?:ssh|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?/.test(url)) {
                            isSSH = true;
                            var row = $('<div class="form-row"></div>').appendTo(message);
                            $('<label for="projects-user-auth-key">SSH Key</label>').appendTo(row);
                            var projectRepoSSHKeySelect = $('<select id="projects-user-auth-key">').width('70%').appendTo(row);
                            $.getJSON("settings/user/keys", function(data) {
                                var count = 0;
                                data.keys.forEach(function(key) {
                                    projectRepoSSHKeySelect.append($("<option></option>").val(key.name).text(key.name));
                                    count++;
                                });
                                if (count === 0) {
                                    //TODO: handle no keys yet setup
                                }
                            });
                            row = $('<div class="form-row"></div>').appendTo(message);
                            $('<label for="projects-user-auth-passphrase">'+RED._("projects.send-req.passphrase")+'</label>').appendTo(row);
                            $('<input id="projects-user-auth-passphrase" type="password"></input>').appendTo(row).typedInput({type:"cred"});
                        }

                        var notification = RED.notify(message,{
                            type:"error",
                            fixed: true,
                            modal: true,
                            buttons: [
                                {
                                    //id: "node-dialog-delete",
                                    //class: 'leftButton',
                                    text: RED._("common.label.cancel"),
                                    click: function() {
                                        notification.close();
                                    }
                                },{
                                    text: '<span><i class="fa fa-refresh"></i> ' +RED._("projects.send-req.retry") +'</span>',
                                    click: function() {
                                        body = body || {};
                                        var authBody = {};
                                        if (isSSH) {
                                            authBody.keyFile = $('#projects-user-auth-key').val();
                                            authBody.passphrase = $('#projects-user-auth-passphrase').val();
                                        } else {
                                            authBody.username = $('#projects-user-auth-username').val();
                                            authBody.password = $('#projects-user-auth-password').val();
                                        }
                                        var done = function(err) {
                                            if (err) {
                                                console.log(RED._("projects.send-req.update-failed"));
                                                console.log(err);
                                            } else {
                                                sendRequest(options,body);
                                                notification.close();
                                            }

                                        }
                                        sendRequest({
                                            url: "projects/"+activeProject.name+"/remotes/"+(xhr.responseJSON.remote||options.remote||'origin'),
                                            type: "PUT",
                                            responses: {
                                                0: function(error) {
                                                    done(error,null);
                                                },
                                                200: function(data) {
                                                    done(null,data);
                                                },
                                                400: {
                                                    'unexpected_error': function(error) {
                                                        done(error,null);
                                                    }
                                                },
                                            }
                                        },{auth:authBody});
                                    }
                                }
                            ]
                        });
                        return;
                    } else if (xhr.responseJSON.code === 'git_host_key_verification_failed') {
                        var message = $('<div>'+
                            '<div class="form-row">'+RED._("projects.send-req.host-key-verify-failed")+'</div>'+
                            '</div>');
                        var notification = RED.notify(message,{
                            type:"error",
                            fixed: true,
                            modal: true,
                            buttons: [
                                {
                                    text: RED._("common.label.close"),
                                    click: function() {
                                        notification.close();
                                    }
                                }
                            ]
                        });
                        return;
                    }
                } else if (responses[xhr.responseJSON.code]) {
                    resultCallback = responses[xhr.responseJSON.code];
                    resultCallbackArgs = xhr.responseJSON;
                    return;
                } else if (responses['*']) {
                    resultCallback = responses['*'];
                    resultCallbackArgs = xhr.responseJSON;
                    return;
                }
            }
            console.log(responses)
            console.log(RED._("projects.send-req.unhandled")+":");
            console.log(xhr);
            console.log(textStatus);
            console.log(err);
        }).always(function() {
            var delta = Date.now() - start;
            delta = Math.max(0,500-delta);
            setTimeout(function() {
                // dialogBody.show();
                $(".red-ui-component-spinner").hide();
                $("#red-ui-projects-dialog").parent().find(".ui-dialog-buttonset").children().css("visibility","")
                if (resultCallback) {
                    resultCallback(resultCallbackArgs)
                }
            },delta);
        });
    }

    function createBranchList(options) {
        var branchFilterTerm = "";
        var branchFilterCreateItem;
        var branches = [];
        var branchNames = new Set();
        var remotes = [];
        var branchPrefix = "";
        var container = $('<div class="red-ui-projects-branch-list">').appendTo(options.container);

        var branchFilter = $('<input type="text">').attr('placeholder',options.placeholder).appendTo(container).searchBox({
            delay: 200,
            change: function() {
                branchFilterTerm = $(this).val();
                // if there is a / then
                //  - check what preceeds it is a known remote

                var valid = false;
                var hasRemote = false;
                var m = /^([^/]+)\/[^/.~*?\[]/.exec(branchFilterTerm);
                if (m && remotes.indexOf(m[1]) > -1) {
                    valid = true;
                    hasRemote = true;
                }

                if (!valid && /(\.\.|\/\.|[?*[~^: \\]|\/\/|\/.$|\/$)/.test(branchFilterTerm)) {
                    if (!branchFilterCreateItem.hasClass("input-error")) {
                        branchFilterCreateItem.addClass("input-error");
                        branchFilterCreateItem.find("i").addClass("fa-warning").removeClass("fa-code-fork");
                    }
                    branchFilterCreateItem.find("span").text(RED._("projects.create-branch-list.invalid")+": "+(hasRemote?"":branchPrefix)+branchFilterTerm);
                } else {
                    if (branchFilterCreateItem.hasClass("input-error")) {
                        branchFilterCreateItem.removeClass("input-error");
                        branchFilterCreateItem.find("i").removeClass("fa-warning").addClass("fa-code-fork");
                    }
                    branchFilterCreateItem.find("span").text(RED._("projects.create-branch-list.create")+":");
                    branchFilterCreateItem.find(".red-ui-sidebar-vc-branch-list-entry-create-name").text((hasRemote?"":branchPrefix)+branchFilterTerm);
                }
                branchList.editableList("filter");
            }
        });
        var branchList = $("<ol>",{style:"height: 130px;"}).appendTo(container);
        branchList.editableList({
            addButton: false,
            scrollOnAdd: false,
            addItem: function(row,index,entry) {
                var container = $('<div class="red-ui-sidebar-vc-branch-list-entry">').appendTo(row);
                if (!entry.hasOwnProperty('commit')) {
                    branchFilterCreateItem = container;
                    $('<i class="fa fa-code-fork"></i>').appendTo(container);
                    $('<span>').text(RED._("projects.create-branch-list.create")+":").appendTo(container);
                    $('<div class="red-ui-sidebar-vc-branch-list-entry-create-name" style="margin-left: 10px;">').text(entry.name).appendTo(container);
                } else {
                    $('<i class="fa fa-code-fork"></i>').appendTo(container);
                    $('<span>').text(entry.name).appendTo(container);
                    if (entry.current) {
                        container.addClass("selected");
                        $('<span class="current"></span>').text(options.currentLabel||RED._("projects.create-branch-list.current")).appendTo(container);
                    }
                }
                container.on("click", function(evt) {
                    evt.preventDefault();
                    if ($(this).hasClass('input-error')) {
                        return;
                    }
                    var body = {};
                    if (!entry.hasOwnProperty('commit')) {
                        body.name = branchFilter.val();
                        body.create = true;

                        if (options.remotes) {
                            var m = /^([^/]+)\/[^/.~*?\[]/.exec(body.name);
                            if (!m || remotes.indexOf(m[1]) === -1) {
                                body.name = remotes[0]+"/"+body.name;
                            }
                        }
                    } else {
                        if ($(this).hasClass('selected')) {
                            body.current = true;
                        }
                        body.name = entry.name;
                    }
                    if (options.onselect) {
                        options.onselect(body);
                    }
                });
            },
            filter: function(data) {
                var isCreateEntry = (!data.hasOwnProperty('commit'));
                var filterTerm = branchFilterTerm;
                if (remotes.length > 0) {
                    var m = /^([^/]+)\/[^/.~*?\[]/.exec(filterTerm);
                    if (filterTerm !== "" && (!m || remotes.indexOf(m[1]) == -1)) {
                        filterTerm = remotes[0]+"/"+filterTerm;
                    }
                }
                return (
                            isCreateEntry &&
                            (
                                filterTerm !== "" && !branchNames.has(filterTerm)
                            )
                     ) ||
                     (
                         !isCreateEntry &&
                         data.name.indexOf(branchFilterTerm) !== -1
                     );
            }
        });
        return {
            refresh: function(url) {
                branchFilter.searchBox("value","");
                branchList.editableList('empty');
                var start = Date.now();
                var spinner = addSpinnerOverlay(container).addClass("red-ui-component-spinner-contain");
                if (options.remotes) {
                    remotes = options.remotes();
                    branchPrefix = remotes[0]+"/";
                } else {
                    branchPrefix = "";
                    remotes = [];
                }
                branchNames = new Set();
                sendRequest({
                    url: url,
                    type: "GET",
                    responses: {
                        0: function(error) {
                            console.log(error);
                        },
                        200: function(result) {
                            branches = result.branches;
                            result.branches.forEach(function(b) {
                                branchList.editableList('addItem',b);
                                branchNames.add(b.name);
                            });
                            branchList.editableList('addItem',{});
                            setTimeout(function() {
                                spinner.remove();
                            },Math.max(300-(Date.now() - start),0));
                        },
                        400: {
                            'git_connection_failed': function(error) {
                                RED.notify(error.message,'error');
                            },
                            'git_not_a_repository': function(error) {
                                RED.notify(error.message,'error');
                            },
                            'git_repository_not_found': function(error) {
                                RED.notify(error.message,'error');
                            },
                            'unexpected_error': function(error) {
                                reportUnexpectedError(error);
                            }
                        }
                    }
                })
            },
            // addItem: function(data) { branchList.editableList('addItem',data) },
            filter: function() { branchList.editableList('filter') },
            focus: function() { branchFilter.trigger("focus") }
        }
    }

    function addSpinnerOverlay(container) {
        var spinner = $('<div class="red-ui-component-spinner"><img src="red/images/spin.svg"/></div>').appendTo(container);
        return spinner;
    }

    function init() {
        dialog = $('<div id="red-ui-projects-dialog" class="hide red-ui-projects-edit-form"><div class="red-ui-projects-dialog-box"><form class="form-horizontal"></form><div class="red-ui-component-spinner hide"><img src="red/images/spin.svg"/></div></div></div>')
            .appendTo("#red-ui-editor")
            .dialog({
                modal: true,
                autoOpen: false,
                width: 600,
                resizable: false,
                open: function(e) {
                    RED.keyboard.disable();
                },
                close: function(e) {
                    RED.keyboard.enable();
                },
                classes: {
                    "ui-dialog": "red-ui-editor-dialog",
                    "ui-dialog-titlebar-close": "hide",
                    "ui-widget-overlay": "red-ui-editor-dialog"
                }
            });
        dialogBody = dialog.find("form");

        RED.actions.add("core:new-project",RED.projects.newProject);
        RED.actions.add("core:open-project",RED.projects.selectProject);
        RED.actions.add("core:show-project-settings",RED.projects.settings.show);
        var projectsAPI = {
            sendRequest:sendRequest,
            createBranchList:createBranchList,
            addSpinnerOverlay:addSpinnerOverlay,
            reportUnexpectedError:reportUnexpectedError
        };
        RED.projects.settings.init(projectsAPI);
        RED.projects.userSettings.init(projectsAPI);
        RED.sidebar.versionControl.init(projectsAPI);
        initScreens();
        // initSidebar();
    }

    function createDefaultFileSet() {
        if (!activeProject) {
            throw new Error(RED._("projects.create-default-file-set.no-active"));
        } else if (!activeProject.empty) {
            throw new Error(RED._("projects.create-default-file-set.no-empty"));
        }
        if (!RED.user.hasPermission("projects.write")) {
            RED.notify(RED._("user.errors.notAuthorized"),"error");
            return;
        }
        createProjectOptions = {};
        show('default-files',{existingProject: true});
    }
    function createDefaultPackageFile() {
        RED.deploy.setDeployInflight(true);
        RED.projects.settings.switchProject(activeProject.name);

        var method = "PUT";
        var url = "projects/"+activeProject.name;
        var createProjectOptions = {
            initialise: true
        };
        sendRequest({
            url: url,
            type: method,
            requireCleanWorkspace: true,
            handleAuthFail: false,
            responses: {
                200: function(data) { },
                400: {
                    'git_error': function(error) {
                        console.log(RED._("projects.create-default-file-set.git-error"),error);
                    },
                    'missing_flow_file': function(error) {
                        // This is a natural next error - but let the runtime event
                        // trigger the dialog rather than double-report it.
                        $( dialog ).dialog( "close" );
                    },
                    '*': function(error) {
                        reportUnexpectedError(error);
                        $( dialog ).dialog( "close" );
                    }
                }
            }
        },createProjectOptions).always(function() {
            setTimeout(function() {
                RED.deploy.setDeployInflight(false);
            },500);
        })
    }

    function refresh(done) {
        $.getJSON("projects",function(data) {
            if (data.active) {
                $.getJSON("projects/"+data.active, function(project) {
                    activeProject = project;
                    RED.events.emit("projects:load",activeProject);
                    RED.sidebar.versionControl.refresh(true);
                    if (done) {
                        done(activeProject);
                    }
                });
            } else {
                if (done) {
                    done(null);
                }
            }
        });
    }


    function showNewProjectScreen() {
        createProjectOptions = {};
        if (!activeProject) {
            show('welcome');
        } else {
            show('create',{screen:'empty'})
        }
    }

    return {
        init: init,
        showStartup: function() {
            console.warn("showStartup")
            if (!RED.user.hasPermission("projects.write")) {
                RED.notify(RED._("user.errors.notAuthorized"),"error");
                return;
            }
            show('welcome');
        },
        newProject: function() {
            if (!RED.user.hasPermission("projects.write")) {
                RED.notify(RED._("user.errors.notAuthorized"),"error");
                return;
            }

            if (RED.nodes.dirty()) {
                return requireCleanWorkspace(function(cancelled) {
                    if (!cancelled) {
                        showNewProjectScreen();
                    }
                })
            } else {
                showNewProjectScreen();
            }
        },
        selectProject: function() {
            if (!RED.user.hasPermission("projects.write")) {
                RED.notify(RED._("user.errors.notAuthorized"),"error");
                return;
            }
            if (RED.nodes.dirty()) {
                return requireCleanWorkspace(function(cancelled) {
                    if (!cancelled) {
                        show('create',{screen:'open'})
                    }
                })
            } else {
                show('create',{screen:'open'})
            }
        },
        showCredentialsPrompt: function() { //TODO: rename this function
            if (!RED.user.hasPermission("projects.write")) {
                RED.notify(RED._("user.errors.notAuthorized"),"error");
                return;
            }
            RED.projects.settings.show('settings');
        },
        showFilesPrompt: function() { //TODO: rename this function
            if (!RED.user.hasPermission("projects.write")) {
                RED.notify(RED._("user.errors.notAuthorized"),"error");
                return;
            }
            RED.projects.settings.show('settings');
            setTimeout(function() {
                $("#project-settings-tab-settings-file-edit").trigger("click");
            },200)
        },
        showProjectDependencies: function() {
            RED.projects.settings.show('deps');
        },
        createDefaultFileSet: createDefaultFileSet,
        createDefaultPackageFile: createDefaultPackageFile,
        // showSidebar: showSidebar,
        refresh: refresh,
        editProject: function() {
            RED.projects.settings.show();
        },
        getActiveProject: function() {
            return activeProject;
        }
    }
})();
;/**
 * 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.
 **/

RED.projects.settings = (function() {

    var trayWidth = 700;
    var settingsVisible = false;

    var panes = [];

    function addPane(options) {
        panes.push(options);
    }

    // TODO: DRY - tab-info.js
    function addTargetToExternalLinks(el) {
        $(el).find("a").each(function(el) {
            var href = $(this).attr('href');
            if (/^https?:/.test(href)) {
                $(this).attr('target','_blank');
            }
        });
        return el;
    }

    function show(initialTab) {
        if (settingsVisible) {
            return;
        }
        if (!RED.user.hasPermission("projects.write")) {
            RED.notify(RED._("user.errors.notAuthorized"),"error");
            return;
        }

        settingsVisible = true;

        var trayOptions = {
            title: RED._("sidebar.project.projectSettings.title"),
            buttons: [
                {
                    id: "node-dialog-ok",
                    text: RED._("common.label.close"),
                    class: "primary",
                    click: function() {
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                trayWidth = dimensions.width;
            },
            open: function(tray) {
                var project = RED.projects.getActiveProject();

                var trayBody = tray.find('.red-ui-tray-body');
                var settingsContent = $('<div></div>').appendTo(trayBody);
                var tabContainer = $('<div></div>',{class:"red-ui-settings-tabs-container"}).appendTo(settingsContent);

                $('<ul></ul>',{id:"user-settings-tabs"}).appendTo(tabContainer);
                var settingsTabs = RED.tabs.create({
                    id: "user-settings-tabs",
                    vertical: true,
                    onchange: function(tab) {
                        setTimeout(function() {
                            tabContents.children().hide();
                            $("#" + tab.id).show();
                            if (tab.pane.focus) {
                                tab.pane.focus();
                            }
                        },50);
                    }
                });
                var tabContents = $('<div></div>',{class:"red-ui-settings-tabs-content"}).appendTo(settingsContent);

                panes.forEach(function(pane) {
                    settingsTabs.addTab({
                        id: "red-ui-project-settings-tab-"+pane.id,
                        label: pane.title,
                        pane: pane
                    });
                    pane.get(project).hide().appendTo(tabContents);
                });
                settingsContent.i18n();
                settingsTabs.activateTab("red-ui-project-settings-tab-"+(initialTab||'main'))
                $("#red-ui-sidebar-shade").show();
            },
            close: function() {
                settingsVisible = false;
                panes.forEach(function(pane) {
                    if (pane.close) {
                        pane.close();
                    }
                });
                $("#red-ui-sidebar-shade").hide();

            },
            show: function() {}
        }
        if (trayWidth !== null) {
            trayOptions.width = trayWidth;
        }
        RED.tray.show(trayOptions);
    }

    function editDescription(activeProject, container) {
        RED.editor.editMarkdown({
            title: RED._('sidebar.project.editDescription'),
            header: $('<span><i class="fa fa-book"></i> README.md</span>'),
            value: activeProject.description,
            stateId: "sidebar.project.editDescription",
            complete: function(v) {
                container.empty();
                var spinner = utils.addSpinnerOverlay(container);
                var done = function(err,res) {
                    if (err) {
                        return editDescription(activeProject, container);
                    }
                    activeProject.description = v;
                    updateProjectDescription(activeProject, container);
                }
                utils.sendRequest({
                    url: "projects/"+activeProject.name,
                    type: "PUT",
                    responses: {
                        0: function(error) {
                            done(error,null);
                        },
                        200: function(data) {
                            done(null,data);
                            RED.sidebar.versionControl.refresh(true);
                        },
                        400: {
                            '*': function(error) {
                                utils.reportUnexpectedError(error);
                                done(error,null);
                            }
                        },
                    }
                },{description:v}).always(function() {
                    spinner.remove();
                });
            }
        });
    }
    function updateProjectDescription(activeProject, container) {
        container.empty();
        var desc;
        if (activeProject.description) {
            desc = RED.utils.renderMarkdown(activeProject.description);
        } else {
            desc = '<span class="red-ui-help-info-none">' + RED._("sidebar.project.noDescriptionAvailable") + '</span>';
        }
        var description = addTargetToExternalLinks($('<span class="red-ui-text-bidi-aware" dir=\"'+RED.text.bidi.resolveBaseTextDir(desc)+'">'+desc+'</span>')).appendTo(container);
        description.find(".red-ui-text-bidi-aware").contents().filter(function() { return this.nodeType === 3 && this.textContent.trim() !== "" }).wrap( "<span></span>" );
        setTimeout(function () {
            RED.editor.mermaid.render()
        }, 200);
    }

    function editSummary(activeProject, summary, container, version, versionContainer) {
        var editButton = container.prev();
        editButton.hide();
        container.empty();
        versionContainer.empty();
        var bg = $('<span class="button-row" style="position: relative; float: right; margin-right:0;"></span>').appendTo(container);
        var input = $('<input type="text" style="width: calc(100% - 150px); margin-right: 10px;">').val(summary||"").appendTo(container);
        var versionInput = $('<input type="text" style="width: calc(100% - 150px); margin-right: 10px;">').val(version||"").appendTo(versionContainer);

        $('<button class="red-ui-button">' + RED._("common.label.cancel") + '</button>')
            .appendTo(bg)
            .on("click", function(evt) {
                evt.preventDefault();
                updateProjectSummary(activeProject.summary, container);
                updateProjectVersion(activeProject.version, versionContainer);
                editButton.show();
            });
        $('<button class="red-ui-button">' + RED._("common.label.save") + '</button>')
            .appendTo(bg)
            .on("click", function(evt) {
                evt.preventDefault();
                var newSummary = input.val();
                var newVersion = versionInput.val();
                updateProjectSummary(newSummary, container);
                updateProjectVersion(newVersion, versionContainer);
                var spinner = utils.addSpinnerOverlay(container).addClass('red-ui-component-spinner-contain');
                var done = function(err,res) {
                    if (err) {
                        spinner.remove();
                        return editSummary(activeProject, summary, container, version, versionContainer);
                    }
                    activeProject.summary = newSummary;
                    activeProject.version = newVersion;
                    spinner.remove();
                    updateProjectSummary(activeProject.summary, container);
                    updateProjectVersion(activeProject.version, versionContainer);
                    editButton.show();
                }
                utils.sendRequest({
                    url: "projects/"+activeProject.name,
                    type: "PUT",
                    responses: {
                        0: function(error) {
                            done(error,null);
                        },
                        200: function(data) {
                            RED.sidebar.versionControl.refresh(true);
                            done(null,data);
                        },
                        400: {
                            '*': function(error) {
                                utils.reportUnexpectedError(error);
                                done(error,null);
                            }
                        },
                    }
                },{summary:newSummary, version: newVersion});
            });
    }
    function updateProjectSummary(summary, container) {
        container.empty();
        if (summary) {
            container.text(summary).removeClass('red-ui-help-info-none');
        } else {
            container.text(RED._("sidebar.project.noSummaryAvailable")).addClass('red-ui-help-info-none');
        }
    }
    function updateProjectVersion(version, container) {
        container.empty();
        if (version) {
            container.text(version);
        }
    }
    function createMainPane(activeProject) {

        var pane = $('<div id="red-ui-project-settings-tab-main" class="red-ui-project-settings-tab-pane red-ui-help"></div>');
        $('<h1>').text(activeProject.name).appendTo(pane);
        var summary = $('<div style="position: relative">').appendTo(pane);
        var summaryContent = $('<div></div>').appendTo(summary);
        var versionContent = $('<div></div>').addClass('red-ui-help-info-none').appendTo(summary);
        updateProjectSummary(activeProject.summary, summaryContent);
        updateProjectVersion(activeProject.version, versionContent);

        if (RED.user.hasPermission("projects.write")) {
            $('<button class="red-ui-button red-ui-button-small" style="float: right;">' + RED._('sidebar.project.editDescription') + '</button>')
                .prependTo(summary)
                .on("click", function(evt) {
                    evt.preventDefault();
                    editSummary(activeProject, activeProject.summary, summaryContent, activeProject.version, versionContent);
                });
        }
        $('<hr>').appendTo(pane);

        var description = $('<div class="red-ui-help" style="position: relative"></div>').appendTo(pane);
        var descriptionContent = $('<div>',{style:"min-height: 200px"}).appendTo(description);

        updateProjectDescription(activeProject, descriptionContent);

        if (RED.user.hasPermission("projects.write")) {
            $('<button class="red-ui-button red-ui-button-small" style="float: right;">' + RED._('sidebar.project.editReadme') + '</button>')
                .prependTo(description)
                .on("click", function(evt) {
                    evt.preventDefault();
                    editDescription(activeProject, descriptionContent);
                });
        }
        return pane;
    }
    function updateProjectDependencies(activeProject,depsList) {
        depsList.editableList('empty');

        var totalCount = 0;
        var unknownCount = 0;
        var unusedCount = 0;
        var notInstalledCount = 0;

        for (var m in modulesInUse) {
            if (modulesInUse.hasOwnProperty(m) && !activeProject.dependencies.hasOwnProperty(m)) {
                depsList.editableList('addItem',{
                    id: modulesInUse[m].module,
                    version: modulesInUse[m].version,
                    count: modulesInUse[m].count,
                    known: activeProject.dependencies.hasOwnProperty(m),
                    installed: true
                });
                totalCount++;
                if (modulesInUse[m].count === 0) {
                    unusedCount++;
                }
                if (!activeProject.dependencies.hasOwnProperty(m)) {
                    unknownCount++;
                }
            }
        }

        if (activeProject.dependencies) {
            for (var m in activeProject.dependencies) {
                if (activeProject.dependencies.hasOwnProperty(m)) {
                    var installed = !!RED.nodes.registry.getModule(m) && activeProject.dependencies[m] === modulesInUse[m]?.version;
                    depsList.editableList('addItem',{
                        id: m,
                        version: activeProject.dependencies[m], //RED.nodes.registry.getModule(module).version,
                        count: 0,
                        known: true,
                        installed: installed
                    });
                    totalCount++;
                    if (installed) {
                        unusedCount++;
                    } else {
                        notInstalledCount++;
                    }
                }
            }
        }
        // if (notInstalledCount > 0) {
        //     depsList.editableList('addItem',{index:1, label:"Missing dependencies"}); // TODO: nls
        // }
        // if (unknownCount > 0) {
        //     depsList.editableList('addItem',{index:1, label:"Unlisted dependencies"}); // TODO: nls
        // }
        // if (unusedCount > 0) {
        //     depsList.editableList('addItem',{index:3, label:"Unused dependencies"}); // TODO: nls
        // }
        if (totalCount === 0) {
            depsList.editableList('addItem',{index:0, label:RED._("sidebar.project.projectSettings.none")});
        }

    }

    function saveDependencies(depsList,container,dependencies,complete) {
        var activeProject = RED.projects.getActiveProject();
        var spinner = utils.addSpinnerOverlay(container).addClass('red-ui-component-spinner-contain');
        var done = function(err,res) {
            spinner.remove();
            if (err) {
                return complete(err);
            }
            activeProject.dependencies = dependencies;
            RED.sidebar.versionControl.refresh(true);
            complete();
        }
        utils.sendRequest({
            url: "projects/"+activeProject.name,
            type: "PUT",
            responses: {
                0: function(error) {
                    done(error,null);
                },
                200: function(data) {
                    RED.sidebar.versionControl.refresh(true);
                    done(null,data);
                },
                400: {
                    '*': function(error) {
                        done(error,null);
                    }
                },
            }
        },{dependencies:dependencies});
    }
    function editDependencies(activeProject,depsJSON,container,depsList) {
        var json = depsJSON||JSON.stringify(activeProject.dependencies||{},"",4);
        if (json === "{}") {
            json = "{\n\n}";
        }
        RED.editor.editJSON({
            title: RED._('sidebar.project.editDependencies'),
            value: json,
            requireValid: true,
            complete: function(v) {
                try {
                    var parsed = JSON.parse(v);
                    saveDependencies(depsList,container,parsed,function(err) {
                        if (err) {
                            return editDependencies(activeProject,v,container,depsList);
                        }
                        activeProject.dependencies = parsed;
                        updateProjectDependencies(activeProject,depsList);
                    });
                } catch(err) {
                    editDependencies(activeProject,v,container,depsList);
                }
            }
        });
    }

    function createDependenciesPane(activeProject) {
        var pane = $('<div id="red-ui-project-settings-tab-deps" class="red-ui-project-settings-tab-pane red-ui-help"></div>');
        if (RED.user.hasPermission("projects.write")) {
            $('<button class="red-ui-button red-ui-button-small" style="margin-top:10px;float: right;">' + RED._("sidebar.project.projectSettings.edit") + '</button>')
                .appendTo(pane)
                .on("click", function(evt) {
                    evt.preventDefault();
                    editDependencies(activeProject,null,pane,depsList)
                });
        }
        var depsList = $("<ol>",{style:"position: absolute;top: 60px;bottom: 20px;left: 20px;right: 20px;"}).appendTo(pane);
        depsList.editableList({
            addButton: false,
            addItem: function(row,index,entry) {
                // console.log(entry);
                var headerRow = $('<div>',{class:"red-ui-palette-module-header"}).appendTo(row);
                if (entry.label) {
                    if (entry.index === 0) {
                        headerRow.addClass("red-ui-search-empty")
                    } else {
                        row.parent().addClass("red-ui-palette-module-section");
                    }
                    headerRow.text(entry.label);
                    // if (RED.user.hasPermission("projects.write")) {
                    //     if (entry.index === 1) {
                    //         var addButton = $('<button class="red-ui-button red-ui-button-small red-ui-palette-module-button">add to project</button>').appendTo(headerRow).on("click", function(evt) {
                    //             evt.preventDefault();
                    //             var deps = $.extend(true, {}, activeProject.dependencies);
                    //             for (var m in modulesInUse) {
                    //                 if (modulesInUse.hasOwnProperty(m) && !modulesInUse[m].known) {
                    //                     deps[m] = modulesInUse[m].version;
                    //                 }
                    //             }
                    //             editDependencies(activeProject,JSON.stringify(deps,"",4),pane,depsList);
                    //         });
                    //     } else if (entry.index === 3) {
                    //         var removeButton = $('<button class="red-ui-button red-ui-button-small red-ui-palette-module-button">remove from project</button>').appendTo(headerRow).on("click", function(evt) {
                    //             evt.preventDefault();
                    //             var deps = $.extend(true, {}, activeProject.dependencies);
                    //             for (var m in activeProject.dependencies) {
                    //                 if (activeProject.dependencies.hasOwnProperty(m) && !modulesInUse.hasOwnProperty(m)) {
                    //                     delete deps[m];
                    //                 }
                    //             }
                    //             editDependencies(activeProject,JSON.stringify(deps,"",4),pane,depsList);
                    //         });
                    //     }
                    // }
                } else {
                    headerRow.addClass("red-ui-palette-module-header");
                    if (!entry.installed) {
                        headerRow.addClass("red-ui-palette-module-not-installed");
                    } else if (entry.count === 0) {
                        headerRow.addClass("red-ui-palette-module-unused");
                    } else if (!entry.known) {
                        headerRow.addClass("red-ui-palette-module-unknown");
                    }

                    entry.element = headerRow;
                    var titleRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-name"></div>').appendTo(headerRow);
                    var iconClass = "fa-cube";
                    if (!entry.installed) {
                        iconClass = "fa-warning";
                    }
                    var icon = $('<i class="fa '+iconClass+'"></i>').appendTo(titleRow);
                    entry.icon = icon;
                    $('<span>').text(entry.id).appendTo(titleRow);
                    var metaRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-version"><i class="fa fa-tag"></i></div>').appendTo(headerRow);
                    var versionSpan = $('<span>').text(entry.version).appendTo(metaRow);
                    metaRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow);
                    var buttons = $('<div class="red-ui-palette-module-button-group"></div>').appendTo(metaRow);
                    if (RED.user.hasPermission("projects.write")) {
                        if (!entry.installed && RED.settings.get('externalModules.palette.allowInstall', true) !== false) {
                            $('<a href="#" class="red-ui-button red-ui-button-small">' + RED._("sidebar.project.projectSettings.install") + '</a>').appendTo(buttons)
                                .on("click", function(evt) {
                                    evt.preventDefault();
                                    RED.palette.editor.install(entry,row,function(err) {
                                        if (!err) {
                                            entry.installed = true;
                                            var spinner = RED.utils.addSpinnerOverlay(row,true);
                                            setTimeout(function() {
                                                depsList.editableList('removeItem',entry);
                                                refreshModuleInUseCounts();
                                                if (modulesInUse.hasOwnProperty(entry.id)) {
                                                    entry.count = modulesInUse[entry.id].count;
                                                } else {
                                                    entry.count = 0;
                                                }
                                                depsList.editableList('addItem',entry);
                                            },500);
                                        }
                                    });
                                })
                        } else if (entry.known && entry.count === 0) {
                            $('<a href="#" class="red-ui-button red-ui-button-small">' + RED._("sidebar.project.projectSettings.removeFromProject") + '</a>').appendTo(buttons)
                                .on("click", function(evt) {
                                    evt.preventDefault();
                                    var deps = $.extend(true, {}, activeProject.dependencies);
                                    delete deps[entry.id];
                                    saveDependencies(depsList,row,deps,function(err) {
                                        if (!err) {
                                            row.fadeOut(200,function() {
                                                depsList.editableList('removeItem',entry);
                                            });
                                        } else {
                                            console.log(err);
                                        }
                                    });
                                });
                        } else if (!entry.known) {
                            $('<a href="#" class="red-ui-button red-ui-button-small">' + RED._("sidebar.project.projectSettings.addToProject") + '</a>').appendTo(buttons)
                                .on("click", function(evt) {
                                    evt.preventDefault();
                                    var deps = $.extend(true, {}, activeProject.dependencies);
                                    deps[entry.id] = modulesInUse[entry.id].version;
                                    saveDependencies(depsList,row,deps,function(err) {
                                        if (!err) {
                                            buttons.remove();
                                            headerRow.removeClass("red-ui-palette-module-unknown");
                                        } else {
                                            console.log(err);
                                        }
                                    });
                                });
                        }
                    }
                }
            },
            sort: function(A,B) {
                return A.id.localeCompare(B.id);
                // if (A.index && B.index) {
                //     return A.index - B.index;
                // }
                // var Acategory = A.index?A.index:(A.known?(A.count>0?0:4):2);
                // var Bcategory = B.index?B.index:(B.known?(B.count>0?0:4):2);
                // if (Acategory === Bcategory) {
                //     return A.id.localeCompare(B.id);
                // } else {
                //     return Acategory - Bcategory;
                // }
            }
        });

        updateProjectDependencies(activeProject,depsList);
        return pane;

    }

    function showProjectFileListing(row,activeProject,current,selectFilter,done) {
        var dialog;
        var dialogBody;
        var filesList;
        var selected;
        var container = $('<div class="red-ui-projects-file-listing-container"></div>',{style:"position: relative; min-height: 175px; height: 175px;"}).hide().appendTo(row);
        var spinner = utils.addSpinnerOverlay(container);
        $.getJSON("projects/"+activeProject.name+"/files",function(result) {
            var fileNames = Object.keys(result);
            fileNames = fileNames.filter(function(fn) {
                return !result[fn].status || !/D/.test(result[fn].status);
            })
            var files = {};
            fileNames.sort();
            fileNames.forEach(function(file) {
                file.split("/").reduce(function(r,v,i,arr) { if (v) { if (i<arr.length-1) { r[v] = r[v]||{};} else { r[v] = true }return r[v];}},files);
            });
            var sortFiles = function(key,value,fullPath) {
                var result = {
                    name: key||"/",
                    path: fullPath+(fullPath?"/":"")+key,
                };
                if (value === true) {
                    result.type = 'f';
                    return result;
                }
                result.type = 'd';
                result.children = [];
                result.path = result.path;
                var files = Object.keys(value);
                files.forEach(function(file) {
                    result.children.push(sortFiles(file,value[file],result.path));
                })
                result.children.sort(function(A,B) {
                    if (A.hasOwnProperty("children") && !B.hasOwnProperty("children")) {
                        return -1;
                    } else if (!A.hasOwnProperty("children") && B.hasOwnProperty("children")) {
                        return 1;
                    }
                    return A.name.localeCompare(B.name);
                })
                return result;
            }
            var files = sortFiles("",files,"")

            createFileSubList(container,files.children,current,selectFilter,done,"height: 175px");
            spinner.remove();
        });
        return container;
    }

    function createFileSubList(container, files, current, selectFilter, onselect, style) {
        style = style || "";
        var list = $('<ol>',{class:"red-ui-projects-dialog-file-list", style:style}).appendTo(container).editableList({
            addButton: false,
            scrollOnAdd: false,
            addItem: function(row,index,entry) {
                var header = $('<div></div>',{class:"red-ui-projects-dialog-file-list-entry"}).appendTo(row);
                if (entry.children) {
                    $('<span class="red-ui-projects-dialog-file-list-entry-folder"><i class="fa fa-angle-right"></i> <i class="fa fa-folder-o"></i></span>').appendTo(header);
                    if (entry.children.length > 0) {
                        var children = $('<div></div>',{style:"padding-left: 20px;"}).appendTo(row);
                        if (current.indexOf(entry.path+"/") === 0) {
                            header.addClass("expanded");
                        } else {
                            children.hide();
                        }
                        createFileSubList(children,entry.children,current,selectFilter,onselect);
                        header.addClass("selectable");
                        header.on("click", function(e) {
                            if ($(this).hasClass("expanded")) {
                                $(this).removeClass("expanded");
                                children.slideUp(200);
                            } else {
                                $(this).addClass("expanded");
                                children.slideDown(200);
                            }

                        });

                    }
                } else {
                    var fileIcon = "fa-file-o";
                    var fileClass = "";
                    if (/\.json$/i.test(entry.name)) {
                        fileIcon = "fa-file-code-o"
                    } else if (/\.md$/i.test(entry.name)) {
                        fileIcon = "fa-book";
                    } else if (/^\.git/i.test(entry.name)) {
                        fileIcon = "fa-code-fork";
                        header.addClass("red-ui-projects-dialog-file-list-entry-file-type-git");
                    }
                    $('<span class="red-ui-projects-dialog-file-list-entry-file"> <i class="fa '+fileIcon+'"></i></span>').appendTo(header);
                    if (selectFilter(entry)) {
                        header.addClass("selectable");
                        if (entry.path === current) {
                            header.addClass("selected");
                        }
                        header.on("click", function(e) {
                            $(".red-ui-projects-dialog-file-list-entry.selected").removeClass("selected");
                            $(this).addClass("selected");
                            onselect(entry.path,true);
                        })
                        header.on("dblclick", function(e) {
                            e.preventDefault();
                            onselect(entry.path,true);
                        })
                    } else {
                        header.addClass("unselectable");
                    }
                }
                $('<span class="red-ui-projects-dialog-file-list-entry-name" style=""></span>').text(entry.name).appendTo(header);
            }
        });
        if (!style) {
            list.parent().css("overflow-y","");
        }
        files.forEach(function(f) {
            list.editableList('addItem',f);
        })
    }

    // function editFiles(activeProject, container,flowFile, flowFileLabel) {
    //     var editButton = container.children().first();
    //     editButton.hide();
    //
    //     var flowFileInput = $('<input id="" type="text" style="width: calc(100% - 300px);">').val(flowFile).insertAfter(flowFileLabel);
    //
    //     var flowFileInputSearch = $('<button class="red-ui-button" style="margin-left: 10px"><i class="fa fa-folder-open-o"></i></button>')
    //         .insertAfter(flowFileInput)
    //         .on("click", function(e) {
    //             showProjectFileListing(activeProject,'Select flow file',flowFileInput.val(),function(result) {
    //                 flowFileInput.val(result);
    //                 checkFiles();
    //             })
    //         })
    //
    //     var checkFiles = function() {
    //         saveButton.toggleClass('disabled',flowFileInput.val()==="");
    //         saveButton.prop('disabled',flowFileInput.val()==="");
    //     }
    //     flowFileInput.on("change keyup paste",checkFiles);
    //     flowFileLabel.hide();
    //
    //     var bg = $('<span class="button-group" style="position: relative; float: right; margin-right:0;"></span>').prependTo(container);
    //     $('<button class="red-ui-button">Cancel</button>')
    //         .appendTo(bg)
    //         .on("click", function(evt) {
    //             evt.preventDefault();
    //
    //             flowFileLabel.show();
    //             flowFileInput.remove();
    //             flowFileInputSearch.remove();
    //             bg.remove();
    //             editButton.show();
    //         });
    //     var saveButton = $('<button class="red-ui-button">Save</button>')
    //         .appendTo(bg)
    //         .on("click", function(evt) {
    //             evt.preventDefault();
    //             var newFlowFile = flowFileInput.val();
    //             var newCredsFile = credentialsFileInput.val();
    //             var spinner = utils.addSpinnerOverlay(container);
    //             var done = function(err,res) {
    //                 if (err) {
    //                     spinner.remove();
    //                     return;
    //                 }
    //                 activeProject.summary = v;
    //                 spinner.remove();
    //                 flowFileLabel.text(newFlowFile);
    //                 flowFileLabel.show();
    //                 flowFileInput.remove();
    //                 flowFileInputSearch.remove();
    //                 bg.remove();
    //                 editButton.show();
    //             }
    //             // utils.sendRequest({
    //             //     url: "projects/"+activeProject.name,
    //             //     type: "PUT",
    //             //     responses: {
    //             //         0: function(error) {
    //             //             done(error,null);
    //             //         },
    //             //         200: function(data) {
    //             //             done(null,data);
    //             //         },
    //             //         400: {
    //             //             'unexpected_error': function(error) {
    //             //                 done(error,null);
    //             //             }
    //             //         },
    //             //     }
    //             // },{summary:v});
    //         });
    //
    //
    //     checkFiles();
    //
    // }

    function createFilesSection(activeProject,pane) {
        var title = $('<h3></h3>').text(RED._("sidebar.project.projectSettings.files")).appendTo(pane);
        var filesContainer = $('<div class="red-ui-settings-section"></div>').appendTo(pane);
        if (RED.user.hasPermission("projects.write")) {
            var editFilesButton = $('<button type="button" id="red-ui-project-settings-tab-settings-file-edit" class="red-ui-button red-ui-button-small" style="float: right;">' + RED._('sidebar.project.projectSettings.edit') + '</button>')
                .appendTo(title)
                .on("click", function(evt) {
                    evt.preventDefault();
                    formButtons.show();
                    editFilesButton.hide();
                    // packageFileLabelText.hide();

                    if (!activeProject.files.package) {
                        packageFileSubLabel.find(".red-ui-projects-edit-form-sublabel-text").text(RED._("sidebar.project.projectSettings.packageCreate"));
                        packageFileSubLabel.show();
                    }

                    packageFileInputSearch.show();
                    // packageFileInputCreate.show();
                    flowFileLabelText.hide();
                    flowFileInput.show();
                    flowFileInputSearch.show();

                    flowFileInputResize();

                    // credentialStateLabel.parent().hide();
                    credentialStateLabel.addClass("uneditable-input");
                    $(".red-ui-settings-row-credentials").show();
                    credentialStateLabel.css('height','auto');
                    credentialFormRows.hide();
                    credentialSecretButtons.show();
                });
        }
        var row;

        // Flow files
        row = $('<div class="red-ui-settings-row"></div>').appendTo(filesContainer);
        $('<label for=""></label>').text(RED._("sidebar.project.projectSettings.package")).appendTo(row);
        var packageFileLabel = $('<div class="uneditable-input" style="padding:0">').appendTo(row);
        var packageFileLabelText = $('<span style="display:inline-block;  padding: 6px">').text(activeProject.files.package||"package.json").appendTo(packageFileLabel);
        var packageFileInput = $('<input type="hidden">').val(activeProject.files.package||"package.json").appendTo(packageFileLabel);

        var packageFileInputSearch = $('<button type="button" class="red-ui-button toggle single" style="border-top-right-radius: 4px; border-bottom-right-radius: 4px; width: 36px; height: 34px; position: absolute; top: -1px; right: -1px;"><i class="fa fa-folder-open-o"></i></button>')
            .hide()
            .appendTo(packageFileLabel)
            .on("click", function(e) {
                if ($(this).hasClass('selected')) {
                    $(this).removeClass('selected');
                    packageFileLabel.find('.red-ui-projects-file-listing-container').slideUp(200,function() {
                         $(this).remove();
                         packageFileLabel.css('height','');
                     });
                    packageFileLabel.css('color','');
                } else {
                    $(this).addClass('selected');
                    packageFileLabel.css('color','inherit');
                    var fileList = showProjectFileListing(packageFileLabel,activeProject,packageFileInput.val(),
                                        function(entry) { return entry.children || /package\.json$/.test(entry.path); },
                                        function(result,close) {
                                            if (result) {
                                                packageFileInput.val(result);
                                                packageFileLabelText.text(result);
                                                var rootDir = result.substring(0,result.length - 12);
                                                flowFileLabelPrefixText.text(rootDir);
                                                credFileLabelPrefixText.text(rootDir);
                                                flowFileInputResize();
                                                packageFileSubLabel.hide();
                                            }
                                            if (close) {
                                                $(packageFileInputSearch).trigger("click");
                                            }
                                            checkFiles();
                                        });
                    packageFileLabel.css('height','auto');
                    setTimeout(function() {
                        fileList.slideDown(200);
                    },50);

                }
            })
        RED.popover.tooltip(packageFileInputSearch,RED._("sidebar.project.projectSettings.selectFile"));
        var packageFileSubLabel = $('<label style="margin-left: 110px" class="red-ui-projects-edit-form-sublabel"><small><span class="form-warning"><i class="fa fa-warning"></i> <span class="red-ui-projects-edit-form-sublabel-text"></span></small></label>').appendTo(row).hide();
        if (!activeProject.files.package) {
            packageFileSubLabel.find(".red-ui-projects-edit-form-sublabel-text").text(RED._("sidebar.project.projectSettings.fileNotExist"));
            packageFileSubLabel.show();
        }


        var projectPackage = activeProject.files.package||"package.json";
        var projectRoot = projectPackage.substring(0,projectPackage.length - 12);

        // Flow files
        row = $('<div class="red-ui-settings-row"></div>').appendTo(filesContainer);
        $('<label for=""></label>').text(RED._("sidebar.project.projectSettings.flow")).appendTo(row);
        var flowFileLabel = $('<div class="uneditable-input" style="padding:0">').appendTo(row);
        var flowFileLabelPrefixText = $('<span style="display:inline-block; padding: 6px 0 6px 6px">').text(projectRoot).appendTo(flowFileLabel);
        var flowFileName = "flows.json";
        if (activeProject.files.flow) {
            if (activeProject.files.flow.indexOf(projectRoot) === 0) {
                flowFileName = activeProject.files.flow.substring(projectRoot.length);
            } else {
                flowFileName = activeProject.files.flow;
            }
        }
        var flowFileLabelText = $('<span style="display:inline-block; padding: 6px 6px 6px 0">').text(flowFileName).appendTo(flowFileLabel);
        var flowFileInputResize = function() {
            flowFileInput.css({
                "width": "calc(100% - "+(flowFileInputSearch.width() + flowFileLabelPrefixText.width())+"px)"
            });
        }
        var flowFileInput = $('<input type="text" style="padding-left:1px; margin-top: -2px; margin-bottom: 0;border: none;">').val(flowFileName).hide().appendTo(flowFileLabel);
        var flowFileInputSearch = $('<button type="button" class="red-ui-button toggle single" style="border-top-right-radius: 4px; border-bottom-right-radius: 4px; width: 36px; height: 34px; position: absolute; top: -1px; right: -1px;"><i class="fa fa-folder-open-o"></i></button>')
            .hide()
            .appendTo(flowFileLabel)
            .on("click", function(e) {
                if ($(this).hasClass('selected')) {
                    $(this).removeClass('selected');
                    flowFileLabel.find('.red-ui-projects-file-listing-container').slideUp(200,function() {
                         $(this).remove();
                         flowFileLabel.css('height','');
                     });
                    flowFileLabel.css('color','');
                } else {
                    $(this).addClass('selected');
                    flowFileLabel.css('color','inherit');
                    var packageFile = packageFileInput.val();
                    var packagePrefix = packageFile.substring(0,packageFile.length - 12);
                    var re = new RegExp("^"+packagePrefix+".*\.json$");
                    var fileList = showProjectFileListing(flowFileLabel,
                                                          activeProject,
                                                          flowFileInput.val(),
                                                          function(entry) { return !/package.json$/.test(entry.path) && re.test(entry.path) && !/_cred\.json$/.test(entry.path) },
                                                          function(result,isDblClick) {
                                                              if (result) {
                                                                  flowFileInput.val(result.substring(packagePrefix.length));

                                                              }
                                                              if (isDblClick) {
                                                                  $(flowFileInputSearch).trigger("click");
                                                              }
                                                              checkFiles();
                                                          }
                                                      );
                    flowFileLabel.css('height','auto');
                    setTimeout(function() {
                        fileList.slideDown(200);
                    },50);

                }
            })
        RED.popover.tooltip(flowFileInputSearch,RED._("sidebar.project.projectSettings.selectFile"));

        row = $('<div class="red-ui-settings-row"></div>').appendTo(filesContainer);
        $('<label for=""></label>').text(RED._("sidebar.project.projectSettings.credentials")).appendTo(row);

        var credFileName = "flows_cred.json";
        if (activeProject.files.credentials) {
            if (activeProject.files.flow.indexOf(projectRoot) === 0) {
                credFileName = activeProject.files.credentials.substring(projectRoot.length);
            } else {
                credFileName = activeProject.files.credentials;
            }
        }

        var credFileLabel = $('<div class="uneditable-input" style="padding:0">').appendTo(row);
        var credFileLabelPrefixText = $('<span style="display:inline-block;padding: 6px 0 6px 6px">').text(projectRoot).appendTo(credFileLabel);
        var credFileLabelText = $('<span style="display:inline-block; padding: 6px 6px 6px 0">').text(credFileName).appendTo(credFileLabel);

        var credFileInput = $('<input type="hidden">').val(credFileName).insertAfter(credFileLabel);

        var checkFiles = function() {
            var saveDisabled;
            var currentFlowValue = flowFileInput.val();
            var m = /^(.+?)(\.[^.]*)?$/.exec(currentFlowValue);
            if (m) {
                credFileLabelText.text(m[1]+"_cred"+(m[2]||".json"));
            } else if (currentFlowValue === "") {
                credFileLabelText.text("");
            }
            credFileInput.val(credFileLabelText.text());
            var isFlowInvalid = currentFlowValue==="" ||
                                /\.\./.test(currentFlowValue) ||
                                /\/$/.test(currentFlowValue);

            saveDisabled = isFlowInvalid || credFileLabelText.text()==="";

            if (credentialSecretExistingRow.is(":visible")) {
                credentialSecretExistingInput.toggleClass("input-error", credentialSecretExistingInput.val() === "");
                saveDisabled = saveDisabled || credentialSecretExistingInput.val() === "";
            }
            if (credentialSecretNewRow.is(":visible")) {
                credentialSecretNewInput.toggleClass("input-error", credentialSecretNewInput.val() === "");
                saveDisabled = saveDisabled || credentialSecretNewInput.val() === "";
            }


            flowFileInput.toggleClass("input-error", isFlowInvalid);
            // credFileInput.toggleClass("input-error",credFileInput.text()==="");
            saveButton.toggleClass('disabled',saveDisabled);
            saveButton.prop('disabled',saveDisabled);
        }
        flowFileInput.on("change keyup paste",checkFiles);


        // if (!activeProject.files.package) {
        //     $('<span class="form-warning"><i class="fa fa-warning"></i> Missing</span>').appendTo(packageFileLabelText);
        // }
        // if (!activeProject.files.flow) {
        //     $('<span class="form-warning"><i class="fa fa-warning"></i> Missing</span>').appendTo(flowFileLabelText);
        // }
        // if (!activeProject.files.credentials) {
        //     $('<span class="form-warning"><i class="fa fa-warning"></i> Missing</span>').appendTo(credFileLabel);
        // }


        row = $('<div class="red-ui-settings-row"></div>').appendTo(filesContainer);

        $('<label></label>').appendTo(row);
        var credentialStateLabel = $('<span><i class="user-settings-credentials-state-icon fa"></i> <span class="user-settings-credentials-state"></span></span>').appendTo(row);
        var credentialSecretButtons = $('<span class="button-group" style="margin-left: -72px;">').hide().appendTo(row);

        credentialStateLabel.css('color','#666');
        credentialSecretButtons.css('vertical-align','top');
        var credentialSecretResetButton = $('<button type="button" class="red-ui-button" style="vertical-align: top; width: 36px; margin-bottom: 10px"><i class="fa fa-trash-o"></i></button>')
            .appendTo(credentialSecretButtons)
            .on("click", function(e) {
                e.preventDefault();
                if (!$(this).hasClass('selected')) {
                    credentialSecretNewInput.val("");
                    credentialSecretExistingRow.hide();
                    credentialSecretNewRow.show();
                    $(this).addClass("selected");
                    credentialSecretEditButton.removeClass("selected");
                    credentialResetLabel.show();
                    credentialResetWarning.show();
                    credentialSetLabel.hide();
                    credentialChangeLabel.hide();

                    credentialFormRows.show();
                } else {
                    $(this).removeClass("selected");
                    credentialFormRows.hide();
                }
                checkFiles();
            });
        RED.popover.tooltip(credentialSecretResetButton,RED._("sidebar.project.projectSettings.resetTheEncryptionKey"));

        var credentialSecretEditButton = $('<button type="button" class="red-ui-button" style="border-top-right-radius: 4px; border-bottom-right-radius: 4px; vertical-align: top; width: 36px; margin-bottom: 10px"><i class="fa fa-pencil"></i></button>')
            .appendTo(credentialSecretButtons)
            .on("click", function(e) {
                e.preventDefault();
                if (!$(this).hasClass('selected')) {
                    credentialSecretExistingInput.val("");
                    credentialSecretNewInput.val("");
                    if (activeProject.settings.credentialSecretInvalid || !activeProject.settings.credentialsEncrypted) {
                        credentialSetLabel.show();
                        credentialChangeLabel.hide();
                        credentialSecretExistingRow.hide();
                    } else {
                        credentialSecretExistingRow.show();
                        credentialSetLabel.hide();
                        credentialChangeLabel.show();
                    }
                    credentialSecretNewRow.show();
                    credentialSecretEditButton.addClass("selected");
                    credentialSecretResetButton.removeClass("selected");

                    credentialResetLabel.hide();
                    credentialResetWarning.hide();
                    credentialFormRows.show();
                } else {
                    $(this).removeClass("selected");
                    credentialFormRows.hide();
                }
                checkFiles();
            })

        RED.popover.tooltip(credentialSecretEditButton,RED._("sidebar.project.projectSettings.changeTheEncryptionKey"));

        row = $('<div class="red-ui-settings-row red-ui-settings-row-credentials"></div>').hide().appendTo(filesContainer);



        var credentialFormRows = $('<div>',{style:"margin-top:10px"}).hide().appendTo(credentialStateLabel);

        var credentialSetLabel = $('<div style="margin: 20px 0 10px 5px;">' + RED._("sidebar.project.projectSettings.setTheEncryptionKey") + '</div>').hide().appendTo(credentialFormRows);
        var credentialChangeLabel = $('<div style="margin: 20px 0 10px 5px;">' + RED._("sidebar.project.projectSettings.changeTheEncryptionKey") + '</div>').hide().appendTo(credentialFormRows);
        var credentialResetLabel = $('<div style="margin: 20px 0 10px 5px;">' + RED._("sidebar.project.projectSettings.resetTheEncryptionKey") + '</div>').hide().appendTo(credentialFormRows);

        var credentialSecretExistingRow = $('<div class="red-ui-settings-row red-ui-settings-row-credentials"></div>').appendTo(credentialFormRows);
        $('<label for=""></label>').text(RED._("sidebar.project.projectSettings.currentKey")).appendTo(credentialSecretExistingRow);
        var credentialSecretExistingInput = $('<input type="text">').appendTo(credentialSecretExistingRow).typedInput({type:"cred"})
            .on("change keyup paste",function() {
                if (popover) {
                    popover.close();
                    popover = null;
                }
                checkFiles();
            });

        var credentialSecretNewRow = $('<div class="red-ui-settings-row red-ui-settings-row-credentials"></div>').appendTo(credentialFormRows);


        $('<label for=""></label>').text(RED._("sidebar.project.projectSettings.newKey")).appendTo(credentialSecretNewRow);
        var credentialSecretNewInput = $('<input type="text">').appendTo(credentialSecretNewRow).typedInput({type:"cred"}).on("change keyup paste",checkFiles);

        var credentialResetWarning = $('<div class="form-tips form-warning" style="margin: 10px;"><i class="fa fa-warning"></i>' + RED._("sidebar.project.projectSettings.credentialsAlert") + '</div>').hide().appendTo(credentialFormRows);


        var hideEditForm = function() {
            editFilesButton.show();
            formButtons.hide();
            // packageFileLabelText.show();
            packageFileInputSearch.hide();
            // packageFileInputCreate.hide();
            flowFileLabelText.show();
            flowFileInput.hide();
            flowFileInputSearch.hide();

            // credentialStateLabel.parent().show();
            credentialStateLabel.removeClass("uneditable-input");
            credentialStateLabel.css('height','');

            flowFileInputSearch.removeClass('selected');
            flowFileLabel.find('.red-ui-projects-file-listing-container').remove();
            flowFileLabel.css('height','');
            flowFileLabel.css('color','');

            $(".red-ui-settings-row-credentials").hide();
            credentialFormRows.hide();
            credentialSecretButtons.hide();
            credentialSecretResetButton.removeClass("selected");
            credentialSecretEditButton.removeClass("selected");


        }

        var formButtons = $('<span class="button-row" style="position: relative; float: right; margin-right:0;"></span>').hide().appendTo(filesContainer);
        $('<button type="button" class="red-ui-button">' + RED._("common.label.cancel") + '</button>')
            .appendTo(formButtons)
            .on("click", function(evt) {
                evt.preventDefault();
                var projectPackage = activeProject.files.package||"package.json";
                var projectRoot = projectPackage.substring(0,projectPackage.length - 12);
                flowFileLabelPrefixText.text(projectRoot);
                credFileLabelPrefixText.text(projectRoot);
                packageFileLabelText.text(activeProject.files.package||"package.json");
                if (!activeProject.files.package) {
                    packageFileSubLabel.find(".red-ui-projects-edit-form-sublabel-text").text(RED._("sidebar.project.projectSettings.fileNotExist"));
                    packageFileSubLabel.show();
                } else {
                    packageFileSubLabel.hide();
                }
                flowFileInput.val(flowFileLabelText.text());
                credFileLabelText.text(credFileName);
                hideEditForm();
            });
        var saveButton = $('<button type="button" class="red-ui-button">' + RED._("common.label.save") + '</button>')
            .appendTo(formButtons)
            .on("click", function(evt) {
                evt.preventDefault();
                var spinner = utils.addSpinnerOverlay(filesContainer);
                var done = function(err) {
                    spinner.remove();
                    if (err) {
                        utils.reportUnexpectedError(err);
                        return;
                    }
                    flowFileLabelText.text(flowFileInput.val());
                    credFileLabelText.text(credFileInput.val());
                    packageFileSubLabel.hide();
                    hideEditForm();
                }
                var rootPath = packageFileInput.val();
                rootPath = rootPath.substring(0,rootPath.length-12);
                var payload = {
                    files: {
                        package: packageFileInput.val(),
                        flow: rootPath+flowFileInput.val(),
                        credentials: rootPath+credFileInput.val()
                    }
                }

                if (credentialSecretResetButton.hasClass('selected')) {
                    payload.resetCredentialSecret = true;
                }
                if (credentialSecretResetButton.hasClass('selected') || credentialSecretEditButton.hasClass('selected')) {
                    payload.credentialSecret = credentialSecretNewInput.val();
                    if (credentialSecretExistingRow.is(":visible")) {
                        payload.currentCredentialSecret = credentialSecretExistingInput.val();
                    }
                }
                RED.deploy.setDeployInflight(true);
                utils.sendRequest({
                    url: "projects/"+activeProject.name,
                    type: "PUT",
                    responses: {
                        0: function(error) {
                            done(error);
                        },
                        200: function(data) {
                            activeProject = data;
                            RED.sidebar.versionControl.refresh(true);
                            updateForm();
                            done();
                        },
                        400: {
                            'credentials_load_failed': function(error) {
                                done(error);
                            },
                            'missing_current_credential_key':  function(error) {
                                credentialSecretExistingInput.addClass("input-error");
                                popover = RED.popover.create({
                                    target: credentialSecretExistingInput,
                                    direction: 'right',
                                    size: 'small',
                                    content: "Incorrect key",
                                    autoClose: 3000
                                }).open();
                                done(error);
                            },
                            '*': function(error) {
                                done(error);
                            }
                        },
                    }
                },payload).always(function() {
                    setTimeout(function() {
                        RED.deploy.setDeployInflight(false);
                    },500);
                });
            });
        var updateForm = function() {
            if (activeProject.settings.credentialSecretInvalid) {
                credentialStateLabel.find(".user-settings-credentials-state-icon").removeClass().addClass("user-settings-credentials-state-icon fa fa-warning");
                credentialStateLabel.find(".user-settings-credentials-state").text(RED._("sidebar.project.projectSettings.invalidEncryptionKey"));
            } else if (activeProject.settings.credentialsEncrypted) {
                credentialStateLabel.find(".user-settings-credentials-state-icon").removeClass().addClass("user-settings-credentials-state-icon fa fa-lock");
                credentialStateLabel.find(".user-settings-credentials-state").text(RED._("sidebar.project.projectSettings.encryptionEnabled"));
            } else {
                credentialStateLabel.find(".user-settings-credentials-state-icon").removeClass().addClass("user-settings-credentials-state-icon fa fa-unlock");
                credentialStateLabel.find(".user-settings-credentials-state").text(RED._("sidebar.project.projectSettings.encryptionDisabled"));
            }
            credentialSecretResetButton.toggleClass('disabled',!activeProject.settings.credentialSecretInvalid && !activeProject.settings.credentialsEncrypted);
            credentialSecretResetButton.prop('disabled',!activeProject.settings.credentialSecretInvalid && !activeProject.settings.credentialsEncrypted);
        }

        checkFiles();
        updateForm();
    }

    function createLocalBranchListSection(activeProject,pane) {
        var localBranchContainer = $('<div class="red-ui-settings-section"></div>').appendTo(pane);
        $('<h4></h4>').text(RED._("sidebar.project.projectSettings.branches")).appendTo(localBranchContainer);

        var row = $('<div class="red-ui-settings-row red-ui-projects-dialog-list"></div>').appendTo(localBranchContainer);


        var branchList = $('<ol>').appendTo(row).editableList({
            height: 'auto',
            addButton: false,
            scrollOnAdd: false,
            addItem: function(row,index,entry) {
                var container = $('<div class="red-ui-projects-dialog-list-entry">').appendTo(row);
                if (entry.empty) {
                    container.addClass('red-ui-search-empty');
                    container.text(RED._("sidebar.project.projectSettings.noBranches"));
                    return;
                }
                if (entry.current) {
                    container.addClass("current");
                }
                $('<span class="entry-icon"><i class="fa fa-code-fork"></i></span>').appendTo(container);
                var content = $('<span>').appendTo(container);
                var topRow = $('<div>').appendTo(content);
                $('<span class="entry-name">').text(entry.name).appendTo(topRow);
                if (entry.commit) {
                    $('<span class="entry-detail">').text(entry.commit.sha).appendTo(topRow);
                }
                if (entry.remote) {
                    var bottomRow = $('<div>').appendTo(content);

                    $('<span class="entry-detail entry-remote-name">').text(entry.remote||"").appendTo(bottomRow);
                    if (entry.status.ahead+entry.status.behind > 0) {
                        $('<span class="entry-detail">'+
                            '<i class="fa fa-long-arrow-up"></i> <span>'+entry.status.ahead+'</span> '+
                            '<i class="fa fa-long-arrow-down"></i> <span>'+entry.status.behind+'</span>'+
                            '</span>').appendTo(bottomRow);
                    }
                }

                if (!entry.current) {
                    var tools = $('<span class="entry-tools">').appendTo(container);
                    $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>')
                        .appendTo(tools)
                        .on("click", function(e) {
                            e.preventDefault();
                            var spinner = utils.addSpinnerOverlay(row).addClass('red-ui-component-spinner-contain');
                            var notification = RED.notify(RED._("sidebar.project.projectSettings.deleteConfirm", { name: entry.name }), {
                                type: "warning",
                                modal: true,
                                fixed: true,
                                buttons: [
                                    {
                                        text: RED._("common.label.cancel"),
                                        click: function() {
                                            spinner.remove();
                                            notification.close();
                                        }
                                    },{
                                        text: RED._("sidebar.project.projectSettings.deleteBranch"),
                                        click: function() {
                                            notification.close();
                                            var url = "projects/"+activeProject.name+"/branches/"+entry.name;
                                            var options = {
                                                url: url,
                                                type: "DELETE",
                                                responses: {
                                                    200: function(data) {
                                                        row.fadeOut(200,function() {
                                                            branchList.editableList('removeItem',entry);
                                                            spinner.remove();
                                                        });
                                                    },
                                                    400: {
                                                        'git_delete_branch_unmerged': function(error) {
                                                            notification = RED.notify(RED._("sidebar.project.projectSettings.unmergedConfirm", { name: entry.name }), {
                                                                type: "warning",
                                                                modal: true,
                                                                fixed: true,
                                                                buttons: [
                                                                    {
                                                                        text: RED._("common.label.cancel"),
                                                                        click: function() {
                                                                            spinner.remove();
                                                                            notification.close();
                                                                        }
                                                                    },{
                                                                        text: RED._("sidebar.project.projectSettings.deleteUnmergedBranch"),
                                                                        click: function() {
                                                                            options.url += "?force=true";
                                                                            notification.close();
                                                                            utils.sendRequest(options);
                                                                        }
                                                                    }
                                                                ]
                                                            });
                                                        },
                                                        '*': function(error) {
                                                            utils.reportUnexpectedError(error);
                                                            spinner.remove();
                                                        }
                                                    },
                                                }
                                            }
                                            utils.sendRequest(options);
                                        }
                                    }

                                ]
                            })
                        })
                }

            }
        });

        $.getJSON("projects/"+activeProject.name+"/branches",function(result) {
            if (result.branches) {
                if (result.branches.length > 0) {
                    result.branches.sort(function(A,B) {
                        if (A.current) { return -1 }
                        if (B.current) { return 1 }
                        return A.name.localeCompare(B.name);
                    });
                    result.branches.forEach(function(branch) {
                        branchList.editableList('addItem',branch);
                    })
                } else {
                    branchList.editableList('addItem',{empty:true});
                }
            }
        })
    }

    function createRemoteRepositorySection(activeProject,pane) {
        $('<h3></h3>').text(RED._("sidebar.project.projectSettings.versionControl")).appendTo(pane);

        createLocalBranchListSection(activeProject,pane);

        var repoContainer = $('<div class="red-ui-settings-section"></div>').appendTo(pane);
        var title = $('<h4></h4>').text(RED._("sidebar.project.projectSettings.gitRemotes")).appendTo(repoContainer);

        var editRepoButton = $('<button class="red-ui-button red-ui-button-small" style="float: right; margin-right: 10px;">' + RED._("sidebar.project.projectSettings.addRemote") + '</button>')
            .appendTo(title)
            .on("click", function(evt) {
                editRepoButton.attr('disabled',true);
                addRemoteDialog.slideDown(200, function() {
                    addRemoteDialog[0].scrollIntoView();
                    if (isEmpty) {
                        remoteNameInput.val('origin');
                        remoteURLInput.trigger("focus");
                    } else {
                        remoteNameInput.trigger("focus");
                    }
                    validateForm();
                });
            });


        var emptyItem = { empty: true };
        var isEmpty = true;
        var row = $('<div class="red-ui-settings-row"></div>').appendTo(repoContainer);
        var addRemoteDialog = $('<div class="red-ui-projects-dialog-list-dialog"></div>').hide().appendTo(row);
        row = $('<div class="red-ui-settings-row red-ui-projects-dialog-list"></div>').appendTo(repoContainer);
        var remotesList = $('<ol>').appendTo(row);
        remotesList.editableList({
            addButton: false,
            height: 'auto',
            addItem: function(row,index,entry) {

                var container = $('<div class="red-ui-projects-dialog-list-entry">').appendTo(row);
                if (entry.empty) {
                    container.addClass('red-ui-search-empty');
                    container.text(RED._("sidebar.project.projectSettings.noRemotes"));
                    return;
                } else {
                    $('<span class="entry-icon"><i class="fa fa-globe"></i></span>').appendTo(container);
                    var content = $('<span>').appendTo(container);
                    $('<div class="entry-name">').text(entry.name).appendTo(content);
                    if (entry.urls.fetch === entry.urls.push) {
                        $('<div class="entry-detail">').text(entry.urls.fetch).appendTo(content);
                    } else {
                        $('<div class="entry-detail">').text("fetch: "+entry.urls.fetch).appendTo(content);
                        $('<div class="entry-detail">').text("push: "+entry.urls.push).appendTo(content);

                    }
                    var tools = $('<span class="entry-tools">').appendTo(container);
                    $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>')
                        .appendTo(tools)
                        .on("click", function(e) {
                            e.preventDefault();
                            var spinner = utils.addSpinnerOverlay(row).addClass('red-ui-component-spinner-contain');
                            var notification = RED.notify(RED._("sidebar.project.projectSettings.deleteRemoteConfrim", { name: entry.name }), {
                                type: "warning",
                                modal: true,
                                fixed: true,
                                buttons: [
                                    {
                                        text: RED._("common.label.cancel"),
                                        click: function() {
                                            spinner.remove();
                                            notification.close();
                                        }
                                    },{
                                        text: RED._("sidebar.project.projectSettings.deleteRemote"),
                                        click: function() {
                                            notification.close();

                                            if (activeProject.git.branches.remote && activeProject.git.branches.remote.indexOf(entry.name+"/") === 0) {
                                                delete activeProject.git.branches.remote;
                                            }
                                            if (activeProject.git.branches.remoteAlt && activeProject.git.branches.remoteAlt.indexOf(entry.name+"/") === 0) {
                                                delete activeProject.git.branches.remoteAlt;
                                            }

                                            var url = "projects/"+activeProject.name+"/remotes/"+entry.name;
                                            var options = {
                                                url: url,
                                                type: "DELETE",
                                                responses: {
                                                    200: function(data) {
                                                        row.fadeOut(200,function() {
                                                            remotesList.editableList('removeItem',entry);
                                                            setTimeout(spinner.remove, 100);
                                                            if (data.remotes.length === 0) {
                                                                delete activeProject.git.remotes;
                                                                isEmpty = true;
                                                                remotesList.editableList('addItem',emptyItem);
                                                            } else {
                                                                activeProject.git.remotes = {};
                                                                data.remotes.forEach(function(remote) {
                                                                    var name = remote.name;
                                                                    delete remote.name;
                                                                    activeProject.git.remotes[name] = remote;
                                                                });
                                                            }
                                                            delete activeProject.git.branches.remoteAlt;
                                                            RED.sidebar.versionControl.refresh();
                                                        });
                                                    },
                                                    400: {
                                                        '*': function(error) {
                                                            utils.reportUnexpectedError(error);
                                                            spinner.remove();
                                                        }
                                                    },
                                                }
                                            }
                                            utils.sendRequest(options);
                                        }
                                    }

                                ]
                            })
                        });
                }


            }
        });

        var validateForm = function() {
            var validName = /^[a-zA-Z0-9\-_]+$/.test(remoteNameInput.val());
            var repo = remoteURLInput.val();
            // var validRepo = /^(?:file|git|ssh|https?|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?[\w\.@:\/~_-]+(?:\.git)?(?:\/?|\#[\d\w\.\-_]+?)$/.test(remoteURLInput.val());
            var validRepo = repo.length > 0 && !/\s/.test(repo);
            if (/^https?:\/\/[^/]+@/i.test(repo)) {
                remoteURLLabel.text(RED._("sidebar.project.projectSettings.urlRule2"));
                validRepo = false;
            } else {
                remoteURLLabel.text(RED._("sidebar.project.projectSettings.urlRule"));
            }
            saveButton.attr('disabled',(!validName || !validRepo))
            remoteNameInput.toggleClass('input-error',remoteNameInputChanged&&!validName);
            remoteURLInput.toggleClass('input-error',remoteURLInputChanged&&!validRepo);
            if (popover) {
                popover.close();
                popover = null;
            }
        };
        var popover;
        var remoteNameInputChanged = false;
        var remoteURLInputChanged = false;

        $('<div class="red-ui-projects-dialog-list-dialog-header">').text(RED._('sidebar.project.projectSettings.addRemote2')).appendTo(addRemoteDialog);

        row = $('<div class="red-ui-settings-row"></div>').appendTo(addRemoteDialog);
        $('<label for=""></label>').text(RED._("sidebar.project.projectSettings.remoteName")).appendTo(row);
        var remoteNameInput = $('<input type="text">').appendTo(row).on("change keyup paste",function() {
            remoteNameInputChanged = true;
            validateForm();
        });
        $('<label class="red-ui-projects-edit-form-sublabel"><small>' + RED._("sidebar.project.projectSettings.nameRule") + '</small></label>').appendTo(row).find("small");
        row = $('<div class="red-ui-settings-row"></div>').appendTo(addRemoteDialog);
        $('<label for=""></label>').text(RED._("sidebar.project.projectSettings.url")).appendTo(row);
        var remoteURLInput = $('<input type="text">').appendTo(row).on("change keyup paste",function() {
            remoteURLInputChanged = true;
            validateForm()
        });
        var remoteURLLabel = $('<label class="red-ui-projects-edit-form-sublabel"><small>' + RED._("sidebar.project.projectSettings.urlRule") +'</small></label>').appendTo(row).find("small");

        var hideEditForm = function() {
            editRepoButton.attr('disabled',false);
            addRemoteDialog.hide();
            remoteNameInput.val("");
            remoteURLInput.val("");
            if (popover) {
                popover.close();
                popover = null;
            }
        }
        var formButtons = $('<span class="button-row" style="position: relative; float: right; margin: 10px;"></span>')
            .appendTo(addRemoteDialog);
        $('<button class="red-ui-button">' + RED._("common.label.cancel") + '</button>')
            .appendTo(formButtons)
            .on("click", function(evt) {
                evt.preventDefault();
                hideEditForm();
            });
        var saveButton = $('<button class="red-ui-button">' + RED._("sidebar.project.projectSettings.addRemote2") + '</button>')
            .appendTo(formButtons)
            .on("click", function(evt) {
                evt.preventDefault();
                var spinner = utils.addSpinnerOverlay(addRemoteDialog).addClass('red-ui-component-spinner-contain');

                var payload = {
                    name: remoteNameInput.val(),
                    url: remoteURLInput.val()
                }
                var done = function(err) {
                    spinner.remove();
                    if (err) {
                        return;
                    }
                    hideEditForm();
                }
                // console.log(JSON.stringify(payload,null,4));
                RED.deploy.setDeployInflight(true);
                utils.sendRequest({
                    url: "projects/"+activeProject.name+"/remotes",
                    type: "POST",
                    responses: {
                        0: function(error) {
                            done(error);
                        },
                        200: function(data) {
                            activeProject.git.remotes = {};
                            data.remotes.forEach(function(remote) {
                                var name = remote.name;
                                delete remote.name;
                                activeProject.git.remotes[name] = remote;
                            });
                            updateForm();
                            RED.sidebar.versionControl.refresh();
                            done();
                        },
                        400: {
                            'git_remote_already_exists': function(error) {
                                popover = RED.popover.create({
                                    target: remoteNameInput,
                                    direction: 'right',
                                    size: 'small',
                                    content: "Remote already exists",
                                    autoClose: 6000
                                }).open();
                                remoteNameInput.addClass('input-error');
                                done(error);
                            },
                            '*': function(error) {
                                utils.reportUnexpectedError(error);
                                done(error);
                            }
                        },
                    }
                },payload);
            });

        var updateForm = function() {
            remotesList.editableList('empty');
            var count = 0;
            if (activeProject.git.hasOwnProperty('remotes')) {
                for (var name in activeProject.git.remotes) {
                    if (activeProject.git.remotes.hasOwnProperty(name)) {
                        count++;
                        remotesList.editableList('addItem',{name:name,urls:activeProject.git.remotes[name]});
                    }
                }
            }
            isEmpty = (count === 0);
            if (isEmpty) {
                remotesList.editableList('addItem',emptyItem);
            }
        }
        updateForm();
    }

    function createSettingsPane(activeProject) {
        var pane = $('<div id="red-ui-project-settings-tab-settings" class="red-ui-project-settings-tab-pane red-ui-help"></div>');
        createFilesSection(activeProject,pane);
        // createLocalRepositorySection(activeProject,pane);
        createRemoteRepositorySection(activeProject,pane);
        return pane;
    }

    function refreshModuleInUseCounts() {
        modulesInUse = {};
        RED.nodes.eachNode(_updateModulesInUse);
        RED.nodes.eachConfig(_updateModulesInUse);
    }

    function _updateModulesInUse(n) {
        if (!/^subflow:/.test(n.type)) {
            var module = RED.nodes.registry.getNodeSetForType(n.type).module;
            if (module !== 'node-red') {
                if (!modulesInUse.hasOwnProperty(module)) {
                    modulesInUse[module] = {
                        module: module,
                        version: RED.nodes.registry.getModule(module).version,
                        count: 0,
                        known: false
                    }
                }
                modulesInUse[module].count++;
            }
        }
    }

    var popover;
    var utils;
    var modulesInUse = {};
    function init(_utils) {
        utils = _utils;
        addPane({
            id:'main',
            title: RED._("sidebar.project.name"),
            get: createMainPane,
            close: function() { }
        });
        addPane({
            id:'deps',
            title: RED._("sidebar.project.dependencies"),
            get: createDependenciesPane,
            close: function() { }
        });
        addPane({
            id:'settings',
            title: RED._("sidebar.project.settings"),
            get: createSettingsPane,
            close: function() {
                if (popover) {
                    popover.close();
                    popover = null;
                }
            }
        });

        RED.events.on('nodes:add', _updateModulesInUse);
        RED.events.on('nodes:remove', function(n) {
            if (!/^subflow:/.test(n.type)) {
                var module = RED.nodes.registry.getNodeSetForType(n.type).module;
                if (module !== 'node-red' && modulesInUse.hasOwnProperty(module)) {
                    modulesInUse[module].count--;
                    if (modulesInUse[module].count === 0) {
                        if (!modulesInUse[module].known) {
                            delete modulesInUse[module];
                        }
                    }
                }
            }
        })



    }
    return {
        init: init,
        show: show,
        switchProject: function(name) {
            // TODO: not ideal way to trigger this; should there be an editor-wide event?
            modulesInUse = {};
        }
    };
})();
;/**
 * 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.
 **/

RED.projects.userSettings = (function() {

    var gitUsernameInput;
    var gitEmailInput;

    function createGitUserSection(pane) {

        var currentGitSettings = RED.settings.get('git') || {};
        currentGitSettings.user = currentGitSettings.user || {};

        var title = $('<h3></h3>').text(RED._("editor:sidebar.project.userSettings.committerDetail")).appendTo(pane);

        var gitconfigContainer = $('<div class="red-ui-settings-section"></div>').appendTo(pane);
        $('<div class="red-ui-settings-section-description"></div>').appendTo(gitconfigContainer).text(RED._("editor:sidebar.project.userSettings.committerTip"));

        var row = $('<div class="red-ui-settings-row"></div>').appendTo(gitconfigContainer);
        $('<label for="user-settings-gitconfig-username"></label>').text(RED._("editor:sidebar.project.userSettings.userName")).appendTo(row);
        gitUsernameInput = $('<input type="text" id="user-settings-gitconfig-username">').appendTo(row);
        gitUsernameInput.val(currentGitSettings.user.name||"");

        row = $('<div class="red-ui-settings-row"></div>').appendTo(gitconfigContainer);
        $('<label for="user-settings-gitconfig-email"></label>').text(RED._("editor:sidebar.project.userSettings.email")).appendTo(row);
        gitEmailInput = $('<input type="text" id="user-settings-gitconfig-email">').appendTo(row);
        gitEmailInput.val(currentGitSettings.user.email||"");

    }

    function createWorkflowSection(pane) {

        var defaultWorkflowMode = RED.settings.theme("projects.workflow.mode","manual");

        var currentGitSettings = RED.settings.get('git') || {};
        currentGitSettings.workflow = currentGitSettings.workflow || {};
        currentGitSettings.workflow.mode = currentGitSettings.workflow.mode || defaultWorkflowMode;

        var title = $('<h3></h3>').text(RED._("editor:sidebar.project.userSettings.workflow")).appendTo(pane);

        var workflowContainer = $('<div class="red-ui-settings-section"></div>').appendTo(pane);
        $('<div class="red-ui-settings-section-description"></div>').appendTo(workflowContainer).text(RED._("editor:sidebar.project.userSettings.workfowTip"));

        var row = $('<div class="red-ui-settings-row"></div>').appendTo(workflowContainer);
        $('<label><input type="radio" name="user-setting-gitworkflow" value="manual"> <div style="margin-left: 3px; display: inline-block"><div data-i18n="editor:sidebar.project.userSettings.workflowManual"></div><div style="color:#aaa;" data-i18n="editor:sidebar.project.userSettings.workflowManualTip"></div></div></label>').appendTo(row);
        row = $('<div class="red-ui-settings-row"></div>').appendTo(workflowContainer);
        $('<label><input type="radio" name="user-setting-gitworkflow" value="auto"> <div style="margin-left: 3px; display: inline-block"><div data-i18n="editor:sidebar.project.userSettings.workflowAuto"></div><div style="color:#aaa;" data-i18n="editor:sidebar.project.userSettings.workflowAutoTip"></div></div></label>').appendTo(row);

        workflowContainer.find('[name="user-setting-gitworkflow"][type="radio"][value="'+currentGitSettings.workflow.mode+'"]').prop('checked',true)

    }


    function createSSHKeySection(pane) {
        var title = $('<h3></h3>').text(RED._("editor:sidebar.project.userSettings.sshKeys")).appendTo(pane);
        var container = $('<div class="red-ui-settings-section"></div>').appendTo(pane);
        var popover;
        var subtitle = $('<div class="red-ui-settings-section-description"></div>').appendTo(container).text(RED._("editor:sidebar.project.userSettings.sshKeysTip"));

        var addKeyButton = $('<button id="user-settings-gitconfig-add-key" class="red-ui-button red-ui-button-small" style="float: right; margin-right: 10px;">'+RED._("editor:sidebar.project.userSettings.add")+'</button>')
            .appendTo(subtitle)
            .on("click", function(evt) {
                addKeyButton.attr('disabled',true);
                saveButton.attr('disabled',true);
                // bg.children().removeClass("selected");
                // addLocalButton.trigger("click");
                addKeyDialog.slideDown(200);
                keyNameInput.trigger("focus");
            });

        var validateForm = function() {
            var valid = /^[a-zA-Z0-9\-_]+$/.test(keyNameInput.val());
            keyNameInput.toggleClass('input-error',keyNameInputChanged&&!valid);

            // var selectedButton = bg.find(".selected");
            // if (selectedButton[0] === addLocalButton[0]) {
            //     valid = valid && localPublicKeyPathInput.val().length > 0 && localPrivateKeyPathInput.val().length > 0;
            // } else if (selectedButton[0] === uploadButton[0]) {
            //     valid = valid && publicKeyInput.val().length > 0 && privateKeyInput.val().length > 0;
            // } else if (selectedButton[0] === generateButton[0]) {
                var passphrase = passphraseInput.val();
                var validPassphrase = passphrase.length === 0 || passphrase.length >= 8;
                passphraseInput.toggleClass('input-error',!validPassphrase);
                if (!validPassphrase) {
                    passphraseInputSubLabel.text(RED._("editor:sidebar.project.userSettings.passphraseShort"));
                } else if (passphrase.length === 0) {
                    passphraseInputSubLabel.text(RED._("editor:sidebar.project.userSettings.optional"));
                } else {
                    passphraseInputSubLabel.text("");
                }
                valid = valid && validPassphrase;
            // }

            saveButton.attr('disabled',!valid);

            if (popover) {
                popover.close();
                popover = null;
            }
        };

        var row = $('<div class="red-ui-settings-row"></div>').appendTo(container);
        var addKeyDialog = $('<div class="red-ui-projects-dialog-list-dialog"></div>').hide().appendTo(row);
        $('<div class="red-ui-projects-dialog-list-dialog-header">').text(RED._("editor:sidebar.project.userSettings.addSshKey")).appendTo(addKeyDialog);
        var addKeyDialogBody = $('<div>').appendTo(addKeyDialog);

        row = $('<div class="red-ui-settings-row"></div>').appendTo(addKeyDialogBody);
        $('<div class="red-ui-settings-section-description"></div>').appendTo(row).text(RED._("editor:sidebar.project.userSettings.addSshKeyTip"));
        // var bg = $('<div></div>',{class:"button-group", style:"text-align: center"}).appendTo(row);
        // var addLocalButton = $('<button class="red-ui-button toggle selected">use local key</button>').appendTo(bg);
        // var uploadButton = $('<button class="red-ui-button toggle">upload key</button>').appendTo(bg);
        // var generateButton = $('<button class="red-ui-button toggle">generate key</button>').appendTo(bg);
        // bg.children().on("click", function(e) {
        //     e.preventDefault();
        //     if ($(this).hasClass("selected")) {
        //         return;
        //     }
        //     bg.children().removeClass("selected");
        //     $(this).addClass("selected");
        //     if (this === addLocalButton[0]) {
        //         addLocalKeyPane.show();
        //         generateKeyPane.hide();
        //         uploadKeyPane.hide();
        //     } else if (this === uploadButton[0]) {
        //         addLocalKeyPane.hide();
        //         generateKeyPane.hide();
        //         uploadKeyPane.show();
        //     } else if (this === generateButton[0]){
        //         addLocalKeyPane.hide();
        //         generateKeyPane.show();
        //         uploadKeyPane.hide();
        //     }
        //     validateForm();
        // })


        row = $('<div class="red-ui-settings-row"></div>').appendTo(addKeyDialogBody);
        $('<label for=""></label>').text(RED._("editor:sidebar.project.userSettings.name")).appendTo(row);
        var keyNameInputChanged = false;
        var keyNameInput = $('<input type="text">').appendTo(row).on("change keyup paste",function() {
            keyNameInputChanged = true;
            validateForm();
        });
        $('<label class="red-ui-projects-edit-form-sublabel"><small>'+RED._("editor:sidebar.project.userSettings.nameRule")+'</small></label>').appendTo(row).find("small");

        var generateKeyPane = $('<div>').appendTo(addKeyDialogBody);
        row = $('<div class="red-ui-settings-row"></div>').appendTo(generateKeyPane);
        $('<label for=""></label>').text(RED._("editor:sidebar.project.userSettings.passphrase")).appendTo(row);
        var passphraseInput = $('<input type="password">').appendTo(row).on("change keyup paste",validateForm);
        var passphraseInputSubLabel = $('<label class="red-ui-projects-edit-form-sublabel"><small>'+RED._("editor:sidebar.project.userSettings.optional")+'</small></label>').appendTo(row).find("small");

        // var addLocalKeyPane = $('<div>').hide().appendTo(addKeyDialogBody);
        // row = $('<div class="red-ui-settings-row"></div>').appendTo(addLocalKeyPane);
        // $('<label for=""></label>').text('Public key').appendTo(row);
        // var localPublicKeyPathInput = $('<input type="text">').appendTo(row).on("change keyup paste",validateForm);
        // $('<label class="red-ui-projects-edit-form-sublabel"><small>Public key file path, for example: ~/.ssh/id_rsa.pub</small></label>').appendTo(row).find("small");
        // row = $('<div class="red-ui-settings-row"></div>').appendTo(addLocalKeyPane);
        // $('<label for=""></label>').text('Private key').appendTo(row);
        // var localPrivateKeyPathInput = $('<input type="text">').appendTo(row).on("change keyup paste",validateForm);
        // $('<label class="red-ui-projects-edit-form-sublabel"><small>Private key file path, for example: ~/.ssh/id_rsa</small></label>').appendTo(row).find("small");
        //
        // var uploadKeyPane = $('<div>').hide().appendTo(addKeyDialogBody);
        // row = $('<div class="red-ui-settings-row"></div>').appendTo(uploadKeyPane);
        // $('<label for=""></label>').text('Public key').appendTo(row);
        // var publicKeyInput = $('<textarea>').appendTo(row).on("change keyup paste",validateForm);
        // $('<label class="red-ui-projects-edit-form-sublabel"><small>Paste in public key contents, for example: ~/.ssh/id_rsa.pub</small></label>').appendTo(row).find("small");
        // row = $('<div class="red-ui-settings-row"></div>').appendTo(uploadKeyPane);
        // $('<label for=""></label>').text('Private key').appendTo(row);
        // var privateKeyInput = $('<textarea>').appendTo(row).on("change keyup paste",validateForm);
        // $('<label class="red-ui-projects-edit-form-sublabel"><small>Paste in private key contents, for example: ~/.ssh/id_rsa</small></label>').appendTo(row).find("small");




        var hideEditForm = function() {
            addKeyButton.attr('disabled',false);
            addKeyDialog.hide();

            keyNameInput.val("");
            keyNameInputChanged = false;
            passphraseInput.val("");
            // localPublicKeyPathInput.val("");
            // localPrivateKeyPathInput.val("");
            // publicKeyInput.val("");
            // privateKeyInput.val("");
            if (popover) {
                popover.close();
                popover = null;
            }
        }
        var formButtons = $('<span class="button-row" style="position: relative; float: right; margin: 10px;"></span>').appendTo(addKeyDialog);
        $('<button class="red-ui-button">'+RED._("editor:sidebar.project.userSettings.cancel")+'</button>')
            .appendTo(formButtons)
            .on("click", function(evt) {
                evt.preventDefault();
                hideEditForm();
            });
        var saveButton = $('<button class="red-ui-button">'+RED._("editor:sidebar.project.userSettings.generate")+'</button>')
            .appendTo(formButtons)
            .on("click", function(evt) {
                evt.preventDefault();
                var spinner = utils.addSpinnerOverlay(addKeyDialog).addClass('red-ui-component-spinner-contain');
                var payload = {
                    name: keyNameInput.val()
                };

                // var selectedButton = bg.find(".selected");
                // if (selectedButton[0] === addLocalButton[0]) {
                //     payload.type = "local";
                //     payload.publicKeyPath = localPublicKeyPathInput.val();
                //     payload.privateKeyPath = localPrivateKeyPathInput.val();
                // } else if (selectedButton[0] === uploadButton[0]) {
                //     payload.type = "upload";
                //     payload.publicKey = publicKeyInput.val();
                //     payload.privateKey = privateKeyInput.val();
                // } else if (selectedButton[0] === generateButton[0]) {
                    payload.type = "generate";
                    payload.comment = gitEmailInput.val();
                    payload.password = passphraseInput.val();
                    payload.size = 4096;
                // }
                var done = function(err) {
                    spinner.remove();
                    if (err) {
                        return;
                    }
                    hideEditForm();
                }
                // console.log(JSON.stringify(payload,null,4));
                RED.deploy.setDeployInflight(true);
                utils.sendRequest({
                    url: "settings/user/keys",
                    type: "POST",
                    responses: {
                        0: function(error) {
                            done(error);
                        },
                        200: function(data) {
                            refreshSSHKeyList(payload.name);
                            done();
                        },
                        400: {
                            'unexpected_error': function(error) {
                                console.log(error);
                                done(error);
                            }
                        },
                    }
                },payload);
            });

        row = $('<div class="red-ui-settings-row red-ui-projects-dialog-list"></div>').appendTo(container);
        var emptyItem = { empty: true };
        var expandKey = function(container,entry) {
            var row = $('<div class="red-ui-projects-dialog-ssh-public-key">',{style:"position:relative"}).appendTo(container);
            var keyBox = $('<pre>',{style:"min-height: 80px"}).appendTo(row);
            var spinner = utils.addSpinnerOverlay(keyBox).addClass('red-ui-component-spinner-contain');
            var options = {
                url: 'settings/user/keys/'+entry.name,
                type: "GET",
                responses: {
                    200: function(data) {
                        keyBox.text(data.publickey);
                        spinner.remove();
                    },
                    400: {
                        'unexpected_error': function(error) {
                            console.log(error);
                            spinner.remove();
                        }
                    },
                }
            }
            utils.sendRequest(options);

            var formButtons = $('<span class="button-row" style="position: relative; float: right; margin: 10px;"></span>').appendTo(row);
            $('<button class="red-ui-button red-ui-button-small">'+RED._("editor:sidebar.project.userSettings.copyPublicKey")+'</button>')
                .appendTo(formButtons)
                .on("click", function(evt) {
                    try {
                        evt.stopPropagation();
                        evt.preventDefault();
                        document.getSelection().selectAllChildren(keyBox[0]);
                        var ret = document.execCommand('copy');
                        document.getSelection().empty();
                    } catch(err) {

                    }

                });

            return row;
        }
        var keyList = $('<ol class="red-ui-projects-dialog-ssh-key-list">').appendTo(row).editableList({
            height: 'auto',
            addButton: false,
            scrollOnAdd: false,
            addItem: function(row,index,entry) {
                var container = $('<div class="red-ui-projects-dialog-list-entry">').appendTo(row);
                if (entry.empty) {
                    container.addClass('red-ui-search-empty');
                    container.text(RED._("editor:sidebar.project.userSettings.noSshKeys"));
                    return;
                }
                var topRow = $('<div class="red-ui-projects-dialog-ssh-key-header">').appendTo(container);
                $('<span class="entry-icon"><i class="fa fa-key"></i></span>').appendTo(topRow);
                $('<span class="entry-name">').text(entry.name).appendTo(topRow);
                var tools = $('<span class="button-row entry-tools">').appendTo(topRow);
                var expandedRow;
                topRow.on("click", function(e) {
                    if (expandedRow) {
                        expandedRow.slideUp(200,function() {
                            expandedRow.remove();
                            expandedRow = null;
                        })
                    } else {
                        expandedRow = expandKey(container,entry);
                    }
                    })
                if (!entry.system) {
                    $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>')
                        .appendTo(tools)
                        .on("click", function(e) {
                            e.stopPropagation();
                            var spinner = utils.addSpinnerOverlay(row).addClass('red-ui-component-spinner-contain');
                            var notification = RED.notify(RED._("editor:sidebar.project.userSettings.deleteConfirm", {name:entry.name}), {
                                type: 'warning',
                                modal: true,
                                fixed: true,
                                buttons: [
                                    {
                                        text: RED._("common.label.cancel"),
                                        click: function() {
                                            spinner.remove();
                                            notification.close();
                                        }
                                    },
                                    {
                                        text: RED._("editor:sidebar.project.userSettings.delete"),
                                        click: function() {
                                            notification.close();
                                            var url = "settings/user/keys/"+entry.name;
                                            var options = {
                                                url: url,
                                                type: "DELETE",
                                                responses: {
                                                    200: function(data) {
                                                        row.fadeOut(200,function() {
                                                            keyList.editableList('removeItem',entry);
                                                            setTimeout(spinner.remove, 100);
                                                            if (keyList.editableList('length') === 0) {
                                                                keyList.editableList('addItem',emptyItem);
                                                            }
                                                        });
                                                    },
                                                    400: {
                                                        'unexpected_error': function(error) {
                                                            console.log(error);
                                                            spinner.remove();
                                                        }
                                                    },
                                                }
                                            }
                                            utils.sendRequest(options);
                                        }
                                    }
                                ]
                            });
                        });
                }
                if (entry.expand) {
                    expandedRow = expandKey(container,entry);
                }
            }
        });

        var refreshSSHKeyList = function(justAdded) {
            $.getJSON("settings/user/keys",function(result) {
                if (result.keys) {
                    result.keys.sort(function(A,B) {
                        return A.name.localeCompare(B.name);
                    });
                    keyList.editableList('empty');
                    result.keys.forEach(function(key) {
                        if (key.name === justAdded) {
                            key.expand = true;
                        }
                        keyList.editableList('addItem',key);
                    });
                    if (keyList.editableList('length') === 0) {
                        keyList.editableList('addItem',emptyItem);
                    }

                }
            })
        }
        refreshSSHKeyList();

    }

    function createSettingsPane(activeProject) {
        var pane = $('<div id="red-ui-settings-tab-gitconfig" class="project-settings-tab-pane red-ui-help"></div>');
        createGitUserSection(pane);
        createWorkflowSection(pane);
        createSSHKeySection(pane);
        return pane;
    }

    var utils;
    function init(_utils) {
        utils = _utils;
        RED.userSettings.add({
            id:'gitconfig',
            title: RED._("editor:sidebar.project.userSettings.gitConfig"),
            get: createSettingsPane,
            close: function() {
                var currentGitSettings = RED.settings.get('git') || {};
                currentGitSettings.user = currentGitSettings.user || {};
                currentGitSettings.user.name = gitUsernameInput.val();
                currentGitSettings.user.email = gitEmailInput.val();
                currentGitSettings.workflow = currentGitSettings.workflow || {};
                currentGitSettings.workflow.mode = $('[name="user-setting-gitworkflow"][type="radio"]:checked').val()

                RED.settings.set('git', currentGitSettings);
            }
        });
    }

    return {
        init: init,
    };
})();
;/**
 * 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.
 **/
RED.sidebar.versionControl = (function() {

    var sidebarContent;
    var sections;

    var allChanges = {};

    var unstagedChangesList;
    var stageAllButton;
    var stagedChangesList;
    var unstageAllButton;
    var unstagedChanges;
    var stagedChanges;
    var bulkChangeSpinner;
    var unmergedContent;
    var unmergedChangesList;
    var commitButton;
    var localChanges;

    var localCommitList;
    var localCommitListShade;
    // var remoteCommitList;

    var isMerging;

    function viewFileDiff(entry,state) {
        var activeProject = RED.projects.getActiveProject();
        var diffTarget = (state === 'staged')?"index":"tree";
        utils.sendRequest({
            url: "projects/"+activeProject.name+"/diff/"+diffTarget+"/"+encodeURIComponent(entry.file),
            type: "GET",
            responses: {
                0: function(error) {
                    console.log(error);
                    // done(error,null);
                },
                200: function(data) {
                    var title;
                    if (state === 'unstaged') {
                        title = RED._("sidebar.project.versionControl.unstagedChanges")+' : '+entry.file
                    } else if (state === 'staged') {
                        title = RED._("sidebar.project.versionControl.stagedChanges")+' : '+entry.file
                    } else {
                        title = RED._("sidebar.project.versionControl.resolveConflicts")+' : '+entry.file
                    }
                    var options = {
                        diff: data.diff,
                        title: title,
                        unmerged: state === 'unmerged',
                        project: activeProject
                    }
                    if (state == 'unstaged') {
                        options.oldRevTitle = entry.indexStatus === " "?RED._("sidebar.project.versionControl.head"):RED._("sidebar.project.versionControl.staged");
                        options.newRevTitle = RED._("sidebar.project.versionControl.unstaged");
                        options.oldRev = entry.indexStatus === " "?"@":":0";
                        options.newRev = "_";
                    } else if (state === 'staged') {
                        options.oldRevTitle = RED._("sidebar.project.versionControl.head");
                        options.newRevTitle = RED._("sidebar.project.versionControl.staged");
                        options.oldRev = "@";
                        options.newRev = ":0";
                    } else {
                        options.oldRevTitle = RED._("sidebar.project.versionControl.local");
                        options.newRevTitle = RED._("sidebar.project.versionControl.remote");
                        options.commonRev = ":1";
                        options.oldRev = ":2";
                        options.newRev = ":3";
                        options.onresolve = function(resolution) {
                            utils.sendRequest({
                                url: "projects/"+activeProject.name+"/resolve/"+encodeURIComponent(entry.file),
                                type: "POST",
                                responses: {
                                    0: function(error) {
                                        console.log(error);
                                        // done(error,null);
                                    },
                                    200: function(data) {
                                        refresh(true);
                                    },
                                    400: {
                                        'unexpected_error': function(error) {
                                            console.log(error);
                                            // done(error,null);
                                        }
                                    },
                                }
                            },{resolutions:resolution.resolutions[entry.file]});
                        }
                    }
                    RED.diff.showUnifiedDiff(options);
                },
                400: {
                    'unexpected_error': function(error) {
                        console.log(error);
                        // done(error,null);
                    }
                }
            }
        })
    }

    function createChangeEntry(row, entry, status, state) {
        row.addClass("red-ui-sidebar-vc-change-entry");
        var container = $('<div>').appendTo(row);
        if (entry.label) {
            row.addClass('red-ui-help-info-none');
            container.text(entry.label);
            if (entry.button) {
                container.css({
                    display: "inline-block",
                    maxWidth: "300px",
                    textAlign: "left"
                })
                var toolbar = $('<div style="float: right; margin: 5px; height: 50px;"></div>').appendTo(container);

                $('<button class="red-ui-button red-ui-button-small"></button>').text(entry.button.label)
                    .appendTo(toolbar)
                    .on("click", entry.button.click);
            }
            return;
        }


        var icon = $('<i class=""></i>').appendTo(container);
        var entryLink = $('<a href="#">')
            .appendTo(container)
            .on("click", function(e) {
                e.preventDefault();
                viewFileDiff(entry,state);
            });
        var label = $('<span>').appendTo(entryLink);

        var entryTools = $('<div class="red-ui-sidebar-vc-change-entry-tools">').appendTo(row);
        var bg;
        var revertButton;
        if (state === 'unstaged') {
            bg = $('<span class="button-group" style="margin-right: 5px;"></span>').appendTo(entryTools);
            revertButton = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-reply"></i></button>')
                .appendTo(bg)
                .on("click", function(evt) {
                    evt.preventDefault();

                    var spinner = utils.addSpinnerOverlay(container).addClass('red-ui-component-spinner-contain');
                    var notification = RED.notify(RED._("sidebar.project.versionControl.revert",{file:entry.file}), {
                        type: "warning",
                        modal: true,
                        fixed: true,
                        buttons: [
                            {
                                text: RED._("common.label.cancel"),
                                click: function() {
                                    spinner.remove();
                                    notification.close();
                                }
                            },{
                                text: RED._("sidebar.project.versionControl.revertChanges"),
                                click: function() {
                                    notification.close();
                                    var activeProject = RED.projects.getActiveProject();
                                    var url = "projects/"+activeProject.name+"/files/_/"+entry.file;
                                    var options = {
                                        url: url,
                                        type: "DELETE",
                                        responses: {
                                            200: function(data) {
                                                spinner.remove();
                                            },
                                            400: {
                                                'unexpected_error': function(error) {
                                                    spinner.remove();
                                                    console.log(error);
                                                    // done(error,null);
                                                }
                                            }
                                        }
                                    }
                                    RED.deploy.setDeployInflight(true);
                                    utils.sendRequest(options).always(function() {
                                        setTimeout(function() {
                                            RED.deploy.setDeployInflight(false);
                                        },500);
                                    });
                                }
                            }

                        ]
                    })
                });
            RED.popover.tooltip(revertButton,RED._("sidebar.project.versionControl.revertChanges"));
        }
        bg = $('<span class="button-group"></span>').appendTo(entryTools);
        if (state !== 'unmerged') {
            var stageButton = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-'+((state==='unstaged')?"plus":"minus")+'"></i></button>')
                .appendTo(bg)
                .on("click", function(evt) {
                    evt.preventDefault();
                    var activeProject = RED.projects.getActiveProject();
                    entry.spinner = utils.addSpinnerOverlay(row).addClass('projects-version-control-spinner-sidebar');
                    utils.sendRequest({
                        url: "projects/"+activeProject.name+"/stage/"+encodeURIComponent(entry.file),
                        type: (state==='unstaged')?"POST":"DELETE",
                        responses: {
                            0: function(error) {
                                console.log(error);
                                // done(error,null);
                            },
                            200: function(data) {
                                refreshFiles(data);
                            },
                            400: {
                                'unexpected_error': function(error) {
                                    console.log(error);
                                    // done(error,null);
                                }
                            },
                        }
                    },{});
                });
            RED.popover.tooltip(stageButton,RED._("sidebar.project.versionControl."+((state==='unstaged')?"stage":"unstage")+"Change"));
        }
        entry["update"+((state==='unstaged')?"Unstaged":"Staged")] = function(entry,status) {
            container.removeClass();
            var iconClass = "";
            if (status === 'A') {
                container.addClass("red-ui-diff-state-added");
                iconClass = "fa-plus-square";
            } else if (status === '?') {
                container.addClass("red-ui-diff-state-unchanged");
                iconClass = "fa-question-circle-o";
            } else if (status === 'D') {
                container.addClass("red-ui-diff-state-deleted");
                iconClass = "fa-minus-square";
            } else if (status === 'M') {
                container.addClass("red-ui-diff-state-changed");
                iconClass = "fa-square";
            } else if (status === 'R') {
                container.addClass("red-ui-diff-state-changed");
                iconClass = "fa-toggle-right";
            } else if (status === 'U') {
                container.addClass("red-ui-diff-state-conflicted");
                iconClass = "fa-exclamation-triangle";
            } else {
                iconClass = "fa-exclamation-triangle"
            }
            label.empty();
            $('<span>').text(entry.file.replace(/\\(.)/g,"$1")).appendTo(label);

            if (entry.oldName) {
                $('<i class="fa fa-long-arrow-right"></i>').prependTo(label);
                $('<span>').text(entry.oldName.replace(/\\(.)/g,"$1")).prependTo(label);
                // label.text(entry.oldName+" -> "+entry.file);
            }
            // console.log(entry.file,status,iconClass);

            icon.removeClass();
            icon.addClass("fa "+iconClass);
            if (entry.spinner) {
                entry.spinner.remove();
                delete entry.spinner;
            }

            if (revertButton) {
                revertButton.toggle(status !== '?');
            }
            entryLink.toggleClass("disabled",(status === 'D' || status === '?'));
        }
        entry["update"+((state==='unstaged')?"Unstaged":"Staged")](entry, status);
    }
    var utils;
    var emptyStagedItem;
    var emptyMergedItem;
    function init(_utils) {
        utils = _utils;

        RED.actions.add("core:show-version-control-tab",show);
        RED.events.on("deploy", function() {
            var activeProject = RED.projects.getActiveProject();
            if (activeProject) {
                // TODO: this is a full refresh of the files - should be able to
                //       just do an incremental refresh

                // Get the default workflow mode from theme settings
                var defaultWorkflowMode = RED.settings.theme("projects.workflow.mode","manual");
                // Check for the user-defined choice of mode
                var workflowMode = ((RED.settings.get('git') || {}).workflow || {}).mode || defaultWorkflowMode;
                if (workflowMode === 'auto') {
                    refresh(true);
                } else {
                    allChanges = {};
                    unstagedChangesList.editableList('empty');
                    stagedChangesList.editableList('empty');
                    unmergedChangesList.editableList('empty');

                    $.getJSON("projects/"+activeProject.name+"/status",function(result) {
                        refreshFiles(result);
                    });
                }
            }
        });
        RED.events.on("login",function() {
            refresh(true);
        });
        sidebarContent = $('<div>', {class:"red-ui-sidebar-vc"});
        var stackContainer = $("<div>",{class:"red-ui-sidebar-vc-stack"}).appendTo(sidebarContent);
        sections = RED.stack.create({
            container: stackContainer,
            fill: true,
            singleExpanded: true
        });

        localChanges = sections.add({
            title: RED._("sidebar.project.versionControl.localChanges"),
            collapsible: true
        });
        localChanges.expand();
        localChanges.content.css({height:"100%"});

        var bg = $('<div style="float: right"></div>').appendTo(localChanges.header);
        var refreshButton = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>')
            .appendTo(bg)
            .on("click", function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                refresh(true);
            });
        RED.popover.tooltip(refreshButton,RED._("sidebar.project.versionControl.refreshChanges"));

        emptyStagedItem = { label: RED._("sidebar.project.versionControl.none") };
        emptyMergedItem = { label: RED._("sidebar.project.versionControl.conflictResolve") };

        var unstagedContent = $('<div class="red-ui-sidebar-vc-change-container"></div>').appendTo(localChanges.content);
        var header = $('<div class="red-ui-sidebar-vc-change-header">'+RED._("sidebar.project.versionControl.localFiles")+'</div>').appendTo(unstagedContent);
        stageAllButton = $('<button class="red-ui-button red-ui-button-small" style="position: absolute; right: 5px; top: 5px;"><i class="fa fa-plus"></i> '+RED._("sidebar.project.versionControl.all")+'</button>')
            .appendTo(header)
            .on("click", function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                var toStage = Object.keys(allChanges).filter(function(fn) {
                    return allChanges[fn].treeStatus !== ' ';
                });
                updateBulk(toStage,true);
            });
        RED.popover.tooltip(stageAllButton,RED._("sidebar.project.versionControl.stageAllChange"));
        unstagedChangesList = $("<ol>",{style:"position: absolute; top: 30px; bottom: 0; right:0; left:0;"}).appendTo(unstagedContent);
        unstagedChangesList.editableList({
            addButton: false,
            scrollOnAdd: false,
            addItem: function(row,index,entry) {
                createChangeEntry(row,entry,entry.treeStatus,'unstaged');
            },
            sort: function(A,B) {
                if (A.treeStatus === '?' && B.treeStatus !== '?') {
                    return 1;
                } else if (A.treeStatus !== '?' && B.treeStatus === '?') {
                    return -1;
                }
                return A.file.localeCompare(B.file);
            }

        })

        unmergedContent = $('<div class="red-ui-sidebar-vc-change-container"></div>').appendTo(localChanges.content);

        header = $('<div class="red-ui-sidebar-vc-change-header">'+RED._("sidebar.project.versionControl.unmergedChanges")+'</div>').appendTo(unmergedContent);
        bg = $('<div style="position: absolute; right: 5px; top: 5px;"></div>').appendTo(header);
        var abortMergeButton = $('<button class="red-ui-button red-ui-button-small" style="margin-right: 5px;">'+RED._("sidebar.project.versionControl.abortMerge")+'</button>')
            .appendTo(bg)
            .on("click", function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                var spinner = utils.addSpinnerOverlay(unmergedContent);
                var activeProject = RED.projects.getActiveProject();
                RED.deploy.setDeployInflight(true);
                utils.sendRequest({
                    url: "projects/"+activeProject.name+"/merge",
                    type: "DELETE",
                    responses: {
                        0: function(error) {
                            console.log(error);
                        },
                        200: function(data) {
                            spinner.remove();
                            refresh(true);
                        },
                        400: {
                            'unexpected_error': function(error) {
                                console.log(error);
                            }
                        },
                    }
                }).always(function() {
                    setTimeout(function() {
                        RED.deploy.setDeployInflight(false);
                    },500);
                });
            });
        unmergedChangesList = $("<ol>",{style:"position: absolute; top: 30px; bottom: 0; right:0; left:0;"}).appendTo(unmergedContent);
        unmergedChangesList.editableList({
            addButton: false,
            scrollOnAdd: false,
            addItem: function(row,index,entry) {
                if (entry === emptyMergedItem) {
                    entry.button = {
                        label: RED._("sidebar.project.versionControl.commit"),
                        click: function(evt) {
                            evt.preventDefault();
                            evt.stopPropagation();
                            showCommitBox();
                        }
                    }
                }
                createChangeEntry(row,entry,entry.treeStatus,'unmerged');
            },
            sort: function(A,B) {
                if (A.treeStatus === '?' && B.treeStatus !== '?') {
                    return 1;
                } else if (A.treeStatus !== '?' && B.treeStatus === '?') {
                    return -1;
                }
                return A.file.localeCompare(B.file);
            }

        })


        var stagedContent = $('<div class="red-ui-sidebar-vc-change-container"></div>').appendTo(localChanges.content);

        header = $('<div class="red-ui-sidebar-vc-change-header">'+RED._("sidebar.project.versionControl.changeToCommit")+'</div>').appendTo(stagedContent);

        bg = $('<div style="position: absolute; right: 5px; top: 5px;"></div>').appendTo(header);
        var showCommitBox = function() {
            commitMessage.val("");
            submitCommitButton.prop("disabled",true);
            unstagedContent.css("height","30px");
            if (unmergedContent.is(":visible")) {
                unmergedContent.css("height","30px");
                stagedContent.css("height","calc(100% - 60px - 175px)");
            } else {
                stagedContent.css("height","calc(100% - 30px - 175px)");
            }
            commitBox.show();
            setTimeout(function() {
                commitBox.css("height","175px");
            },10);
            stageAllButton.prop("disabled",true);
            unstageAllButton.prop("disabled",true);
            commitButton.prop("disabled",true);
            abortMergeButton.prop("disabled",true);
            commitMessage.trigger("focus");
        }
        commitButton = $('<button class="red-ui-button red-ui-button-small" style="margin-right: 5px;">'+RED._("sidebar.project.versionControl.commit")+'</button>')
            .appendTo(bg)
            .on("click", function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                showCommitBox();
            });
        RED.popover.tooltip(commitButton,RED._("sidebar.project.versionControl.commitChanges"));
        unstageAllButton = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-minus"></i> '+RED._("sidebar.project.versionControl.all")+'</button>')
            .appendTo(bg)
            .on("click", function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                var toUnstage = Object.keys(allChanges).filter(function(fn) {
                    return allChanges[fn].indexStatus !== ' ' && allChanges[fn].indexStatus !== '?';
                });
                updateBulk(toUnstage,false);

            });
        RED.popover.tooltip(unstageAllButton,RED._("sidebar.project.versionControl.unstageAllChange"));


        stagedChangesList = $("<ol>",{style:"position: absolute; top: 30px; bottom: 0; right:0; left:0;"}).appendTo(stagedContent);
        stagedChangesList.editableList({
            addButton: false,
            scrollOnAdd: false,
            addItem: function(row,index,entry) {
                createChangeEntry(row,entry,entry.indexStatus,'staged');
            },
            sort: function(A,B) {
                return A.file.localeCompare(B.file);
            }
        })

        commitBox = $('<div class="red-ui-sidebar-vc-slide-box red-ui-sidebar-vc-slide-box-bottom"></div>').hide().appendTo(localChanges.content);

        var commitMessage = $('<textarea></textarea>').attr("placeholder",RED._("sidebar.project.versionControl.commitPlaceholder"))
            .appendTo(commitBox)
            .on("change keyup paste",function() {
                submitCommitButton.prop('disabled',$(this).val().trim()==="");
            });
        var commitToolbar = $('<div class="red-ui-sidebar-vc-slide-box-toolbar button-group">').appendTo(commitBox);

        var cancelCommitButton = $('<button class="red-ui-button">'+RED._("sidebar.project.versionControl.cancelCapital")+'</button>')
            .appendTo(commitToolbar)
            .on("click", function(evt) {
                evt.preventDefault();
                commitMessage.val("");
                unstagedContent.css("height","");
                unmergedContent.css("height","");
                stagedContent.css("height","");
                commitBox.css("height",0);
                setTimeout(function() {
                    commitBox.hide();
                },200);
                stageAllButton.prop("disabled",false);
                unstageAllButton.prop("disabled",false);
                commitButton.prop("disabled",false);
                abortMergeButton.prop("disabled",false);

            })
        var submitCommitButton = $('<button class="red-ui-button">'+RED._("sidebar.project.versionControl.commitCapital")+'</button>')
            .appendTo(commitToolbar)
            .on("click", function(evt) {
                evt.preventDefault();
                var spinner = utils.addSpinnerOverlay(submitCommitButton).addClass('red-ui-component-spinner-sidebar');
                var activeProject = RED.projects.getActiveProject();
                RED.deploy.setDeployInflight(true);
                utils.sendRequest({
                    url: "projects/"+activeProject.name+"/commit",
                    type: "POST",
                    responses: {
                        0: function(error) {
                            console.log(error);
                        },
                        200: function(data) {
                            spinner.remove();
                            cancelCommitButton.trigger("click");
                            refresh(true);
                        },
                        400: {
                            '*': function(error) {
                                utils.reportUnexpectedError(error);
                            }
                        },
                    }
                },{
                    message:commitMessage.val()
                }).always(function() {
                    setTimeout(function() {
                        RED.deploy.setDeployInflight(false);
                    },500);
                })
            })


        var localHistory = sections.add({
            title: RED._("sidebar.project.versionControl.commitHistory"),
            collapsible: true
        });

        bg = $('<div style="float: right"></div>').appendTo(localHistory.header);
        refreshButton = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>')
            .appendTo(bg)
            .on("click", function(evt) {
                evt.preventDefault();
                evt.stopPropagation();
                refresh(true,true);
            })
        RED.popover.tooltip(refreshButton,RED._("sidebar.project.versionControl.refreshCommitHistory"))

        var localBranchToolbar = $('<div class="red-ui-sidebar-vc-change-header" style="text-align: right;"></div>').appendTo(localHistory.content);

        var localBranchButton = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-code-fork"></i> '+RED._("sidebar.project.versionControl.branch")+' <span id="red-ui-sidebar-vc-local-branch"></span></button>')
            .appendTo(localBranchToolbar)
            .on("click", function(evt) {
                evt.preventDefault();
                if ($(this).hasClass('selected')) {
                    closeBranchBox();
                } else {
                    closeRemoteBox();
                    localCommitListShade.show();
                    $(this).addClass('selected');
                    var activeProject = RED.projects.getActiveProject();
                    localBranchList.refresh("projects/"+activeProject.name+"/branches");
                    localBranchBox.show();
                    setTimeout(function() {
                        localBranchBox.css("height","215px");
                        localBranchList.focus();
                    },100);
                }
            })
        RED.popover.tooltip(localBranchButton,RED._("sidebar.project.versionControl.changeLocalBranch"))
        var repoStatusButton = $('<button class="red-ui-button red-ui-button-small" style="margin-left: 10px;" id="red-ui-sidebar-vc-repo-status-button">'+
                                 '<span id="red-ui-sidebar-vc-repo-status-stats">'+
                                    '<i class="fa fa-long-arrow-up"></i> <span id="red-ui-sidebar-vc-commits-ahead"></span> '+
                                    '<i class="fa fa-long-arrow-down"></i> <span id="red-ui-sidebar-vc-commits-behind"></span>'+
                                 '</span>'+
                                 '<span id="red-ui-sidebar-vc-repo-status-auth-issue">'+
                                    '<i class="fa fa-warning"></i>'+
                                 '</span>'+
                                 '</button>')
            .appendTo(localBranchToolbar)
            .on("click", function(evt) {
                evt.preventDefault();
                if ($(this).hasClass('selected')) {
                    closeRemoteBox();
                } else {
                    closeBranchBox();
                    localCommitListShade.show();
                    $(this).addClass('selected');
                    var activeProject = RED.projects.getActiveProject();
                    $("#red-ui-sidebar-vc-repo-toolbar-set-upstream-row").toggle(!!activeProject.git.branches.remoteAlt);
                    remoteBox.show();

                    setTimeout(function() {
                        remoteBox.css("height","265px");
                    },100);

                }
            });
        RED.popover.tooltip(repoStatusButton,RED._("sidebar.project.versionControl.manageRemoteBranch"))

        localCommitList = $("<ol>",{style:"position: absolute; top: 30px; bottom: 0px; right:0; left:0;"}).appendTo(localHistory.content);
        localCommitListShade = $('<div class="red-ui-shade" style="z-Index: 3"></div>').css('top',"30px").hide().appendTo(localHistory.content);
        localCommitList.editableList({
            addButton: false,
            scrollOnAdd: false,
            addItem: function(row,index,entry) {
                row.addClass('red-ui-sidebar-vc-commit-entry');
                if (entry.url) {
                    row.addClass('red-ui-sidebar-vc-commit-more');
                    row.text("+ "+(entry.total-entry.totalKnown)+RED._("sidebar.project.versionControl.moreCommits"));
                    row.on("click", function(e) {
                        e.preventDefault();
                        getCommits(entry.url,localCommitList,row,entry.limit,entry.before);
                    })
                } else {
                    row.on("click", function(e) {
                        var activeProject = RED.projects.getActiveProject();
                        if (activeProject) {
                            $.getJSON("projects/"+activeProject.name+"/commits/"+entry.sha,function(result) {
                                result.project = activeProject;
                                result.parents = entry.parents;
                                result.oldRev = entry.parents[0].length !== 0 ? entry.sha+"~1" : entry.sha;
                                result.newRev = entry.sha;
                                result.oldRevTitle = entry.parents[0].length !== 0 ? RED._("sidebar.project.versionControl.commitCapital")+" "+entry.sha.substring(0,7)+"~1" : " ";
                                result.newRevTitle = RED._("sidebar.project.versionControl.commitCapital")+" "+entry.sha.substring(0,7);
                                result.date = humanizeSinceDate(parseInt(entry.date));
                                RED.diff.showCommitDiff(result);
                            });
                        }
                    });
                    var container = $('<div>').appendTo(row);
                    $('<div class="red-ui-sidebar-vc-commit-subject">').text(entry.subject).appendTo(container);
                    if (entry.refs) {
                        var refDiv = $('<div class="red-ui-sidebar-vc-commit-refs">').appendTo(container);
                        entry.refs.forEach(function(ref) {
                            var label = ref;
                            if (/HEAD -> /.test(ref)) {
                                label = ref.substring(8);
                            }
                            $('<span class="red-ui-sidebar-vc-commit-ref">').text(label).appendTo(refDiv);
                        });
                        row.addClass('red-ui-sidebar-vc-commit-head');
                    }
                    $('<div class="red-ui-sidebar-vc-commit-sha">').text(entry.sha.substring(0,7)).appendTo(container);
                    // $('<div class="red-ui-sidebar-vc-commit-user">').text(entry.author).appendTo(container);
                    $('<div class="red-ui-sidebar-vc-commit-date">').text(humanizeSinceDate(parseInt(entry.date))).appendTo(container);
                }
            }
        });


        var closeBranchBox = function(done) {
            localBranchButton.removeClass('selected')
            localBranchBox.css("height","0");
            localCommitListShade.hide();

            setTimeout(function() {
                localBranchBox.hide();
                if (done) { done() }
            },200);
        }
        var localBranchBox = $('<div class="red-ui-sidebar-vc-slide-box red-ui-sidebar-vc-slide-box-top" style="top:30px;"></div>').hide().appendTo(localHistory.content);

        $('<div class="red-ui-sidebar-vc-slide-box-header"></div>').text(RED._("sidebar.project.versionControl.changeLocalBranch")).appendTo(localBranchBox);

        var localBranchList = utils.createBranchList({
            placeholder: RED._("sidebar.project.versionControl.createBranchPlaceholder"),
            container: localBranchBox,
            onselect: function(body) {
                if (body.current) {
                    return closeBranchBox();
                }
                var spinner = utils.addSpinnerOverlay(localBranchBox);
                var activeProject = RED.projects.getActiveProject();
                RED.deploy.setDeployInflight(true);
                utils.sendRequest({
                    url: "projects/"+activeProject.name+"/branches",
                    type: "POST",
                    requireCleanWorkspace: true,
                    cancel: function() {
                        spinner.remove();
                    },
                    responses: {
                        0: function(error) {
                            spinner.remove();
                            console.log(error);
                            // done(error,null);
                        },
                        200: function(data) {
                            // Changing branch will trigger a runtime event
                            // that leads to a project refresh.
                            closeBranchBox(function() {
                                spinner.remove();
                            });
                        },
                        400: {
                            'git_local_overwrite': function(error) {
                                spinner.remove();
                                RED.notify(RED._("sidebar.project.versionControl.localOverwrite"),{
                                    type:'error',
                                    timeout: 8000
                                });
                            },
                            'unexpected_error': function(error) {
                                spinner.remove();
                                console.log(error);
                                // done(error,null);
                            }
                        },
                    }
                },body).always(function(){
                    setTimeout(function() {
                        RED.deploy.setDeployInflight(false);
                    },500);
                });
            }
        });

        var remoteBox = $('<div class="red-ui-sidebar-vc-slide-box red-ui-sidebar-vc-slide-box-top" style="top:30px"></div>').hide().appendTo(localHistory.content);
        var closeRemoteBox = function() {
            $("#red-ui-sidebar-vc-repo-toolbar-set-upstream").prop('checked',false);
            repoStatusButton.removeClass('selected')
            remoteBox.css("height","0");
            localCommitListShade.hide();
            setTimeout(function() {
                remoteBox.hide();
                closeRemoteBranchBox();
            },200);
        }

        var closeRemoteBranchBox = function(done) {
            if (remoteBranchButton.hasClass('selected')) {
                remoteBranchButton.removeClass('selected');
                remoteBranchSubRow.height(0);
                remoteBox.css("height","265px");
                setTimeout(function() {
                    remoteBranchSubRow.hide();
                    if (done) { done(); }
                },200);
            }
        }
        $('<div class="red-ui-sidebar-vc-slide-box-header"></div>').text(RED._("sidebar.project.versionControl.manageRemoteBranch")).appendTo(remoteBox);

        var remoteBranchRow = $('<div style="margin-bottom: 5px;"></div>').appendTo(remoteBox);
        var remoteBranchButton = $('<button id="red-ui-sidebar-vc-repo-branch" class="red-ui-sidebar-vc-repo-action red-ui-button"><i class="fa fa-code-fork"></i> '+RED._("sidebar.project.versionControl.remote")+': <span id="red-ui-sidebar-vc-remote-branch"></span></button>')
            .appendTo(remoteBranchRow)
            .on("click", function(evt) {
                evt.preventDefault();
                if ($(this).hasClass('selected')) {
                    closeRemoteBranchBox();
                } else {
                    $(this).addClass('selected');
                    var activeProject = RED.projects.getActiveProject();
                    remoteBranchList.refresh("projects/"+activeProject.name+"/branches/remote");
                    remoteBranchSubRow.show();
                    setTimeout(function() {
                        remoteBranchSubRow.height(180);
                        remoteBox.css("height","445px");
                        remoteBranchList.focus();
                    },100);
                }
            });

        $('<div id="red-ui-sidebar-vc-repo-toolbar-message" class="red-ui-sidebar-vc-slide-box-header" style="min-height: 100px;"></div>').appendTo(remoteBox);


        var errorMessage = $('<div id="red-ui-sidebar-vc-repo-toolbar-error-message" class="red-ui-sidebar-vc-slide-box-header" style="min-height: 100px;"></div>').hide().appendTo(remoteBox);
        $('<div style="margin-top: 10px;"><i class="fa fa-warning"></i> '+RED._("sidebar.project.versionControl.unableToAccess")+'</div>').appendTo(errorMessage)
        var buttonRow = $('<div style="margin: 10px 30px; text-align: center"></div>').appendTo(errorMessage);
        $('<button class="red-ui-button" style="width: 80%;"><i class="fa fa-refresh"></i> '+RED._("sidebar.project.versionControl.retry")+'</button>')
            .appendTo(buttonRow)
            .on("click", function(e) {
                e.preventDefault();
                var activeProject = RED.projects.getActiveProject();
                var spinner = utils.addSpinnerOverlay(remoteBox).addClass("red-ui-component-spinner-contain");
                utils.sendRequest({
                    url: "projects/"+activeProject.name+"/branches/remote",
                    type: "GET",
                    responses: {
                        0: function(error) {
                            console.log(error);
                            // done(error,null);
                        },
                        200: function(data) {
                            refresh(true);
                        },
                        400: {
                            'git_connection_failed': function(error) {
                                RED.notify(error.message,'error');
                            },
                            'git_not_a_repository': function(error) {
                                RED.notify(error.message,'error');
                            },
                            'git_repository_not_found': function(error) {
                                RED.notify(error.message,'error');
                            },
                            'unexpected_error': function(error) {
                                console.log(error);
                                // done(error,null);
                            }
                        }
                    }
                }).always(function() {
                    spinner.remove();
                });
            })

        $('<div class="red-ui-sidebar-vc-slide-box-header" style="height: 20px;"><label id="red-ui-sidebar-vc-repo-toolbar-set-upstream-row" for="red-ui-sidebar-vc-repo-toolbar-set-upstream" class="hide"><input type="checkbox" id="red-ui-sidebar-vc-repo-toolbar-set-upstream"> '+RED._("sidebar.project.versionControl.setUpstreamBranch")+'</label></div>').appendTo(remoteBox);

        var remoteBranchSubRow = $('<div style="height: 0;overflow:hidden; transition: height 0.2s ease-in-out;"></div>').hide().appendTo(remoteBranchRow);
        var remoteBranchList = utils.createBranchList({
            placeholder: RED._("sidebar.project.versionControl.createRemoteBranchPlaceholder"),
            currentLabel: RED._("sidebar.project.versionControl.upstream"),
            remotes: function() {
                var project = RED.projects.getActiveProject();
                return Object.keys(project.git.remotes);
            },
            container: remoteBranchSubRow,
            onselect: function(body) {
                $("#red-ui-sidebar-vc-repo-toolbar-set-upstream").prop('checked',false);
                $("#red-ui-sidebar-vc-repo-toolbar-set-upstream").prop('disabled',false);
                $("#red-ui-sidebar-vc-remote-branch").text(body.name+(body.create?" *":""));
                var activeProject = RED.projects.getActiveProject();
                if (activeProject.git.branches.remote === body.name) {
                    delete activeProject.git.branches.remoteAlt;
                } else {
                    activeProject.git.branches.remoteAlt = body.name;
                }
                $("#red-ui-sidebar-vc-repo-toolbar-set-upstream-row").toggle(!!activeProject.git.branches.remoteAlt);
                closeRemoteBranchBox(function() {
                    if (!body.create) {
                        var start = Date.now();
                        var spinner = utils.addSpinnerOverlay($('#red-ui-sidebar-vc-repo-toolbar-message')).addClass("red-ui-component-spinner-contain");
                        $.getJSON("projects/"+activeProject.name+"/branches/remote/"+body.name+"/status", function(result) {
                            setTimeout(function() {
                                updateRemoteStatus(result.commits.ahead, result.commits.behind);
                                spinner.remove();
                            },Math.max(400-(Date.now() - start),0));
                        })
                    } else {
                        if (!activeProject.git.branches.remote) {
                            $('#red-ui-sidebar-vc-repo-toolbar-message').text(RED._("sidebar.project.versionControl.trackedUpstreamBranch"));
                            $("#red-ui-sidebar-vc-repo-toolbar-set-upstream").prop('checked',true);
                            $("#red-ui-sidebar-vc-repo-toolbar-set-upstream").prop('disabled',true);
                        } else {
                            $('#red-ui-sidebar-vc-repo-toolbar-message').text(RED._("sidebar.project.versionControl.selectUpstreamBranch"));
                        }
                        $("#red-ui-sidebar-vc-repo-pull").prop('disabled',true);
                        $("#red-ui-sidebar-vc-repo-push").prop('disabled',false);
                    }
                });
            }
        });


        var row = $('<div style="margin-bottom: 5px;"></div>').appendTo(remoteBox);

        $('<button id="red-ui-sidebar-vc-repo-push" class="red-ui-sidebar-vc-repo-sub-action red-ui-button"><i class="fa fa-long-arrow-up"></i> <span data-i18n="sidebar.project.versionControl.push"></span></button>')
            .appendTo(row)
            .on("click", function(e) {
                e.preventDefault();
                var spinner = utils.addSpinnerOverlay(remoteBox).addClass("red-ui-component-spinner-contain");
                var buttonRow = $('<div style="position: relative; bottom: 60px;"></div>').appendTo(spinner);
                $('<button class="red-ui-button"></button>').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function(evt) {
                    evt.preventDefault();
                    RED.actions.invoke("core:show-event-log");
                });
                var activeProject = RED.projects.getActiveProject();
                RED.eventLog.startEvent("Push changes"+(activeProject.git.branches.remoteAlt?(" : "+activeProject.git.branches.remoteAlt):""));
                var url = "projects/"+activeProject.name+"/push";
                if (activeProject.git.branches.remoteAlt) {
                    url+="/"+activeProject.git.branches.remoteAlt;
                }
                var setUpstream = $("#red-ui-sidebar-vc-repo-toolbar-set-upstream").prop('checked');
                if (setUpstream) {
                    url+="?u=true"
                }
                utils.sendRequest({
                    url: url,
                    type: "POST",
                    responses: {
                        0: function(error) {
                            console.log(error);
                            // done(error,null);
                        },
                        200: function(data) {
                            if (setUpstream && activeProject.git.branches.remoteAlt) {
                                activeProject.git.branches.remote = activeProject.git.branches.remoteAlt;
                                delete activeProject.git.branches.remoteAlt;
                            }
                            refresh(true);
                            closeRemoteBox();
                        },
                        400: {
                            'git_push_failed': function(err) {
                                // TODO: better message
                                RED.notify(RED._("sidebar.project.versionControl.pushFailed"),"error");
                            },
                            'unexpected_error': function(error) {
                                console.log(error);
                                // done(error,null);
                            }
                        },
                    }
                },{}).always(function() {
                    spinner.remove();
                });
            });

        var pullRemote = function(options) {
            options = options || {};
            var spinner = utils.addSpinnerOverlay(remoteBox).addClass("red-ui-component-spinner-contain");
            var buttonRow = $('<div style="position: relative; bottom: 60px;"></div>').appendTo(spinner);
            $('<button class="red-ui-button"></button>').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function(evt) {
                evt.preventDefault();
                RED.actions.invoke("core:show-event-log");
            });
            var activeProject = RED.projects.getActiveProject();
            RED.eventLog.startEvent("Pull changes"+(activeProject.git.branches.remoteAlt?(" : "+activeProject.git.branches.remoteAlt):""));
            var url = "projects/"+activeProject.name+"/pull";
            if (activeProject.git.branches.remoteAlt) {
                url+="/"+activeProject.git.branches.remoteAlt;
            }
            if (options.setUpstream || options.allowUnrelatedHistories) {
                url+="?";
            }
            if (options.setUpstream) {
                url += "setUpstream=true"
                if (options.allowUnrelatedHistories) {
                    url += "&";
                }
            }
            if (options.allowUnrelatedHistories) {
                url += "allowUnrelatedHistories=true"
            }
            utils.sendRequest({
                url: url,
                type: "POST",
                responses: {
                    0: function(error) {
                        console.log(error);
                        // done(error,null);
                    },
                    200: function(data) {
                        if (options.setUpstream && activeProject.git.branches.remoteAlt) {
                            activeProject.git.branches.remote = activeProject.git.branches.remoteAlt;
                            delete activeProject.git.branches.remoteAlt;
                        }
                        refresh(true);
                        closeRemoteBox();
                    },
                    400: {
                        'git_local_overwrite': function(err) {
                            RED.notify(RED._("sidebar.project.versionControl.unablePull")+
                                '<p><a href="#" onclick="RED.sidebar.versionControl.showLocalChanges(); return false;">'+RED._("sidebar.project.versionControl.showUnstagedChanges")+'</a></p>',"error",false,10000000);
                        },
                        'git_pull_merge_conflict': function(err) {
                            refresh(true);
                            closeRemoteBox();
                        },
                        'git_connection_failed': function(err) {
                            RED.notify(RED._("sidebar.project.versionControl.connectionFailed")+err.toString(),"warning")
                        },
                        'git_pull_unrelated_history': function(error) {
                            var notification = RED.notify(RED._("sidebar.project.versionControl.pullUnrelatedHistory"),{
                                type: 'error',
                                modal: true,
                                fixed: true,
                                buttons: [
                                    {
                                        text: RED._("common.label.cancel"),
                                        click: function() {
                                            notification.close();
                                        }
                                    },{
                                        text: RED._("sidebar.project.versionControl.pullChanges"),
                                        click: function() {
                                            notification.close();
                                            options.allowUnrelatedHistories = true;
                                            pullRemote(options)
                                        }
                                    }
                                ]
                            });
                        },
                        '*': function(error) {
                            utils.reportUnexpectedError(error);
                        }
                    },
                }
            },{}).always(function() {
                spinner.remove();
            });
        }
        $('<button id="red-ui-sidebar-vc-repo-pull" class="red-ui-sidebar-vc-repo-sub-action red-ui-button"><i class="fa fa-long-arrow-down"></i> <span data-i18n="sidebar.project.versionControl.pull"></span></button>')
            .appendTo(row)
            .on("click", function(e) {
                e.preventDefault();
                pullRemote({
                    setUpstream: $("#red-ui-sidebar-vc-repo-toolbar-set-upstream").prop('checked')
                });
            });

        $('<div class="red-ui-shade red-ui-sidebar-vc-shade">').appendTo(sidebarContent);

        RED.sidebar.addTab({
            id: "version-control",
            label: RED._("sidebar.project.versionControl.history"),
            name: RED._("sidebar.project.versionControl.projectHistory"),
            content: sidebarContent,
            enableOnEdit: false,
            pinned: true,
            iconClass: "fa fa-code-fork",
            action: "core:show-version-control-tab",
            onchange: function() {
                setTimeout(function() {
                    sections.resize();
                },10);
            }
        });

    }

    function humanizeSinceDate(date) {
        var delta = (Date.now()/1000) - date;

        var daysDelta = Math.floor(delta / (60*60*24));
        if (daysDelta > 30) {
            return (new Date(date*1000)).toLocaleDateString();
        } else if (daysDelta > 0) {
            return RED._("sidebar.project.versionControl.daysAgo", {count:daysDelta})
        }
        var hoursDelta = Math.floor(delta / (60*60));
        if (hoursDelta > 0) {
            return RED._("sidebar.project.versionControl.hoursAgo", {count:hoursDelta})
        }
        var minutesDelta = Math.floor(delta / 60);
        if (minutesDelta > 0) {
            return RED._("sidebar.project.versionControl.minsAgo", {count:minutesDelta})
        }
        return RED._("sidebar.project.versionControl.secondsAgo");
    }

    function updateBulk(files,unstaged) {
        var activeProject = RED.projects.getActiveProject();
        if (unstaged) {
            bulkChangeSpinner = utils.addSpinnerOverlay(unstagedChangesList.parent());
        } else {
            bulkChangeSpinner = utils.addSpinnerOverlay(stagedChangesList.parent());
        }
        bulkChangeSpinner.addClass('red-ui-component-spinner-sidebar');
        var body = unstaged?{files:files}:undefined;
        utils.sendRequest({
            url: "projects/"+activeProject.name+"/stage",
            type: unstaged?"POST":"DELETE",
            responses: {
                0: function(error) {
                    console.log(error);
                    // done(error,null);
                },
                200: function(data) {
                    refreshFiles(data);
                },
                400: {
                    'unexpected_error': function(error) {
                        console.log(error);
                        // done(error,null);
                    }
                },
            }
        },body);
    }

    var refreshInProgress = false;

    function getCommits(url,targetList,spinnerTarget,limit,before) {
        var spinner = utils.addSpinnerOverlay(spinnerTarget);
        var fullUrl = url+"?limit="+(limit||20);
        if (before) {
            fullUrl+="&before="+before;
        }
        utils.sendRequest({
            url: fullUrl,
            type: "GET",
            responses: {
                0: function(error) {
                    console.log(error);
                    // done(error,null);
                },
                200: function(result) {
                    var lastSha;
                    result.commits.forEach(function(c) {
                        targetList.editableList('addItem',c);
                        lastSha = c.sha;
                    })
                    if (targetList.loadMoreItem) {
                        targetList.editableList('removeItem',targetList.loadMoreItem);
                        delete targetList.loadMoreItem;
                    }
                    var totalKnown = targetList.editableList('length');
                    if (totalKnown < result.total) {
                        targetList.loadMoreItem = {
                            totalKnown: totalKnown,
                            total: result.total,
                            url: url,
                            before: lastSha+"~1",
                            limit: limit,
                        };
                        targetList.editableList('addItem',targetList.loadMoreItem);
                    }
                    spinner.remove();
                },
                400: {
                    'unexpected_error': function(error) {
                        console.log(error);
                        // done(error,null);
                    }
                }
            }
        });
    }
    function refreshLocalCommits() {
        localCommitList.editableList('empty');
        var activeProject = RED.projects.getActiveProject();
        if (activeProject) {
            getCommits("projects/"+activeProject.name+"/commits",localCommitList,localCommitList.parent());
        }
    }
    // function refreshRemoteCommits() {
    //     remoteCommitList.editableList('empty');
    //     var spinner = utils.addSpinnerOverlay(remoteCommitList);
    //     var activeProject = RED.projects.getActiveProject();
    //     if (activeProject) {
    //         getCommits("projects/"+activeProject.name+"/commits/origin",remoteCommitList,remoteCommitList.parent());
    //     }
    // }

    function refreshFiles(result) {
        var files = result.files;
        if (bulkChangeSpinner) {
            bulkChangeSpinner.remove();
            bulkChangeSpinner = null;
        }
        isMerging = !!result.merging;
        if (isMerging) {
            sidebarContent.addClass("red-ui-sidebar-vc-merging");
            unmergedContent.show();
        } else {
            sidebarContent.removeClass("red-ui-sidebar-vc-merging");
            unmergedContent.hide();
        }
        unstagedChangesList.editableList('removeItem',emptyStagedItem);
        stagedChangesList.editableList('removeItem',emptyStagedItem);
        unmergedChangesList.editableList('removeItem',emptyMergedItem);

        var fileNames = Object.keys(files).filter(function(f) { return files[f].type === 'f'})
        fileNames.sort();
        var updateIndex = Date.now()+Math.floor(Math.random()*100);
        fileNames.forEach(function(fn) {
            var entry = files[fn];
            var addEntry = false;
            if (entry.status) {
                entry.file = fn;
                entry.indexStatus = entry.status[0];
                entry.treeStatus = entry.status[1];
                if ((entry.indexStatus === 'A' && /[AU]/.test(entry.treeStatus)) ||
                    (entry.indexStatus === 'U' && /[DAU]/.test(entry.treeStatus)) ||
                    (entry.indexStatus === 'D' && /[DU]/.test(entry.treeStatus))) {
                        entry.unmerged = true;
                }
                if (allChanges[fn]) {
                    if (allChanges[fn].unmerged && !entry.unmerged) {
                        unmergedChangesList.editableList('removeItem', allChanges[fn])
                        addEntry = true;
                    } else if (!allChanges[fn].unmerged && entry.unmerged) {
                        unstagedChangesList.editableList('removeItem', allChanges[fn])
                        stagedChangesList.editableList('removeItem', allChanges[fn])
                    }
                    // Known file
                    if (allChanges[fn].status !== entry.status) {
                        // Status changed.
                        if (allChanges[fn].treeStatus !== ' ') {
                            // Already in the unstaged list
                            if (entry.treeStatus === ' ') {
                                unstagedChangesList.editableList('removeItem', allChanges[fn])
                            } else if (entry.treeStatus !== allChanges[fn].treeStatus) {
                                allChanges[fn].updateUnstaged(entry,entry.treeStatus);
                            }
                        } else {
                            addEntry = true;
                        }
                        if (allChanges[fn].indexStatus !== ' ' && allChanges[fn].indexStatus !== '?') {
                            // Already in the staged list
                            if (entry.indexStatus === ' '||entry.indexStatus === '?') {
                                stagedChangesList.editableList('removeItem', allChanges[fn])
                            } else if (entry.indexStatus !== allChanges[fn].indexStatus) {
                                allChanges[fn].updateStaged(entry,entry.indexStatus);
                            }
                        } else {
                            addEntry = true;
                        }
                    }
                    allChanges[fn].status = entry.status;
                    allChanges[fn].indexStatus = entry.indexStatus;
                    allChanges[fn].treeStatus = entry.treeStatus;
                    allChanges[fn].oldName = entry.oldName;
                    allChanges[fn].unmerged = entry.unmerged;

                } else {
                    addEntry = true;
                    allChanges[fn] = entry;
                }
                allChanges[fn].updateIndex = updateIndex;
                if (addEntry) {
                    if (entry.unmerged) {
                        unmergedChangesList.editableList('addItem', allChanges[fn]);
                    } else {
                        if (entry.treeStatus !== ' ') {
                            unstagedChangesList.editableList('addItem', allChanges[fn])
                        }
                        if (entry.indexStatus !== ' ' && entry.indexStatus !== '?') {
                            stagedChangesList.editableList('addItem', allChanges[fn])
                        }
                    }
                }
            }
        });
        Object.keys(allChanges).forEach(function(fn) {
            if (allChanges[fn].updateIndex !== updateIndex) {
                unstagedChangesList.editableList('removeItem', allChanges[fn]);
                stagedChangesList.editableList('removeItem', allChanges[fn]);
                delete allChanges[fn];
            }
        });

        var stagedCount = stagedChangesList.editableList('length');
        var unstagedCount = unstagedChangesList.editableList('length');
        var unmergedCount = unmergedChangesList.editableList('length');

        commitButton.prop('disabled',(isMerging && unmergedCount > 0)||(!isMerging && stagedCount === 0));
        stageAllButton.prop('disabled',unstagedCount === 0);
        unstageAllButton.prop('disabled',stagedCount === 0);

        if (stagedCount === 0) {
            stagedChangesList.editableList('addItem',emptyStagedItem);
        }
        if (unstagedCount === 0) {
            unstagedChangesList.editableList('addItem',emptyStagedItem);
        }
        if (unmergedCount === 0) {
            unmergedChangesList.editableList('addItem',emptyMergedItem);
        }
    }

    function refresh(full, includeRemote) {
        if (refreshInProgress) {
            return;
        }
        if (full) {
            allChanges = {};
            unstagedChangesList.editableList('empty');
            stagedChangesList.editableList('empty');
            unmergedChangesList.editableList('empty');
        }
        if (!RED.user.hasPermission("projects.write")) {
            return;
        }


        refreshInProgress = true;
        refreshLocalCommits();

        var activeProject = RED.projects.getActiveProject();
        if (activeProject) {
            var url = "projects/"+activeProject.name+"/status";
            if (includeRemote) {
                url += "?remote=true"
            }
            $.getJSON(url,function(result) {
                refreshFiles(result);

                $('#red-ui-sidebar-vc-local-branch').text(result.branches.local);
                $('#red-ui-sidebar-vc-remote-branch').text(result.branches.remote||RED._("sidebar.project.versionControl.none"));

                var commitsAhead = result.commits.ahead || 0;
                var commitsBehind = result.commits.behind || 0;

                if (activeProject.git.hasOwnProperty('remotes')) {
                    if (result.branches.hasOwnProperty("remoteError") && result.branches.remoteError.code !== 'git_remote_gone') {
                        $("#red-ui-sidebar-vc-repo-status-auth-issue").show();
                        $("#red-ui-sidebar-vc-repo-status-stats").hide();
                        $('#red-ui-sidebar-vc-repo-branch').prop('disabled',true);
                        $("#red-ui-sidebar-vc-repo-pull").prop('disabled',true);
                        $("#red-ui-sidebar-vc-repo-push").prop('disabled',true);
                        $('#red-ui-sidebar-vc-repo-toolbar-message').hide();
                        $('#red-ui-sidebar-vc-repo-toolbar-error-message').show();
                    } else {
                        $('#red-ui-sidebar-vc-repo-toolbar-message').show();
                        $('#red-ui-sidebar-vc-repo-toolbar-error-message').hide();

                        $("#red-ui-sidebar-vc-repo-status-auth-issue").hide();
                        $("#red-ui-sidebar-vc-repo-status-stats").show();

                        $('#red-ui-sidebar-vc-repo-branch').prop('disabled',false);

                        $("#red-ui-sidebar-vc-repo-status-button").show();
                        if (result.branches.hasOwnProperty('remote')) {
                            updateRemoteStatus(commitsAhead, commitsBehind);
                        } else {
                            $('#red-ui-sidebar-vc-commits-ahead').text("");
                            $('#red-ui-sidebar-vc-commits-behind').text("");

                            $('#red-ui-sidebar-vc-repo-toolbar-message').text(RED._("sidebar.project.versionControl.notTracking"));
                            $("#red-ui-sidebar-vc-repo-pull").prop('disabled',true);
                            $("#red-ui-sidebar-vc-repo-push").prop('disabled',true);
                        }
                    }
                } else {
                    $("#red-ui-sidebar-vc-repo-status-button").hide();
                }
                refreshInProgress = false;
                $('.red-ui-sidebar-vc-shade').hide();
            }).fail(function() {
                refreshInProgress = false;
            });
        } else {
            $('.red-ui-sidebar-vc-shade').show();
            unstagedChangesList.editableList('empty');
            stagedChangesList.editableList('empty');
            unmergedChangesList.editableList('empty');
        }
    }


    function updateRemoteStatus(commitsAhead, commitsBehind) {
        $('#red-ui-sidebar-vc-commits-ahead').text(commitsAhead);
        $('#red-ui-sidebar-vc-commits-behind').text(commitsBehind);
        if (isMerging) {
            $('#red-ui-sidebar-vc-repo-toolbar-message').text(RED._("sidebar.project.versionControl.statusUnmergedChanged"));
            $("#red-ui-sidebar-vc-repo-pull").prop('disabled',true);
            $("#red-ui-sidebar-vc-repo-push").prop('disabled',true);
        } else if (commitsAhead > 0 && commitsBehind === 0) {
            $('#red-ui-sidebar-vc-repo-toolbar-message').text(RED._("sidebar.project.versionControl.commitsAhead", {count:commitsAhead}));
            $("#red-ui-sidebar-vc-repo-pull").prop('disabled',true);
            $("#red-ui-sidebar-vc-repo-push").prop('disabled',false);
        } else if (commitsAhead === 0 && commitsBehind > 0) {
            $('#red-ui-sidebar-vc-repo-toolbar-message').text(RED._("sidebar.project.versionControl.commitsBehind",{ count: commitsBehind }));
            $("#red-ui-sidebar-vc-repo-pull").prop('disabled',false);
            $("#red-ui-sidebar-vc-repo-push").prop('disabled',true);
        } else if (commitsAhead > 0 && commitsBehind > 0) {
            $('#red-ui-sidebar-vc-repo-toolbar-message').text(
                RED._("sidebar.project.versionControl.commitsAheadAndBehind1",{ count:commitsBehind })+
                RED._("sidebar.project.versionControl.commitsAheadAndBehind2",{ count:commitsAhead })+
                RED._("sidebar.project.versionControl.commitsAheadAndBehind3",{ count:commitsBehind }));
            $("#red-ui-sidebar-vc-repo-pull").prop('disabled',false);
            $("#red-ui-sidebar-vc-repo-push").prop('disabled',true);
        } else if (commitsAhead === 0 && commitsBehind === 0) {
            $('#red-ui-sidebar-vc-repo-toolbar-message').text(RED._("sidebar.project.versionControl.repositoryUpToDate"));
            $("#red-ui-sidebar-vc-repo-pull").prop('disabled',true);
            $("#red-ui-sidebar-vc-repo-push").prop('disabled',true);
        }
    }
    function show() {
        refresh();
        RED.sidebar.show("version-control");
    }
    function showLocalChanges() {
        RED.sidebar.show("version-control");
        localChanges.expand();
    }
    return {
        init: init,
        show: show,
        refresh: refresh,
        showLocalChanges: showLocalChanges
    }
})();
;/**
 * 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.
 **/

RED.touch = RED.touch||{};
RED.touch.radialMenu = (function() {


    var touchMenu = null;
    var isActive = false;
    var isOutside = false;
    var activeOption = null;


    function createRadial(obj,pos,options) {
        isActive = true;
        try {
            touchMenu = d3.select("body").append("div").classed("red-ui-editor-radial-menu",true)
                .on('touchstart',function() {
                    hide();
                    d3.event.preventDefault();
                });

            var menu = touchMenu.append("div")
                .style({
                    top: (pos[1]-80)+"px",
                    left:(pos[0]-80)+"px",
                });

            var menuOpts = [];
            var createMenuOpt = function(x,y,opt) {
                opt.el = menu.append("div").classed("red-ui-editor-radial-menu-opt",true)
                    .style({
                        top: (y+80-25)+"px",
                        left:(x+80-25)+"px"
                    })
                    .classed("red-ui-editor-radial-menu-opt-disabled",!!opt.disabled)

                opt.el.html(opt.name);

                opt.x = x;
                opt.y = y;
                menuOpts.push(opt);

                opt.el.on('touchstart',function() {
                    opt.el.classed("red-ui-editor-radial-menu-opt-active",true)
                    d3.event.preventDefault();
                    d3.event.stopPropagation();
                });
                opt.el.on('touchend',function() {
                    hide();
                    opt.onselect();
                    d3.event.preventDefault();
                    d3.event.stopPropagation();
                });
            }

            var n = options.length;
            var dang = Math.max(Math.PI/(n-1),Math.PI/4);
            var ang = Math.PI;
            for (var i=0;i<n;i++) {
                var x = Math.floor(Math.cos(ang)*80);
                var y = Math.floor(Math.sin(ang)*80);
                if (options[i].name) {
                    createMenuOpt(x,y,options[i]);
                }
                ang += dang;
            }


            var hide = function() {
                isActive = false;
                activeOption = null;
                touchMenu.remove();
                touchMenu = null;
            }

            obj.on('touchend.radial',function() {
                    obj.on('touchend.radial',null);
                    obj.on('touchmenu.radial',null);

                    if (activeOption) {
                        try {
                            activeOption.onselect();
                        } catch(err) {
                            RED._debug(err);
                        }
                        hide();
                    } else if (isOutside) {
                        hide();
                    }
            });

            obj.on('touchmove.radial',function() {
            try {
                var touch0 = d3.event.touches.item(0);
                var p = [touch0.pageX - pos[0],touch0.pageY-pos[1]];
                for (var i=0;i<menuOpts.length;i++) {
                    var opt = menuOpts[i];
                    if (!opt.disabled) {
                        if (p[0]>opt.x-30 && p[0]<opt.x+30 && p[1]>opt.y-30 && p[1]<opt.y+30) {
                            if (opt !== activeOption) {
                                opt.el.classed("selected",true);
                                activeOption = opt;
                            }
                        } else {
                            if (opt === activeOption) {
                                activeOption = null;
                            }
                            opt.el.classed("selected",false);
                        }
                    }
                }
                if (!activeOption) {
                    var d = Math.abs((p[0]*p[0])+(p[1]*p[1]));
                    isOutside = (d > 80*80);
                }

            } catch(err) {
                RED._debug(err);
            }


            });

        } catch(err) {
            RED._debug(err);
        }
    }


    return {
        show: createRadial,
        active: function() {
            return isActive;
        }
    }

})();
;RED.tourGuide = (function() {
    var activeListeners = [];
    var shade;
    var focus;
    var popover;
    var stepContent;
    var targetElement;
    var fullscreen;

    var tourCache = {};

    function run(tourPath, done) {
        done = done || function(err) {
            if (err) {
                console.error(err);
            }
        };
        loadTour(tourPath, function(err, tour) {
            if (err) {
                console.warn("Error loading tour:",err);
                return;
            }
            runTour(tour, done);
        })

    }

    function loadTour(tourPath, done) {
        if (tourCache[tourPath]) {
            done(null, tourCache[tourPath]);
        } else {
            /* jshint ignore:start */
            // jshint<2.13 doesn't support dynamic imports. Once grunt-contrib-jshint
            // has been updated with the new jshint, we can stop ignoring this block
            import(tourPath).then(function(module) {
                tourCache[tourPath] = module.default;
                done(null, tourCache[tourPath]);
            }).catch(function(err) {
                done(err);
            })
            /* jshint ignore:end */
        }
    }

    function repositionFocus() {
        if (targetElement) {
            if (!fullscreen) {
                var pos = targetElement[0].getBoundingClientRect();
                var dimension = Math.max(50, Math.max(pos.width,pos.height)*1.5);
                focus.css({
                    left: (pos.left+pos.width/2)+"px",
                    top: (pos.top+pos.height/2)+"px",
                    width: (2*dimension)+"px",
                    height: (2*dimension)+"px"
                })
                var flush = focus[0].offsetHeight; // Flush CSS changes
                focus.addClass("transition");
                focus.css({
                    width: dimension+"px",
                    height: dimension+"px"
                })
            } else {
                focus.css({
                    left: ($(window).width()/2)+"px",
                    top: ($(window).height()/2)+"px",
                    width: "0px",
                    height: "0px"
                })
            }
            if (popover) {
                popover.move({
                    target: targetElement,
                })
            }
        }
    }
    function runTour(tour, done) {

        shade = $('<div class="red-ui-tourGuide-shade"></div>').appendTo(document.body);
        focus = $('<div class="red-ui-tourGuide-shade-focus"></div>').appendTo(shade);

        // var resizeTimer;
        //
        $(window).on("resize.red-ui-tourGuide", function() {
            repositionFocus();
        })



        var i = 0;
        var state = {
            index: 0,
            count: tour.steps.length
        };

        function endTour(err) {
            $(window).off("resize.red-ui-tourGuide");
            $(document).off('keydown.red-ui-tourGuide');
            if (popover) {
                popover.close();
            }
            stepContent = null;
            popover = null;
            shade.remove();
            shade = null;
            done(err);
        }
        function runStep(carryOn) {
            if (carryOn === false) {
                endTour(false);
                return;
            }
            if (i === tour.steps.length) {
                endTour();
                return
            }
            state.index = i;
            // console.log("TOUR STEP",i+1,"OF",tour.steps.length)
            try {
                runTourStep(tour.steps[i++], state, runStep)
            } catch(err) {
                endTour(err);
                return;
            }
        }
        runStep();
    }

    function clearListeners() {
        activeListeners.forEach(function(listener) {
            if (listener.type === "dom-event") {
                listener.target[0].removeEventListener(listener.event,listener.listener,listener.opts);
            } else if (listener.type === "nr-event") {
                RED.events.off(listener.event, listener.listener)
            }
        })
        activeListeners = [];
    }

    function prepareStep(step, state, done) {
        if (step.prepare) {
            if (step.prepare.length === 0) {
                step.prepare.call(state);
            } else {
                if (popover) {
                    popover.element.hide();
                    if (!fullscreen) {
                        fullscreen = true;
                        repositionFocus()
                    }
                }
                step.prepare.call(state, function() {
                    if (popover) {
                        popover.element.show();
                    }
                    done();
                })
                return;
            }
        }
        done();
    }
    function completeStep(step, state, done) {
        function finish() {
            clearListeners();
            setTimeout(function() {
                done();
            },0)
        }
        if (step.complete) {
            if (step.complete.length === 0) {
                step.complete.call(state);
            } else {
                if (popover) {
                    popover.element.hide();
                    if (!fullscreen) {
                        fullscreen = true;
                        repositionFocus()
                    }
                }
                step.complete.call(state, function() {
                    if (popover) {
                        popover.element.show();
                    }
                    finish();
                })
                return;
            }
        }
        finish();

    }
    function getLocaleText(property) {
        if (typeof property === 'string') {
            return property;
        }
        var currentLang = RED.i18n.lang() || 'en-US';
        var availableLangs = Object.keys(property);
        return property[currentLang]||property['en-US']||property[availableLangs[0]]

    }
    function runTourStep(step, state, done) {
        shade.fadeIn();
        prepareStep(step, state, function() {
            var zIndex;
            var direction = step.direction || "bottom";
            fullscreen = false;

            if (typeof step.element === "string") {
                targetElement = $(step.element)
            } else if (typeof step.element === "function") {
                targetElement = step.element.call(state);
            } else if (!step.element) {
                targetElement = $(".red-ui-editor")
                fullscreen = true;
                direction = "inset";
            } else {
                targetElement = step.element;
            }

            if (targetElement.length === 0) {
                targetElement = null;
                shade.hide();
                throw new Error("Element not found")
            }
            if ($(window).width() < 400) {
                targetElement = $(".red-ui-editor");
                fullscreen = true;
                direction = "inset";
            }

            zIndex = targetElement.css("z-index");
            if (!fullscreen && (step.interactive || step.wait)) {
                targetElement.css("z-index",2002);
            }
            repositionFocus();

            if (!stepContent) {
                stepContent = $('<div style="position:relative"></div>');
            } else {
                stepContent.empty();
            }
            $('<button type="button" class="red-ui-button red-ui-button-small" style="float: right; margin-top: -4px; margin-right: -4px;"><i class="fa fa-times"></i></button>').appendTo(stepContent).click(function(evt) {
                evt.preventDefault();
                completeStep(step, state, function() {
                    done(false);
                });
            })

            var stepDescription = $('<div class="red-ui-tourGuide-popover-description"></div>').appendTo(stepContent);
            if (step.titleIcon) {
                $('<h2><i class="'+step.titleIcon+'"></i></h2>').appendTo(stepDescription);
            }
            if (step.title) {
                $('<h2>').text(getLocaleText(step.title)).appendTo(stepDescription);
            }
            $('<div>').css("text-align","left").html(getLocaleText(step.description)).appendTo(stepDescription);

            if (step.image) {
                $(`<img src="red/tours/${step.image}" />`).appendTo(stepDescription)
            }

            var stepToolbar = $('<div>',{class:"red-ui-tourGuide-toolbar"}).appendTo(stepContent);

            // var breadcrumbs = $('<div>',{class:"red-ui-tourGuide-breadcrumbs"}).appendTo(stepToolbar);
            // var bcStart = Math.max(0,state.index - 3);
            // var bcEnd = Math.min(state.count, bcStart + 7);
            // if (bcEnd === state.count) {
            //     bcStart = Math.max(0,bcEnd - 7);
            // }
            // for (var i = bcStart; i < bcEnd; i++) {
            //     var bullet = $('<i class="fa"></i>').addClass(i===state.index ? "fa-circle":"fa-circle-o").appendTo(breadcrumbs);
            //     if (i === bcStart) {
            //         if (i > 1) {
            //             bullet.css("font-size", "3px");
            //         } else if (i === 1) {
            //             bullet.css("font-size", "4px");
            //         }
            //     } else if (i === bcStart + 1) {
            //         if (i > 2) {
            //             bullet.css("font-size", "4px");
            //         }
            //     }
            //     if (i === bcEnd - 1) {
            //         if (i < state.count - 2) {
            //             bullet.css("font-size", "3px");
            //         } else if (i === state.count - 2) {
            //             bullet.css("font-size", "4px");
            //         }
            //     } else if (i === bcEnd - 2) {
            //         if (i < state.count - 3) {
            //             bullet.css("font-size", "4px");
            //         }
            //     }
            //     // if (i === bcEnd - 1) {
            //     //     if (i < state.count - 2) {
            //     //         bullet.css("font-size", "3px");
            //     //     } else if (i === state.count - 2) {
            //     //         bullet.css("font-size", "4px");
            //     //     }
            //     // }
            // }

            $('<small>').text((state.index+1)+"/"+state.count).appendTo(stepToolbar)
            var nextButton;
            if (fullscreen || !step.wait) {
                nextButton = $('<button type="button" class="red-ui-button" style="position: absolute; right:0;bottom:0;"></button>').appendTo(stepToolbar).one('click',function(evt) {
                    evt.preventDefault();
                    stepEventListener();
                });
                if (state.index === state.count - 1) {
                    $('<span></span>').text(RED._("common.label.close")).appendTo(nextButton);
                } else if (state.index === 0) {
                    $('<span>start</span>').text(RED._("tourGuide.start")).appendTo(nextButton);
                    $('<span style="margin-left: 6px"><i class="fa fa-chevron-right"></i></span>').appendTo(nextButton);
                } else if (state.index < state.count-1) {
                    $('<span></span>').text(RED._("tourGuide.next")).appendTo(nextButton);
                    $('<span style="margin-left: 6px"><i class="fa fa-chevron-right"></i></span>').appendTo(nextButton);
                }
            }

            var width = step.width;
            if (fullscreen) {
                width = 500;
            }
            var maxWidth = Math.min($(window).width()-10,Math.max(width || 0, 300));
            if (!popover) {
                popover = RED.popover.create({
                    target: targetElement,
                    width: width || "auto",
                    maxWidth: maxWidth+"px",
                    direction: direction,
                    class: "red-ui-tourGuide-popover"+(fullscreen?" ":""),
                    trigger: "manual",
                    content: stepContent
                }).open();
            }
            $(document).off('keydown.red-ui-tourGuide');
            $(document).on('keydown.red-ui-tourGuide', function(evt) {
                if (evt.key === "Escape" || evt.key === "Esc") {
                    evt.preventDefault();
                    evt.stopPropagation();
                    completeStep(step, state, function() {
                        done(false);
                    });
                }
            })
            popover.element.toggleClass("red-ui-tourGuide-popover-full",!!fullscreen);
            popover.move({
                target: targetElement,
                width: width || "auto",
                maxWidth: maxWidth+"px",
                direction: direction,
            })
            setTimeout(function() {
                var pos = popover.element.position()
                if (pos.left < 0) {
                    popover.element.css({left: 0});
                }
            },100);
            if (nextButton) {
                setTimeout(function() {
                    nextButton.focus();
                },100);
            }

            var isSVG = targetElement[0] instanceof SVGElement;
            if (step.fallback) {
                focus.one("mouseenter", function(evt) {
                    setTimeout(function() {
                        var pos = targetElement[0].getBoundingClientRect();
                        var dimension = Math.max(50, Math.max(pos.width,pos.height)*1.5);
                        focus.css({
                            width: (4*dimension)+"px",
                            height: (4*dimension)+"px"
                        })
                        shade.fadeOut();
                        popover.move({
                            target: $(".red-ui-editor"),
                            direction: step.fallback,
                            offset: 10,
                            transition: true
                        })
                        // popover.element.addClass('red-ui-tourGuide-popover-bounce');
                    },isSVG?0:500);
                })
            }

            var stepEventListener = function() {
                focus.removeClass("transition");
                targetElement.css("z-index",zIndex);
                completeStep(step, state, done);
            }

            if (step.wait) {
                if (step.wait.type === "dom-event") {
                    var eventTarget = targetElement;
                    if (step.wait.element) {
                        if (typeof step.wait.element === "string") {
                            eventTarget = $(step.wait.element);
                        } else if (typeof step.wait.element === "function") {
                            eventTarget = step.wait.element.call(state);
                        }
                    }
                    var listener = {
                        type: step.wait.type,
                        target: eventTarget,
                        event: step.wait.event,
                        listener: function() {
                            stepEventListener();
                        },
                        opts: { once: true }
                    }
                    activeListeners.push(listener)
                    eventTarget[0].addEventListener(listener.event,listener.listener,listener.opts)
                } else if (step.wait.type === "nr-event") {
                    var listener = {
                        type: step.wait.type,
                        event: step.wait.event,
                        listener: function() {
                            if (step.wait.filter) {
                                if (!step.wait.filter.apply(state,arguments)) {
                                    return;
                                }
                            }
                            stepEventListener();
                        }
                    }
                    activeListeners.push(listener);
                    RED.events.on(listener.event,listener.listener);
                }
            }
        })
    }

    function listTour() {
        return [
            {
                id: "4_0",
                label: "4.0",
                path: "./tours/welcome.js"
            },
            {
                id: "3_1",
                label: "3.1",
                path: "./tours/3.1/welcome.js"
            },
            {
                id: "3_0",
                label: "3.0",
                path: "./tours/3.0/welcome.js"
            },
            {
                id: "2_2",
                label: "2.2",
                path: "./tours/2.2/welcome.js"
            },
            {
                id: "2_1",
                label: "2.1",
                path: "./tours/2.1/welcome.js"
            }
        ];
    }

    return {
        load: loadTour,
        run: run,
        list: listTour,
        reset: function() {
            RED.settings.set("editor.tours.welcome",'');
        }
    }


})();
