!C99Shell v. 2.5 [PHP 8 Update] [24.05.2025]!

Software: Apache/2.4.41 (Ubuntu). PHP/8.0.30 

uname -a: Linux apirnd 5.4.0-204-generic #224-Ubuntu SMP Thu Dec 5 13:38:28 UTC 2024 x86_64 

uid=33(www-data) gid=33(www-data) groups=33(www-data) 

Safe-mode: OFF (not secure)

/var/www/html/node-red/packages/node_modules/@node-red/editor-client/public/red/   drwxr-xr-x
Free 12.99 GB of 57.97 GB (22.41%)
Home    Back    Forward    UPDIR    Refresh    Search    Buffer    Encoder    Tools    Proc.    FTP brute    Sec.    SQL    PHP-code    Update    Self remove    Logout    


Viewing file:     red.js (2.1 MB)      -rw-r--r--
Select action/file-type:
(+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
(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) {
                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\/.+$/.test(currentHash)) {
                            RED.workspaces.show(currentHash.substring(6),true);
                        }
                        if (RED.workspaces.active() === 0 && RED.workspaces.count() > 0) {
                            RED.workspaces.show(RED.nodes.getWorkspaceOrder()[0])
                        }
                    } 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 === "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()
                    });
                });
                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") {
                    RED.events.emit("runtime-state",msg);
                    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._("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();
                                    }
                                }
                            ]
                        }
                    }
                }
                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/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) {
                        typeList = "<ul><li>"+m.types.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
                        RED.notify(RED._("palette.event.nodeRemoved", {count:m.types.length})+typeList,"success");
                    }
                }
                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");

        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"}
        ]});

        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-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"},
            null,
            {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"}
        ]});

        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.rename"),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","http://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.diff.init();


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

        RED.keyboard.init(buildMainMenu);

        RED.nodes.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"></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]) {
             for (var i=0;i<handlers[evt].length;i++) {
                 try {
                     handlers[evt][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 === "auth-tokens") {
            localStorage.setItem(key, 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 === "auth-tokens") {
            return JSON.parse(localStorage.getItem(key));
        } 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 === "auth-tokens") {
            localStorage.removeItem(key);
        } 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);
        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") {
                    i = 0;
                    for (;i<data.prompts.length;i++) {
                        var field = data.prompts[i];
                        var sessionMessage = /[?&]session_message=(.*?)(?:$|&)/.exec(window.location.search);
                        if (sessionMessage) {
                            RED.sessionMessages = RED.sessionMessages || [];
                            RED.sessionMessages.push(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) {
                            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();
                    }


                }
                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() {
        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();
        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();
                }
            });
        }

    }

    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");
                if (RED.settings.user.image) {
                    $('<span class="user-profile"></span>').css({
                        backgroundImage: "url("+RED.settings.user.image+")",
                    }).appendTo(userMenu.find("a"));
                } else {
                    $('<i class="fa fa-user"></i>').appendTo(userMenu.find("a"));
                }

                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;
    }


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

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

    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}));
                }
            }
        }

        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];
            }
        }
    }

    return {
        connect: connectWS,
        subscribe: subscribe,
        unsubscribe:unsubscribe
    }
})();
;/**
 * 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
}
;RED.plugins = (function() {
    var plugins = {};
    var pluginsByType = {};

    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] || [];
    }
    return {
        registerPlugin: registerPlugin,
        getPlugin: getPlugin,
        getPluginsByType: getPluginsByType
    }
})();
;/**
 * 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 = (function() {

    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 initialLoad;

    var dirty = false;

    function setDirty(d) {
        dirty = d;
        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},
                info: {value: ""},
                env: {value: []}
            }
        };


        var exports = {
            setModulePendingUpdated: function(module,version) {
                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);
            },
            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];
                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() {
        var nodes = {};
        var tabMap = {};
        var api = {
            addTab: function(id) {
                tabMap[id] = [];
            },
            hasTab: function(z) {
                return tabMap.hasOwnProperty(z)
            },
            removeTab: function(id) {
                delete tabMap[id];
            },
            addNode: function(n) {
                nodes[n.id] = n;
                if (tabMap.hasOwnProperty(n.z)) {
                    tabMap[n.z].push(n);
                } 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);
                    }
                }
            },
            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)
            },
            moveNodesForwards: function(nodes) {
                var result = [];
                if (!Array.isArray(nodes)) {
                    nodes = [nodes]
                }
                // Can only do this for nodes on the same tab.
                // Use nodes[0] to get the z
                var tabNodes = tabMap[nodes[0].z];
                var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
                var moved = new Set();
                for (var i = tabNodes.length-1; i >= 0; i--) {
                    if (toMove.size === 0) {
                        break;
                    }
                    var n = tabNodes[i];
                    if (toMove.has(n)) {
                        // This is a node to move.
                        if (i < tabNodes.length-1 && !moved.has(tabNodes[i+1])) {
                            // Remove from current position
                            tabNodes.splice(i,1);
                            // Add it back one position higher
                            tabNodes.splice(i+1,0,n);
                            n._reordered = true;
                            result.push(n);
                        }
                        toMove.delete(n);
                        moved.add(n);
                    }
                }
                if (result.length > 0) {
                    RED.events.emit('nodes:reorder',{
                        z: nodes[0].z,
                        nodes: result
                    });
                }
                return result;
            },
            moveNodesBackwards: function(nodes) {
                var result = [];
                if (!Array.isArray(nodes)) {
                    nodes = [nodes]
                }
                // Can only do this for nodes on the same tab.
                // Use nodes[0] to get the z
                var tabNodes = tabMap[nodes[0].z];
                var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
                var moved = new Set();
                for (var i = 0; i < tabNodes.length; i++) {
                    if (toMove.size === 0) {
                        break;
                    }
                    var n = tabNodes[i];
                    if (toMove.has(n)) {
                        // This is a node to move.
                        if (i > 0 && !moved.has(tabNodes[i-1])) {
                            // Remove from current position
                            tabNodes.splice(i,1);
                            // Add it back one position lower
                            tabNodes.splice(i-1,0,n);
                            n._reordered = true;
                            result.push(n);
                        }
                        toMove.delete(n);
                        moved.add(n);
                    }
                }
                if (result.length > 0) {
                    RED.events.emit('nodes:reorder',{
                        z: nodes[0].z,
                        nodes: result
                    });
                }
                return result;
            },
            moveNodesToFront: function(nodes) {
                var result = [];
                if (!Array.isArray(nodes)) {
                    nodes = [nodes]
                }
                // Can only do this for nodes on the same tab.
                // Use nodes[0] to get the z
                var tabNodes = tabMap[nodes[0].z];
                var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
                var target = tabNodes.length-1;
                for (var i = tabNodes.length-1; i >= 0; i--) {
                    if (toMove.size === 0) {
                        break;
                    }
                    var n = tabNodes[i];
                    if (toMove.has(n)) {
                        // This is a node to move.
                        if (i < target) {
                            // Remove from current position
                            tabNodes.splice(i,1);
                            tabNodes.splice(target,0,n);
                            n._reordered = true;
                            result.push(n);
                        }
                        target--;
                        toMove.delete(n);
                    }
                }
                if (result.length > 0) {
                    RED.events.emit('nodes:reorder',{
                        z: nodes[0].z,
                        nodes: result
                    });
                }
                return result;
            },
            moveNodesToBack: function(nodes) {
                var result = [];
                if (!Array.isArray(nodes)) {
                    nodes = [nodes]
                }
                // Can only do this for nodes on the same tab.
                // Use nodes[0] to get the z
                var tabNodes = tabMap[nodes[0].z];
                var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
                var target = 0;
                for (var i = 0; i < tabNodes.length; i++) {
                    if (toMove.size === 0) {
                        break;
                    }
                    var n = tabNodes[i];
                    if (toMove.has(n)) {
                        // This is a node to move.
                        if (i > target) {
                            // Remove from current position
                            tabNodes.splice(i,1);
                            // Add it back one position lower
                            tabNodes.splice(target,0,n);
                            n._reordered = true;
                            result.push(n);
                        }
                        target++;
                        toMove.delete(n);
                    }
                }
                if (result.length > 0) {
                    RED.events.emit('nodes:reorder',{
                        z: nodes[0].z,
                        nodes: result
                    });
                }
                return result;
            },
            getNodes: function(z) {
                return tabMap[z];
            },
            clear: function() {
                nodes = {};
                tabMap = {};
            },
            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 tabMap[z].map(function(n) { return 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];
                })
            }
        }
        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
        }
    }


    function addNode(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(sf);
            }
            n["_"] = RED._;
        }
        if (n._def.category == "config") {
            configNodes[n.id] = n;
        } else {
            if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; }
            n.dirty = true;
            updateConfigNodeUsers(n);
            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(n);
            if (!nodeLinks[n.id]) {
                nodeLinks[n.id] = {in:[],out:[]};
            }
        }
        RED.events.emit('nodes:add',n);
    }
    function addLink(l) {
        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);
        }
        RED.events.emit("links:add",l);
    }

    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];
            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);
            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);
                                } else {
                                    var users = configNode.users;
                                    users.splice(users.indexOf(node),1);
                                    RED.events.emit('nodes:change',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 oneditremove - please report");
            node._def.onremove.call(n);
        }
        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;
        }
        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 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)
                }
            }
        }
        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 = [];
        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);
                    }
                }
            }

            for (i=0;i<removedNodes.length;i++) {
                var result = removeNode(removedNodes[i].id);
                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};
    }

    function addSubflow(sf, createNewIds) {
        if (createNewIds) {
            var subflowNames = Object.keys(subflows).map(function(sfid) {
                return subflows[sfid].name;
            });

            subflowNames.sort();
            var copyNumber = 1;
            var subflowName = sf.name;
            subflowNames.forEach(function(name) {
                if (subflowName == name) {
                    copyNumber++;
                    subflowName = sf.name+" ("+copyNumber+")";
                }
            });
            sf.name = subflowName;
        }
        subflows[sf.id] = sf;
        allNodes.addTab(sf.id);
        linkTabMap[sf.id] = [];

        RED.nodes.registerType("subflow:"+sf.id, {
            defaults:{
                name:{value:""},
                env:{value:[]}
            },
            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.instances = [];
        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 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)) {
                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) {
                    node.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") {
            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];
        }
        console.log(nodeSet);
        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;
                        }
                    });
                    var exportableSubflow = createExportableNodeSet(subflowSet, exportedIds, exportedSubflows, exportedConfigNodes);
                    nns = exportableSubflow.concat(nns);
                }
            }
            if (node.type !== "subflow") {
                var convertedNode = RED.nodes.convertNode(node);
                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);
                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 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: {},
            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.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]) {
                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
            var subflowNodes = [n].concat(zMap[n.id]);
            // 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) {
            removedNodes = removedNodes.concat(convertNode(getNode(id)));
            removeNode(id);
            importNodes([newConfigNodes[id]])
        });

        return {
            removedNodes: removedNodes
        }

    }

    /**
     * Options:
     *  - generateIds - whether to replace all node ids
     *  - addFlow - whether to import nodes to a new tab
     *  - importToCurrent
     *  - 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) {
        options = options || {
            generateIds: false,
            addFlow: false,
        }
        options.importMap = options.importMap || {};

        var createNewIds = options.generateIds;
        var 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 (!$.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];
                    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" &&
                !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_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 {
                    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[n.id] === "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();
        }

        // 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 {
                    if (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:{}
                    };
                    if (!n.z) {
                        delete configNode.z;
                    }
                    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.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;
                    new_nodes.push(configNode);
                }
            }
        }

        // 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:0,
                        info: n.info,
                        changed:false,
                        _config:{}
                    }
                    if (n.type !== "group") {
                        node.wires = n.wires||[];
                        node.inputLabels = n.inputLabels;
                        node.outputLabels = n.outputLabels;
                        node.icon = n.icon;
                    }
                    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 (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;
                        if (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.type = n.type;
                    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;
                    } 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 (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 (!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.inputs = n.inputs;
                                node._config.inputs = JSON.stringify(n.inputs);
                            } else {
                                node.inputs = node._def.inputs;
                            }
                            if (n.hasOwnProperty('outputs')) {
                                node.outputs = n.outputs;
                                node._config.outputs = JSON.stringify(n.outputs);
                            } else {
                                node.outputs = node._def.outputs;
                            }
                            if (node.hasOwnProperty('wires') && node.wires.length > node.outputs) {
                                if (!node._def.defaults.hasOwnProperty("outputs") || !isNaN(parseInt(n.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);
                                } else {
                                    // The node declares outputs in its defaults, but has not got a valid value
                                    // Defer to the length of the wires array
                                    node.outputs = node.wires.length;
                                }
                            }
                            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 === "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;i++) {
            n = new_nodes[i];
            if (n.wires) {
                for (var w1=0;w1<n.wires.length;w1++) {
                    var wires = (n.wires[w1] instanceof Array)?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
            }
            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) {
                                if (node._def.category === 'config') {
                                    if (node.users.indexOf(n) === -1) {
                                        node.users.push(n);
                                    }
                                }
                                return node.id;
                            }
                            return id;
                        })
                        n[d3] = Array.isArray(n[d3])?nodeList:nodeList[0];
                    }
                }
            }
            // If importing into a subflow, ensure an outbound-link doesn't
            // get added
            if (activeSubflow && /^link /.test(n.type) && n.links) {
                n.links = n.links.filter(function(id) {
                    var otherNode = RED.nodes.node(id);
                    return (otherNode && otherNode.z === activeWorkspace)
                });
            }
        }
        for (i=0;i<new_subflows.length;i++) {
            n = new_subflows[i];
            n.in.forEach(function(input) {
                input.wires.forEach(function(wire) {
                    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 {
                        link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output};
                    }
                    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 {
                        link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status};
                    }
                    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;
            }
            n.nodes = n.nodes.map(function(id) {
                return node_map[id];
            })
            // Just in case the group references a node that doesn't exist for some reason
            n.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
            });
            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++) {
            n = new_groups[i];
            addGroup(n);
        }

        // Now the nodes have been fully updated, add them.
        for (i=0;i<new_nodes.length;i++) {
            var node = new_nodes[i];
            addNode(node);
        }
        // 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);
        }

        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,
            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
    function updateConfigNodeUsers(n) {
        for (var d in n._def.defaults) {
            if (n._def.defaults.hasOwnProperty(d)) {
                var property = n._def.defaults[d];
                if (property.type) {
                    var type = registry.getNodeType(property.type);
                    if (type && type.category == "config") {
                        var configNode = configNodes[n[d]];
                        if (configNode) {
                            if (configNode.users.indexOf(n) === -1) {
                                configNode.users.push(n);
                                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 = {};

        var subflowIds = Object.keys(subflows);
        subflowIds.forEach(function(id) {
            RED.subflow.removeSubflow(id)
        });
        var workspaceIds = Object.keys(workspaces);
        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) {
        groupsByZ[group.z] = groupsByZ[group.z] || [];
        groupsByZ[group.z].push(group);
        groups[group.id] = group;
        RED.events.emit("groups:add",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);

        delete groups[group.id];
        RED.events.emit("groups:remove",group);
    }

    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;
    }

    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;
                    }
                });

                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);
                        }
                        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});
                    var newNodeMap = {};
                    result.nodes.forEach(function(n) {
                        newNodeMap[n.id] = 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);
                }
            });
        },
        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,

        moveNodesForwards: moveNodesForwards,
        moveNodesBackwards: moveNodesBackwards,
        moveNodesToFront: moveNodesToFront,
        moveNodesToBack: moveNodesToBack,
        getNodeOrder: getNodeOrder,
        setNodeOrder: setNodeOrder,

        moveNodeToTab: moveNodeToTab,

        addLink: addLink,
        removeLink: removeLink,

        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():[] },

        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;
                }
            }
        },

        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,
        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.
 **/
RED.history = (function() {
    var undoHistory = [];
    var redoHistory = [];

    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: {},
                        rev: RED.nodes.version()
                    };
                    RED.nodes.clear();
                    var imported = RED.nodes.import(ev.config);
                    imported.nodes.forEach(function(n) {
                        if (ev.changed[n.id]) {
                            n.changed = true;
                            inverseEv.changed[n.id] = true;
                        }
                    })

                    RED.nodes.version(ev.rev);
                } 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.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.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;
                } else 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);
                                }
                            });
                        }
                        ev.node[i] = ev.changes[i];
                    }
                }
                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);
                    $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!ev.node.disabled);
                }
                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 (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]);
                    }
                }
                ev.node.dirty = true;
                ev.node.changed = ev.changed;
            } 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.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(RED.nodes.node(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);
                    }
                }
            } 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];
        },
        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){return function(v) { return (blankAllowed&&(v===''||v===undefined)) || (v!=='' && !isNaN(v));}},
    regex: function(re){return function(v) { return re.test(v);}},
    typedInput: function(ptypeName,isConfig) { return function(v) {
        var ptype = $("#node-"+(isConfig?"config-":"")+"input-"+ptypeName).val() || this[ptypeName];
        if (ptype === 'json') {
            try {
                JSON.parse(v);
                return true;
            } catch(err) {
                return false;
            }
        } else if (ptype === 'msg' || ptype === 'flow' || ptype === 'global' ) {
            return RED.utils.validatePropertyExpression(v);
        } else if (ptype === 'num') {
            return /^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/.test(v);
        }
        return true;
    }}
};
;/**
 * 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>'
        }
    }

    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(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) {
        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 (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) {
        if (expandPaths && expandPaths.length > 0) {
            if (strippedKey === '' && minRange === undefined) {
                return true;
            }
            for (var i=0;i<expandPaths.length;i++) {
                var p = expandPaths[i];
                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');
        }
    }

    function buildMessageElement(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;
        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);
        }
        if (!key) {
            element.addClass("red-ui-debug-msg-top-level");
            if (sourceId) {
                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));
            }
            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);
            }

        } 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+"]"] = buildMessageElement(
                                data[i],
                                {
                                    key: ""+i,
                                    typeHint: type==='buffer'?'hex':false,
                                    hideKey: false,
                                    path: path+"["+i+"]",
                                    sourceId: sourceId,
                                    rootPath: rootPath,
                                    expandPaths: expandPaths,
                                    ontoggle: ontoggle,
                                    exposeApi: exposeApi,
                                    // tools: tools // Do not pass tools down as we
                                                    // keep them attached to the top-level header
                                }
                            ).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+"]"] = buildMessageElement(
                                            data[i],
                                            {
                                                key: ""+i,
                                                typeHint: type==='buffer'?'hex':false,
                                                hideKey: false,
                                                path: path+"["+i+"]",
                                                sourceId: sourceId,
                                                rootPath: rootPath,
                                                expandPaths: expandPaths,
                                                ontoggle: ontoggle,
                                                exposeApi: exposeApi,
                                                // tools: tools // Do not pass tools down as we
                                                                // keep them attached to the top-level header
                                            }
                                        ).appendTo(row);
                                    }
                                }
                            })(),
                            (function() { var path = path+"["+i+"]"; return function(state) {if (ontoggle) { ontoggle(path,state);}}})(),
                            checkExpanded(strippedKey,expandPaths,minRange,Math.min(fullLength-1,(minRange+9))));
                            $('<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));
            }
        } 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] = buildMessageElement(
                            data[keys[i]],
                            {
                                key: keys[i],
                                typeHint: false,
                                hideKey: false,
                                path: newPath,
                                sourceId: sourceId,
                                rootPath: rootPath,
                                expandPaths: expandPaths,
                                ontoggle: ontoggle,
                                exposeApi: exposeApi,
                                // tools: tools // Do not pass tools down as we
                                                // keep them attached to the top-level header
                            }
                        ).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));
            }
            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;
    }

    function validatePropertyExpression(str) {
        try {
            var parts = normalisePropertyExpression(str);
            return true;
        } catch(err) {
            return false;
        }
    }

    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 (def.category === 'config') {
            return RED.settings.apiRootUrl+"icons/node-red/cog.svg"
        } 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 {
            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);
    }

    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 === '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: buildMessageElement,
        getMessageProperty: getMessageProperty,
        setMessageProperty: setMessageProperty,
        normalisePropertyExpression: normalisePropertyExpression,
        validatePropertyExpression: validatePropertyExpression,
        separateIconPath: separateIconPath,
        getDefaultNodeIcon: getDefaultNodeIcon,
        getNodeIcon: getNodeIcon,
        getNodeLabel: getNodeLabel,
        getNodeColor: getNodeColor,
        clearNodeColorCache: clearNodeColorCache,
        addSpinnerOverlay: addSpinnerOverlay,
        decodeObject: decodeObject,
        parseContextKey: parseContextKey,
        createIconElement: createIconElement,
        sanitize: sanitize,
        renderMarkdown: renderMarkdown,
        createNodeIcon: createNodeIcon,
        getDarkerColor: getDarkerColor,
        parseModuleList: parseModuleList,
        checkModuleAllowed: checkModuleAllowed,
        getBrowserInfo: getBrowserInfo
    }
})();
;/**
 * 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","scroll");
                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 handle = (typeof this.options.sortable === 'string')?
                                this.options.sortable :
                                ".red-ui-editableList-item-handle";
                var sortOptions = {
                    axis: "y",
                    update: function( event, ui ) {
                        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(e, ui){
                        ui.placeholder.height(ui.item.height()-4);
                    }
                };
                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;
            }
        }
    });
})(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
 *   - 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 (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 = {};

    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.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 pull-left"></i>';
                linkContent += '<i class="fa fa-check-square 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> ';
                }
            }

            if (opt.sublabel) {
                linkContent += '<span class="red-ui-menu-label-container"><span class="red-ui-menu-label">'+opt.label+'</span>'+
                               '<span class="red-ui-menu-sublabel">'+opt.sublabel+'</span></span>'
            } else {
                linkContent += '<span class="red-ui-menu-label"><span>'+opt.label+'</span></span>'
            }

            linkContent += '</a>';

            var link = $(linkContent).appendTo(item);
            opt.link = link;
            if (typeof opt.onselect === 'string') {
                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(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 pull-left");
                var submenu = $('<ul id="'+opt.id+'-submenu" class="red-ui-menu-dropdown"></ul>').appendTo(item);

                for (var i=0;i<opt.options.length;i++) {
                    var li = createMenuItem(opt.options[i]);
                    if (li) {
                        li.appendTo(submenu);
                    }
                }
            }
            if (opt.disabled) {
                item.addClass("disabled");
            }
        }


        return item;

    }
    function createMenu(options) {
        var topMenu = $("<ul/>",{class:"red-ui-menu red-ui-menu-dropdown pull-right"});

        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;
        for (var i=0;i<options.options.length;i++) {
            var opt = options.options[i];
            if (opt !== null || !lastAddedSeparator) {
                var li = createMenuItem(opt);
                if (li) {
                    li.appendTo(topMenu);
                    lastAddedSeparator = (opt === null);
                }
            }
        }

        return topMenu;
    }

    function triggerAction(id, args) {
        var opt = menuItems[id];
        var callback = opt.onselect;
        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);
        }
    }

    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 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,
        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)
                }
            }
        }

        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) {
            var label = function() {
                var 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,
                delay: { show: 750, hide: 50 }
            });
            popover.setContent = function(newContent) {
                content = newContent;
            }
            popover.setAction = function(newAction) {
                action = newAction;
            }
            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 pos = target.offset();
                var targetWidth = target.width();
                var targetHeight = 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) {
                        //DOWN
                        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();
            })
        }



        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)]);
            }
            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)
            .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.one("transitionend", function(evt) {
                li.remove();
                if (tabs[id].pinned) {
                    pinnedTabsCount--;
                }
                if (options.onremove) {
                    options.onremove(tabs[id]);
                }
                delete tabs[id];
                updateTabWidths();
                if (collapsibleMenu) {
                    collapsibleMenu.remove();
                    collapsibleMenu = null;
                }
            })
            li.addClass("hide-tab");
            li.width(0);
        }

        function findPreviousVisibleTab(li) {
            if (!li) {
                li = ul.find("li.active").parent();
            }
            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").parent();
            }
            var next = ul.find("li.active").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) {
                    $('<img src="'+tab.icon+'" class="red-ui-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);
                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.hideFlow"));
                }
                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-changed fa fa-circle"></i>').appendTo(badges);
                    $('<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 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()
            },
            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;
    }

    var autoComplete = function(options) {
        return function(val) {
            var matches = [];
            options.forEach(opt => {
                let v = opt.value;
                var i = v.toLowerCase().indexOf(val.toLowerCase());
                if (i > -1) {
                    var pre = v.substring(0,i);
                    var matchedVal = v.substring(i,i+val.length);
                    var post = v.substring(i+val.length)

                    var el = $('<div/>',{style:"white-space:nowrap; overflow: hidden; flex-grow:1"});
                    $('<span/>').text(pre).appendTo(el);
                    $('<span/>',{style:"font-weight: bold"}).text(matchedVal).appendTo(el);
                    $('<span/>').text(post).appendTo(el);

                    var element = $('<div>',{style: "display: flex"});
                    el.appendTo(element);
                    if (opt.source) {
                        $('<div>').css({
                            "font-size": "0.8em"
                        }).text(opt.source.join(",")).appendTo(element);
                    }

                    matches.push({
                        value: v,
                        label: element,
                        i:i
                    })
                }
            })
            matches.sort(function(A,B){return A.i-B.i})
            return matches;
        }
    }

    // This is a hand-generated list of completions for the core nodes (based on the node help html).
    var msgCompletions = [
        { value: "payload" },
        { 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: "complete", source: ["join"] },
        { value: "contentType", source: ["mqtt"] },
        { value: "cookies", source: ["http in","http request"] },
        { value: "correlationData", source: ["mqtt"] },
        { value: "delay", source: ["delay","trigger"] },
        { value: "encoding", source: ["file"] },
        { value: "error", source: ["catch"] },
        { value: "filename", source: ["file","file in"] },
        { value: "flush", source: ["delay"] },
        { value: "followRedirects", source: ["http request"] },
        { value: "headers", source: ["http in"," http request"] },
        { value: "kill", source: ["exec"] },
        { value: "messageExpiryInterval", source: ["mqtt"] },
        { value: "method", source: ["http-request"] },
        { value: "options", source: ["xml"] },
        { value: "parts", source: ["split","join"] },
        { value: "pid", source: ["exec"] },
        { value: "qos", source: ["mqtt"] },
        { value: "rate", source: ["delay"] },
        { value: "rejectUnauthorized", source: ["http request"] },
        { value: "requestTimeout", source: ["http request"] },
        { value: "reset", source: ["delay","trigger","join","rbe"] },
        { value: "responseTopic", source: ["mqtt"] },
        { value: "restartTimeout", source: ["join"] },
        { value: "retain", source: ["mqtt"] },
        { value: "select", source: ["html"] },
        { value: "statusCode", source: ["http in"] },
        { value: "template", source: ["template"] },
        { value: "toFront", source: ["delay"] },
        { value: "topic", source: ["inject","mqtt","rbe"] },
        { value: "url", source: ["http request"] },
        { value: "userProperties", source: ["mqtt"] }
    ]
    var allOptions = {
        msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)},
        flow: {value:"flow",label:"flow.",hasValue:true,
            options:[],
            validate:RED.utils.validatePropertyExpression,
            parse: contextParse,
            export: contextExport,
            valueLabel: contextLabel
        },
        global: {value:"global",label:"global.",hasValue:true,
            options:[],
            validate:RED.utils.validatePropertyExpression,
            parse: contextParse,
            export: contextExport,
            valueLabel: contextLabel
        },
        str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"},
        num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate:/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/},
        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) { try{JSON.parse(v);return true;}catch(e){return false;}},
            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,
                    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",hasValue:false},
        jsonata: {
            value: "jsonata",
            label: "expression",
            icon: "red/images/typedInput/expr.svg",
            validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}},
            expand:function() {
                var that = this;
                RED.editor.editExpression({
                    value: this.value().replace(/\t/g,"\n"),
                    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(),
                    complete: function(v) {
                        that.value(v);
                    }
                })
            }
        },
        env: {
            value: "env",
            label: "env variable",
            icon: "red/images/typedInput/env.svg"
        },
        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();
                }
            }
        }
    };

    // 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) {
        if (!opt.multiple) {
            for (var i=0;i<opt.options.length;i++) {
                op = opt.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<opt.options.length;i++) {
                op = opt.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;

    $.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;
                var contextOptions = 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 (contextOptions.length < 2) {
                    allOptions.flow.options = [];
                    allOptions.global.options = [];
                } else {
                    allOptions.flow.options = contextOptions;
                    allOptions.global.options = contextOptions;
                }
            }
            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 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) {
                    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 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 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) {
                        $('<img>',{src:mapDeprecatedIcon(opt.icon),style:"margin-right: 4px; height: 18px;"}).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')])
                })
            }


            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(o.length+" selected");
                }
            }
        },
        _destroy: function() {
            if (this.optionMenu) {
                this.optionMenu.remove();
            }
            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);
                }
            } 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) {
                    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<opt.options.length;i++) {
                            var op = opt.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;
            } 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 (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) {
                            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) {
                            image = new Image();
                            image.name = opt.icon;
                            image.src = mapDeprecatedIcon(opt.icon);
                            $('<img>',{src:mapDeprecatedIcon(opt.icon),style:"margin-right: 4px;height: 18px;"}).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) {
                        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 = {};
                            opt.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 = opt.options[0];
                                        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||opt.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();
                                }
                            }
                            this.optionMenu = this._createMenu(opt.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) {
                                this.input.autoComplete({
                                    search: opt.autoComplete
                                })
                            }
                        }
                        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() {
            var result;
            var value = this.value();
            var type = this.type();
            if (this.typeMap[type] && this.typeMap[type].validate) {
                var val = this.typeMap[type].validate;
                if (typeof val === 'function') {
                    result = val(value);
                } else {
                    result = val.test(value);
                }
            } else {
                result = true;
            }
            if (result) {
                this.uiSelect.removeClass('input-error');
            } else {
                this.uiSelect.addClass('input-error');
            }
            return result;
        },
        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
 *
 * 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() {
            var that = this;
            this.completionMenuShown = false;
            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: 300,
                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) {
            var that = this;
            if (val.trim() === "") {
                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) {
                var 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();
            }
        }
    });
})(jQuery);
;RED.actions = (function() {
    var actions = {

    }

    function addAction(name,handler) {
        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;
    }
    function removeAction(name) {
        delete actions[name];
    }
    function getAction(name) {
        return actions[name];
    }
    function invokeAction() {
        var args = Array.prototype.slice.call(arguments);
        var name = args.shift();
        if (actions.hasOwnProperty(name)) {
            actions[name].apply(null, args);
        }
    }
    function listActions() {
        var result = [];
        Object.keys(actions).forEach(function(action) {
            var shortcut = RED.keyboard.getShortcut(action);
            var isUser = false;
            if (shortcut) {
                isUser = shortcut.user;
            } else {
                isUser = !!RED.keyboard.getUserShortcut(action);
            }
            result.push({
                id:action,
                scope:shortcut?shortcut.scope:undefined,
                key:shortcut?shortcut.key:undefined,
                user:isUser
            })
        })
        return result;
    }
    return {
        add: addAction,
        remove: removeAction,
        get: getAction,
        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;

    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
     *   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";

        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>'+RED._("deploy.deploy")+'</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></a>'+
              '</span></li>').prependTo(".red-ui-header-toolbar");
              RED.menu.init({id:"red-ui-header-button-deploy-options",
                  options: [
                      {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,
                      {id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"},

                  ]
              });
        } else if (type == "simple") {
            var label = options.label || RED._("deploy.deploy");
            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") {
            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); });
        }



        RED.events.on('workspace:dirty',function(state) {
            if (state.dirty) {
                window.onbeforeunload = function() {
                    return RED._("deploy.confirm.undeployedChanges");
                }
                $("#red-ui-header-button-deploy").removeClass("disabled");
            } else {
                window.onbeforeunload = null;
                $("#red-ui-header-button-deploy").addClass("disabled");
            }
        });

        var activeNotifyMessage;
        RED.comms.subscribe("notification/runtime-deploy",function(topic,msg) {
            if (!activeNotifyMessage) {
                var currentRev = RED.nodes.version();
                if (currentRev === null || deployInflight || currentRev === msg.revision) {
                    return;
                }
                var message = $('<p>').text(RED._('deploy.confirm.backgroundUpdate'));
                activeNotifyMessage = RED.notify(message,{
                    modal: true,
                    fixed: true,
                    buttons: [
                        {
                            text: RED._('deploy.confirm.button.ignore'),
                            click: function() {
                                activeNotifyMessage.close();
                                activeNotifyMessage = null;
                            }
                        },
                        {
                            text: RED._('deploy.confirm.button.review'),
                            class: "primary",
                            click: function() {
                                activeNotifyMessage.close();
                                var nns = RED.nodes.createCompleteNodeSet();
                                resolveConflict(nns,false);
                                activeNotifyMessage = null;
                            }
                        }
                    ]
                });
            }
        });
    }

    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();
                        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();
                    }
                }
            }
        ];
        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();
                }
            })
        }
        var conflictNotification = RED.notify(message,{
            modal: true,
            fixed: true,
            width: 600,
            buttons: buttons
        });

        var now = Date.now();
        RED.diff.getRemoteDiff(function(diff) {
            var ellapsed = Math.max(1000 - (Date.now()-now), 0);
            currentDiff = diff;
            setTimeout(function() {
                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')
            },ellapsed);
        })
    }
    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 restart() {
        var startTime = Date.now();
        $(".red-ui-deploy-button-content").css('opacity',0);
        $(".red-ui-deploy-button-spinner").show();
        var deployWasEnabled = !$("#red-ui-header-button-deploy").hasClass("disabled");
        $("#red-ui-header-button-deploy").addClass("disabled");
        deployInflight = true;
        $("#red-ui-header-shade").show();
        $("#red-ui-editor-shade").show();
        $("#red-ui-palette-shade").show();
        $("#red-ui-sidebar-shade").show();

        $.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() {
            deployInflight = false;
            var delta = Math.max(0,300-(Date.now()-startTime));
            setTimeout(function() {
                $(".red-ui-deploy-button-content").css('opacity',1);
                $(".red-ui-deploy-button-spinner").hide();
                $("#red-ui-header-shade").hide();
                $("#red-ui-editor-shade").hide();
                $("#red-ui-palette-shade").hide();
                $("#red-ui-sidebar-shade").hide();
            },delta);
        });
    }
    function save(skipValidation,force) {
        if (!$("#red-ui-header-button-deploy").hasClass("disabled")) {
            if (!RED.user.hasPermission("flows.write")) {
                RED.notify(RED._("user.errors.deploy"),"error");
                return;
            }
            if (!skipValidation) {
                var hasUnknown = false;
                var hasInvalid = false;
                var hasUnusedConfig = false;

                var unknownNodes = [];
                var invalidNodes = [];

                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;

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

                var showWarning = false;
                var notificationMessage;
                var notificationButtons = [];
                var 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= [
                        {
                            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= [
                        {
                            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;
                }
            }

            var nns = RED.nodes.createCompleteNodeSet();

            var startTime = Date.now();
            $(".red-ui-deploy-button-content").css('opacity',0);
            $(".red-ui-deploy-button-spinner").show();
            $("#red-ui-header-button-deploy").addClass("disabled");

            var data = {flows:nns};

            if (!force) {
                data.rev = RED.nodes.version();
            }

            deployInflight = true;
            $("#red-ui-header-shade").show();
            $("#red-ui-editor-shade").show();
            $("#red-ui-palette-shade").show();
            $("#red-ui-sidebar-shade").show();
            $.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) {
                    RED.notify(
                    '<p>'+RED._("deploy.successfulDeploy")+'</p>'+
                    '<p>'+RED._("deploy.unusedConfigNodes")+' <a href="#" onclick="RED.sidebar.config.show(true); return false;">'+RED._("deploy.unusedConfigNodesLink")+'</a></p>',"success",false,6000);
                } else {
                    RED.notify('<p>'+RED._("deploy.successfulDeploy")+'</p>',"success");
                }
                RED.nodes.eachNode(function(node) {
                    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.eachConfig(function (confNode) {
                    confNode.changed = false;
                    if (confNode.credentials) {
                        delete confNode.credentials;
                    }
                });
                RED.nodes.eachSubflow(function(subflow) {
                    subflow.changed = false;
                });
                RED.nodes.eachWorkspace(function(ws) {
                    ws.changed = false;
                });
                // Once deployed, cannot undo back to a clean state
                RED.history.markAllDirty();
                RED.view.redraw();
                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() {
                deployInflight = false;
                var delta = Math.max(0,300-(Date.now()-startTime));
                setTimeout(function() {
                    $(".red-ui-deploy-button-content").css('opacity',1);
                    $(".red-ui-deploy-button-spinner").hide();
                    $("#red-ui-header-shade").hide();
                    $("#red-ui-editor-shade").hide();
                    $("#red-ui-palette-shade").hide();
                    $("#red-ui-sidebar-shade").hide();
                },delta);
            });
        }
    }
    return {
        init: init,
        setDeployInflight: function(state) {
            deployInflight = state;
        }

    }
})();
;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,
                        unchangedCount: 0
                    },
                    remote: {
                        addedCount:0,
                        deletedCount:0,
                        changedCount: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,conflicts).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.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) {
                                $('<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);
                            }
                            if (flowStats.local.changedCount > 0) {
                                $('<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);
                            }
                            if (flowStats.local.deletedCount > 0) {
                                $('<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);
                            }
                            $('<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.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) {
                                    $('<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);
                                }
                                if (flowStats.remote.changedCount > 0) {
                                    $('<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);
                                }
                                if (flowStats.remote.deletedCount > 0) {
                                    $('<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);
                                }
                                $('<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);

        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("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
        var localChanged = false;

        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]) {
            stats.local.changedCount++;
            hasChanges = true;
            unChanged = false;
        }
        if (remoteDiff && remoteDiff.changed[node.id]) {
            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) {
                        localMovedMessage = RED._("diff.type.movedFrom",{id:(localDiff.currentConfig.all[node.id].z||'global')});
                    } else {
                        localMovedMessage = RED._("diff.type.movedTo",{id:(localN.z||'global')});
                    }
                    $('<span class="red-ui-diff-status"><i class="fa fa-caret-square-o-right"></i> '+localMovedMessage+'</span>').appendTo(localNodeDiv);
                }
                localChanged = true;
            } else if (localDiff.deleted[node.z]) {
                localNodeDiv.addClass("red-ui-diff-empty");
                localChanged = true;
            } 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);
                localChanged = true;
            } else if (localDiff.changed[node.id]) {
                if (localDiff.newConfig.all[node.id].z !== node.z) {
                    localNodeDiv.addClass("red-ui-diff-empty");
                } 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);
                    localChanged = true;
                }
            } 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) {
                            remoteMovedMessage = RED._("diff.type.movedFrom",{id:(remoteDiff.currentConfig.all[node.id].z||'global')});
                        } else {
                            remoteMovedMessage = RED._("diff.type.movedTo",{id:(remoteN.z||'global')});
                        }
                        $('<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 {
                        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("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?"changed":"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?"changed":"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("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])) {
                    $('<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);
                }
            }


        });
        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) {
        if (diff === undefined) {
            getRemoteDiff(showRemoteDiff);
        } else {
            showDiff(diff,{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) {
        var currentConfig = parseNodes(currentNodes);
        var newConfig = parseNodes(newNodes);
        var added = {};
        var deleted = {};
        var changed = {};
        var moved = {};

        Object.keys(currentConfig.all).forEach(function(id) {
            var node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id);
            if (!newConfig.all.hasOwnProperty(id)) {
                deleted[id] = true;
            } else if (JSON.stringify(currentConfig.all[id]) !== JSON.stringify(newConfig.all[id])) {
                changed[id] = true;

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

        var diff = {
            currentConfig: currentConfig,
            newConfig: newConfig,
            added: added,
            deleted: deleted,
            changed: changed,
            moved: 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';

        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);
                            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 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;
                    }
                    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;
                        }
                        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: nodeChangedStates,
            localChangedStates: 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 localChangedStates = appliedDiff.localChangedStates;

        var isDirty = RED.nodes.dirty();

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

        RED.history.push(historyEvent);

        var originalFlow = RED.nodes.originalFlow();
        // originalFlow is what the editor things 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);
        imported.nodes.forEach(function(n) {
            if (nodeChangedStates[n.id] || localChangedStates[n.id]) {
                n.changed = 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("<<<<<<< Local Changes").appendTo(line);
                                    hunk.localChangeStart = actualLineNumber;
                                } else {
                                    hunk.remoteChangeEnd = actualLineNumber;
                                    $('<span>').text(">>>>>>> Remote Changes").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> use '+(isOurs?"local":"remote")+' changes</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("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) && (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);
                } else if (Object.keys(handler).length > 0) {
                    partialState = handler;
                    evt.preventDefault();
                    return null;
                } else {
                    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></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
                }
                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.id.replace(/(^.+:([a-z]))|(-([a-z]))/g,function() {
            if (arguments[5] === 0) {
                return arguments[2].toUpperCase();
            } else {
                return " "+arguments[4].toUpperCase();
            }
        });
        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();
                if (filterValue === "") {
                    shortcutList.editableList('filter', null);
                } else {
                    filterValue = filterValue.replace(/\s/g,"");
                    shortcutList.editableList('filter', function(data) {
                        return data.id.toLowerCase().replace(/^.*:/,"").replace("-","").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 Aid = A.id.replace(/^.*:/,"").replace(/[ -]/g,"").toLowerCase();
            var Bid = B.id.replace(/^.*:/,"").replace(/[ -]/g,"").toLowerCase();
            return Aid.localeCompare(Bid);
        });
        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
    }

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

    var activeWorkspace = 0;
    var workspaceIndex = 0;

    var viewStack = [];
    var hideStack = [];
    var viewStackPos = 0;

    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;
            }
            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 a[title='"+RED._('workspace.defaultName',{number:workspaceIndex})+"']").size() !== 0);

            ws = {
                type: "tab",
                id: tabId,
                disabled: false,
                info: "",
                label: RED._('workspace.defaultName',{number:workspaceIndex}),
                env: [],
                hideable: 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.view.focus();
        return ws;
    }

    function deleteWorkspace(ws) {
        if (workspaceTabCount === 1) {
            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 {
            RED.editor.editFlow(workspace);
        }
    }


    var workspace_tabs;
    var workspaceTabCount = 0;
    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;
                    $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!tab.disabled);
                    } else {
                    $("#red-ui-workspace-chart").hide();
                    activeWorkspace = 0;
                    window.location.hash = '';
                }
                event.workspace = activeWorkspace;
                RED.events.emit("workspace:change",event);
                RED.sidebar.config.refresh();
                RED.view.focus();
            },
            onclick: function(tab) {
                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');
                }
                RED.menu.setDisabled("menu-item-workspace-delete",activeWorkspace === 0 || workspaceTabCount <= 1);
                if (workspaceTabCount === 1) {
                    showWorkspace();
                }
            },
            onremove: function(tab) {
                if (tab.type === "tab") {
                    workspaceTabCount--;
                } else {
                    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()
                });
                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);
                RED.events.emit("workspace:hide",{workspace: tab.id})
            },
            onshow: function(tab) {
                removeFromHideStack(tab.id);
                RED.events.emit("workspace:show",{workspace: tab.id})
            },
            minimumActiveTabWidth: 150,
            scrollable: true,
            addButton: "core:add-flow",
            addButtonCaption: RED._("workspace.addFlow"),
            menu: function() {
                var menuItems = [
                    {
                        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,
                    {
                        id:"red-ui-tabs-menu-option-add-flow",
                        label: RED._("workspace.addFlow"),
                        onselect: "core:add-flow"
                    },
                    {
                        id:"red-ui-tabs-menu-option-add-flow-right",
                        label: RED._("workspace.addFlowToRight"),
                        onselect: "core:add-flow-to-right"
                    },
                    null,
                    {
                        id:"red-ui-tabs-menu-option-add-hide-flows",
                        label: RED._("workspace.hideFlow"),
                        onselect: "core:hide-flow"
                    },
                    {
                        id:"red-ui-tabs-menu-option-add-hide-other-flows",
                        label: RED._("workspace.hideOtherFlows"),
                        onselect: "core:hide-other-flows"
                    },
                    {
                        id:"red-ui-tabs-menu-option-add-show-all-flows",
                        label: RED._("workspace.showAllFlows"),
                        onselect: "core:show-all-flows"
                    },
                    {
                        id:"red-ui-tabs-menu-option-add-hide-all-flows",
                        label: RED._("workspace.hideAllFlows"),
                        onselect: "core:hide-all-flows"
                    },
                    {
                        id:"red-ui-tabs-menu-option-add-show-last-flow",
                        label: RED._("workspace.showLastHiddenFlow"),
                        onselect: "core:show-last-hidden-flow"
                    }
                ]
                if (hideStack.length > 0) {
                    menuItems.unshift({
                        label: RED._("workspace.hiddenFlows",{count: hideStack.length}),
                        onselect: "core:list-hidden-flows"
                    })
                }
                return menuItems;
            }
        });
        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.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();
        });

        RED.actions.add("core:add-flow",function(opts) { addWorkspace(undefined,undefined,opts?opts.index:undefined)});
        RED.actions.add("core:add-flow-to-right",function(opts) { addWorkspace(undefined,undefined,workspace_tabs.activeIndex()+1)});
        RED.actions.add("core:edit-flow",editWorkspace);
        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:hide-flow", function() {
            var 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() {
            var 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-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);
            }
        })
        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) {
            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 removeWorkspace(ws) {
        if (!ws) {
            deleteWorkspace(RED.nodes.workspace(activeWorkspace));
        } else {
            if (workspace_tabs.contains(ws.id)) {
                workspace_tabs.removeTab(ws.id);
            }
            if (ws.id === activeWorkspace) {
                activeWorkspace = 0;
            }
        }
    }

    function setWorkspaceOrder(order) {
        var newOrder = order.filter(function(id) {
            return RED.nodes.workspace(id) !== undefined;
        })
        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 {
        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
        },
        selection: function() {
            return workspace_tabs.selection();
        },
        hide: function(id) {
            if (!id) {
                id = activeWorkspace;
            }
            if (workspace_tabs.contains(id)) {
                workspace_tabs.hideTab(id);
                var hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}");
                hiddenTabs[id] = true;
                RED.settings.setLocal("hiddenTabs",JSON.stringify(hiddenTabs));
            }
        },
        isHidden: function(id) {
            return hideStack.includes(id)
        },
        show: function(id,skipStack,unhideOnly) {
            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);
            }
            var hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}");
            delete hiddenTabs[id];
            RED.settings.setLocal("hiddenTabs",JSON.stringify(hiddenTabs));
        },
        refresh: function() {
            RED.nodes.eachWorkspace(function(ws) {
                workspace_tabs.renameTab(ws.id,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
    }
})();
;/**
 * 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>');
        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> "dragGroupLayer"
  *                |- <g> "nodeLayer"
  */

RED.view = (function() {
    var space_width = 5000,
        space_height = 5000,
        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 activeSubflow = null;
    var activeNodes = [];
    var activeLinks = [];
    var activeFlowLinks = [];
    var activeLinkNodes = {};
    var activeGroup = null;
    var activeHoverGroup = null;
    var activeGroups = [];
    var dirtyGroups = {};

    var selected_link = null;
    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 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;

    var clipboard = "";

    // Note: these are the permitted status colour aliases. The actual RGB values
    //       are set in the CSS - flow.scss/colors.scss
    var status_colours = {
        "red":    "#c00",
        "green":  "#5a8",
        "yellow": "#F9DF31",
        "blue":   "#53A3F3",
        "grey":   "#d3d3d3",
        "gray":   "#d3d3d3"
    }

    var PORT_TYPE_INPUT = 1;
    var PORT_TYPE_OUTPUT = 0;

    var chart;
    var outer;
    var eventLayer;
    var gridLayer;
    var linkLayer;
    var dragGroupLayer;
    var groupSelectLayer;
    var nodeLayer;
    var groupLayer;
    var drag_lines;

    var movingSet = (function() {
        var setIds = new Set();
        var set = [];
        var 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);
                    }
                }
            },
            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;
                            }
                        }
                    }
                }
            },
            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 })}
        }
        return api;
    })();

    function init() {

        chart = $("#red-ui-workspace-chart");

        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() {
                if (lasso) {
                    if (d3.event.buttons !== 1) {
                        lasso.remove();
                        lasso = null;
                    }
                } else if (mouse_mode === RED.state.PANNING && d3.event.buttons !== 4) {
                    resetMouseVars();
                }
            })
            .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);
                        //lasso = eventLayer.append("rect")
                        //    .attr("ox",point[0])
                        //    .attr("oy",point[1])
                        //    .attr("rx",2)
                        //    .attr("ry",2)
                        //    .attr("x",point[0])
                        //    .attr("y",point[1])
                        //    .attr("width",0)
                        //    .attr("height",0)
                        //    .attr("class","nr-ui-view-lasso");
                    },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();
            });

        // 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");
        nodeLayer = eventLayer.append("g");

        drag_lines = [];

        RED.events.on("workspace:change",function(event) {
            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);

            RED.menu.setDisabled("menu-item-workspace-edit", activeSubflow || event.workspace === 0);
            RED.menu.setDisabled("menu-item-workspace-delete",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.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(); }
            }
        });

        // Handle nodes dragged from the palette
        chart.droppable({
            accept:".red-ui-palette-node",
            drop: function( event, ui ) {
                d3.event = event;
                var selected_tool = $(ui.draggable[0]).attr("data-palette-type");
                var result = addNode(selected_tool);
                if (!result) {
                    return;
                }
                var historyEvent = result.historyEvent;
                var nn = 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 mousePos = d3.touches(this)[0]||d3.mouse(this);

                mousePos[1] += this.scrollTop + ((nn.h/2)-helperOffset[1]);
                mousePos[0] += this.scrollLeft + ((nn.w/2)-helperOffset[0]);
                mousePos[1] /= scaleFactor;
                mousePos[0] /= scaleFactor;

                if (snapGrid) {
                    mousePos[0] = gridSize*(Math.ceil(mousePos[0]/gridSize));
                    mousePos[1] = gridSize*(Math.ceil(mousePos[1]/gridSize));
                }
                nn.x = mousePos[0];
                nn.y = mousePos[1];

                var spliceLink = $(ui.helper).data("splice");
                if (spliceLink) {
                    // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp/showQuickAddDialog
                    RED.nodes.removeLink(spliceLink);
                    var link1 = {
                        source:spliceLink.source,
                        sourcePort:spliceLink.sourcePort,
                        target: nn
                    };
                    var link2 = {
                        source:nn,
                        sourcePort:0,
                        target: spliceLink.target
                    };
                    RED.nodes.addLink(link1);
                    RED.nodes.addLink(link2);
                    historyEvent.links = [link1,link2];
                    historyEvent.removedLinks = [spliceLink];
                }

                RED.nodes.add(nn);

                var group = $(ui.helper).data("group");
                if (group) {
                    RED.group.addToGroup(group, nn);
                    historyEvent = {
                        t: 'multi',
                        events: [historyEvent],

                    }
                    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)
                exitActiveGroup();
                clearSelection();
                nn.selected = true;
                movingSet.add(nn);
                if (group) {
                    selectGroup(group,false);
                    enterActiveGroup(group);
                    activeGroup = group;
                }
                updateActiveNodes();
                updateSelection();
                redraw();

                if (nn._def.autoedit) {
                    RED.editor.edit(nn);
                }
            }
        });
        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();deleteSelection();});
        RED.actions.add("core:paste-from-internal-clipboard",function(){importNodes(clipboard,{generateIds: true});});

        RED.events.on("view:selection-changed", function(selection) {
            var hasSelection = (selection.nodes && selection.nodes.length > 0);
            var hasMultipleSelection = hasSelection && selection.nodes.length > 1;
            RED.menu.setDisabled("menu-item-edit-cut",!hasSelection);
            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",!hasSelection);
            RED.menu.setDisabled("menu-item-view-tools-move-to-front",!hasSelection);
            RED.menu.setDisabled("menu-item-view-tools-move-backwards",!hasSelection);
            RED.menu.setDisabled("menu-item-view-tools-move-forwards",!hasSelection);

            RED.menu.setDisabled("menu-item-view-tools-align-left",!hasMultipleSelection);
            RED.menu.setDisabled("menu-item-view-tools-align-center",!hasMultipleSelection);
            RED.menu.setDisabled("menu-item-view-tools-align-right",!hasMultipleSelection);
            RED.menu.setDisabled("menu-item-view-tools-align-top",!hasMultipleSelection);
            RED.menu.setDisabled("menu-item-view-tools-align-middle",!hasMultipleSelection);
            RED.menu.setDisabled("menu-item-view-tools-align-bottom",!hasMultipleSelection);
            RED.menu.setDisabled("menu-item-view-tools-distribute-horizontally",!hasMultipleSelection);
            RED.menu.setDisabled("menu-item-view-tools-distribute-veritcally",!hasMultipleSelection);
        })

        RED.actions.add("core:delete-selection",deleteSelection);
        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 }
        })

    }



    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}
            });

            activeGroups = RED.nodes.groups(activeWorkspace)||[];
            activeGroups.forEach(function(g, i) {
                g._index = i;
                if (g.g) {
                    g._root = g.g;
                    g._depth = 1;
                } else {
                    g._root = g.id;
                    g._depth = 0;
                }
            });
        } else {
            activeNodes = [];
            activeLinks = [];
            activeGroups = [];
        }

        var changed = false;
        do {
            changed = false;
            activeGroups.forEach(function(g) {
                if (g.g) {
                    var parentGroup = RED.nodes.group(g.g);
                    if (parentGroup) {
                        var parentDepth = parentGroup._depth;
                        if (g._depth !== parentDepth + 1) {
                            g._depth = parentDepth + 1;
                            changed = true;
                        }
                        if (g._root !== parentGroup._root) {
                            g._root = parentGroup._root;
                            changed = true;
                        }
                    }
                }
            });
        } while (changed)
        activeGroups.sort(function(a,b) {
            if (a._root === b._root) {
                return a._depth - b._depth;
            } else {
                // return a._root.localeCompare(b._root);
                return a._index - b._index;
            }
        });

        var group = groupLayer.selectAll(".red-ui-flow-group").data(activeGroups,function(d) { return d.id });
        group.sort(function(a,b) {
            if (a._root === b._root) {
                return a._depth - b._depth;
            } else {
                return a._index - b._index;
                // return a._root.localeCompare(b._root);
            }
        })
    }

    function generateLinkPath(origX,origY, destX, destY, sc) {
        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));
        }
        if (dx*sc > 0) {
            return "M "+origX+" "+origY+
                " C "+(origX+sc*(node_width*scale))+" "+(origY+scaleY*node_height)+" "+
                (destX-sc*(scale)*node_width)+" "+(destY-scaleY*node_height)+" "+
                destX+" "+destY
        } else {

            var midX = Math.floor(destX-dx/2);
            var midY = Math.floor(destY-dy/2);
            //
            if (dy === 0) {
                midY = destY + node_height;
            }
            var cp_height = node_height/2;
            var y1 = (destY + midY)/2
            var topX =origX + sc*node_width*scale;
            var topY = dy>0?Math.min(y1 - dy/2 , origY+cp_height):Math.max(y1 - dy/2 , origY-cp_height);
            var bottomX = destX - sc*node_width*scale;
            var 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;
            var 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
        }
    }

    function addNode(type,x,y) {
        var m = /^subflow:(.+)$/.exec(type);

        if (activeSubflow && m) {
            var subflowId = m[1];
            if (subflowId === activeSubflow.id) {
                RED.notify(RED._("notification.error",{message: RED._("notification.errors.cannotAddSubflowToItself")}),"error");
                return;
            }
            if (RED.nodes.subflowContains(m[1],activeSubflow.id)) {
                RED.notify(RED._("notification.error",{message: RED._("notification.errors.cannotAddCircularReference")}),"error");
                return;
            }
        }

        var nn = { id:RED.nodes.id(),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 = node_width;
        nn.h = Math.max(node_height,(nn.outputs||0) * 15);
        nn.resize = true;

        var historyEvent = {
            t:"add",
            nodes:[nn.id],
            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
                }
            }
        }
        return {
            node: nn,
            historyEvent: historyEvent
        }

    }

    function canvasMouseDown() {
        if (RED.view.DEBUG) { console.warn("canvasMouseDown", mouse_mode); }
        var point;
        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }

        if (d3.event.button === 1) {
            // Middle Click pan
            mouse_mode = RED.state.PANNING;
            mouse_position = [d3.event.pageX,d3.event.pageY]
            scroll_position = [chart.scrollLeft(),chart.scrollTop()];
            return;
        }
        if (!mousedown_node && !mousedown_link && !mousedown_group) {
            selected_link = null;
            updateSelection();
        }
        if (mouse_mode === 0) {
            if (lasso) {
                lasso.remove();
                lasso = null;
            }
        }
        if (mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) {
            if (d3.event.metaKey || d3.event.ctrlKey) {
                d3.event.stopPropagation();
                clearSelection();
                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});
            }
        }
        if (mouse_mode === 0 && !(d3.event.metaKey || d3.event.ctrlKey)) {
            if (!touchStartTime) {
                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();
            }
        }
    }

    function showQuickAddDialog(options) {
        options = options || {};
        var point = options.position || lastClickPosition;
        var spliceLink = options.splice;
        var targetGroup = options.group;
        var touchTrigger = options.touchTrigger;

        if (targetGroup && !targetGroup.active) {
            selectGroup(targetGroup,false);
            enterActiveGroup(targetGroup);
            RED.view.redraw();
        }

        var ox = point[0];
        var oy = point[1];

        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 (spliceLink) {
            filter = {input:true, output:true}
        }

        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:d3.event.clientX-mainPos.left-node_width/2 - (ox-point[0]),
            y:d3.event.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 result = addNode(type);
                if (!result) {
                    return;
                }
                if (keepAdding) {
                    mouse_mode = RED.state.QUICK_JOINING;
                }

                var nn = result.node;
                var historyEvent = result.historyEvent;
                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 (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.nodes.add(nn);
                RED.editor.validateNode(nn);

                if (targetGroup) {
                    RED.group.addToGroup(targetGroup, nn);
                    if (historyEvent.t !== "multi") {
                        historyEvent = {
                            t:'multi',
                            events: [historyEvent]
                        }
                    }
                    historyEvent.events.push({
                        t: "addToGroup",
                        group: targetGroup,
                        nodes: nn
                    })

                }

                if (spliceLink) {
                    resetMouseVars();
                    // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp/showQuickAddDialog
                    RED.nodes.removeLink(spliceLink);
                    var link1 = {
                        source:spliceLink.source,
                        sourcePort:spliceLink.sourcePort,
                        target: nn
                    };
                    var link2 = {
                        source:nn,
                        sourcePort:0,
                        target: spliceLink.target
                    };
                    RED.nodes.addLink(link1);
                    RED.nodes.addLink(link2);
                    historyEvent.links = (historyEvent.links || []).concat([link1,link2]);
                    historyEvent.removedLinks = [spliceLink];
                }
                RED.history.push(historyEvent);
                RED.nodes.dirty(true);
                // auto select dropped node - so info shows (if visible)
                clearSelection();
                nn.selected = true;
                if (targetGroup) {
                    selectGroup(targetGroup,false);
                    enterActiveGroup(targetGroup);
                }
                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;
        }

        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }

        if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && !mousedown_group && selected_link == null) {
            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 (selected_link &&
                        ((mousedown_port_type === PORT_TYPE_OUTPUT &&
                            selected_link.source === mousedown_node &&
                            selected_link.sourcePort === mousedown_port_index
                        ) ||
                        (mousedown_port_type === PORT_TYPE_INPUT &&
                            selected_link.target === mousedown_node
                        ))
                    ) {
                        existingLinks = [selected_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}]);
                }
                selected_link = null;
            }
            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));
            }
            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)) {
                mouse_mode = RED.state.MOVING_ACTIVE;
                clickElapsed = 0;
                spliceActive = false;
                if (movingSet.length() === 1) {
                    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;
                }
            }
        } else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_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.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 {
                    gridOffset[0] = node.n.x-(gridSize*Math.floor((node.n.x-node.n.w/2)/gridSize)+node.n.w/2);
                    gridOffset[1] = node.n.y-(gridSize*Math.floor(node.n.y/gridSize));
                }
                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 or group-add
            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);
                    }
                }
                if (node.n.type !== 'subflow' && !node.n.g && activeGroups) {
                    if (!groupHoverTimer) {
                        groupHoverTimer = setTimeout(function() {
                            activeHoverGroup = getGroupAt(node.n.x,node.n.y);
                            for (var i=0;i<activeGroups.length;i++) {
                                var g = activeGroups[i];
                                if (g === activeHoverGroup) {
                                    g.hovered = true;
                                    g.dirty = true;
                                } else if (g.hovered) {
                                    g.hovered = false;
                                    g.dirty = true;
                                }
                            }
                            groupHoverTimer = null;
                        },50);
                    }
                }
            }


        }
        if (mouse_mode !== 0) {
            redraw();
        }
    }

    function canvasMouseUp() {
        lastClickPosition = [d3.event.offsetX/scaleFactor,d3.event.offsetY/scaleFactor];
        if (RED.view.DEBUG) { console.warn("canvasMouseUp", mouse_mode); }
        var i;
        var historyEvent;
        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);
            }
            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"));
            var ag = activeGroup;
            if (!d3.event.shiftKey) {
                clearSelection();
                if (ag) {
                    if (x < ag.x+ag.w && x2 > ag.x && y < ag.y+ag.h && y2 > ag.y) {
                        // There was an active group and the lasso intersects with it,
                        // so reenter the group
                        enterActiveGroup(ag);
                        activeGroup.selected = true;
                    }
                }
            }
            activeGroups.forEach(function(g) {
                if (!g.selected) {
                    if (g.x > x && g.x+g.w < x2 && g.y > y && g.y+g.h < y2) {
                        if (!activeGroup || RED.group.contains(activeGroup,g)) {
                            while (g.g && (!activeGroup || g.g !== activeGroup.id)) {
                                g = RED.nodes.group(g.g);
                            }
                            if (!g.selected) {
                                selectGroup(g,true);
                            }
                        }
                    }
                }
            })

            activeNodes.forEach(function(n) {
                if (!n.selected) {
                    if (n.x > x && n.x < x2 && n.y > y && n.y < y2) {
                        if (!activeGroup || RED.group.contains(activeGroup,n)) {
                            if (n.g && (!activeGroup || n.g !== activeGroup.id)) {
                                var group = RED.nodes.group(n.g);
                                while (group.g && (!activeGroup || group.g !== activeGroup.id)) {
                                    group = RED.nodes.group(group.g);
                                }
                                if (!group.selected) {
                                    selectGroup(group,true);
                                }
                            } else {
                                n.selected = true;
                                n.dirty = true;
                                movingSet.add(n);
                            }
                        }
                    }
                }
            });



            // var selectionChanged = false;
            // do {
            //     selectionChanged = false;
            //     selectedGroups.forEach(function(g) {
            //         if (g.g && g.selected && RED.nodes.group(g.g).selected) {
            //             g.selected = false;
            //             selectionChanged = true;
            //         }
            //     })
            // } while(selectionChanged);

            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();
            lasso.remove();
            lasso = null;
        } else if (mouse_mode == RED.state.DEFAULT && mousedown_link == null && !d3.event.ctrlKey && !d3.event.metaKey ) {
            clearSelection();
            updateSelection();
        }
        if (mouse_mode == RED.state.MOVING_ACTIVE) {
            if (movingSet.length() > 0) {
                var addedToGroup = null;
                if (activeHoverGroup) {
                    for (var j=0;j<movingSet.length();j++) {
                        var n = movingSet.get(j);
                        RED.group.addToGroup(activeHoverGroup,n.n);
                    }
                    addedToGroup = activeHoverGroup;

                    activeHoverGroup.hovered = false;
                    enterActiveGroup(activeHoverGroup)
                    // TODO: check back whether this should add to moving_set
                    activeGroup.selected = true;
                    activeHoverGroup = null;
                }

                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;
                    }
                }

                if (ns.length > 0 && mouse_mode == RED.state.MOVING_ACTIVE) {
                    historyEvent = {t:"move",nodes:ns,dirty:RED.nodes.dirty()};
                    if (activeSpliceLink) {
                        // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp
                        var spliceLink = d3.select(activeSpliceLink).data()[0];
                        RED.nodes.removeLink(spliceLink);
                        var link1 = {
                            source:spliceLink.source,
                            sourcePort:spliceLink.sourcePort,
                            target: movingSet.get(0).n
                        };
                        var link2 = {
                            source:movingSet.get(0).n,
                            sourcePort:0,
                            target: spliceLink.target
                        };
                        RED.nodes.addLink(link1);
                        RED.nodes.addLink(link2);
                        historyEvent.links = [link1,link2];
                        historyEvent.removedLinks = [spliceLink];
                        updateActiveNodes();
                    }
                    if (addedToGroup) {
                        historyEvent.addToGroup = addedToGroup;
                    }
                    RED.nodes.dirty(true);
                    RED.history.push(historyEvent);
                }
            }
        }
        // if (mouse_mode === RED.state.MOVING && mousedown_node && mousedown_node.g) {
        //     if (mousedown_node.gSelected) {
        //         delete mousedown_node.gSelected
        //     } else {
        //         if (!d3.event.ctrlKey && !d3.event.metaKey) {
        //             clearSelection();
        //         }
        //         RED.nodes.group(mousedown_node.g).selected = true;
        //         mousedown_node.selected = true;
        //         mousedown_node.dirty = true;
        //         movingSet.add(mousedown_node);
        //     }
        // }
        if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE) {
            // if (mousedown_node) {
            //     delete mousedown_node.gSelected;
            // }
            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) {
            updateActiveNodes();
            RED.nodes.dirty(true);
        }
        resetMouseVars();
        redraw();
    }

    function zoomIn() {
        if (scaleFactor < 2) {
            zoomView(scaleFactor+0.1);
        }
    }
    function zoomOut() {
        if (scaleFactor > 0.3) {
            zoomView(scaleFactor-0.1);
        }
    }
    function zoomZero() { zoomView(1); }

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

    function selectNone() {
        if (mouse_mode === RED.state.MOVING || mouse_mode === RED.state.MOVING_ACTIVE) {
            return;
        }
        if (mouse_mode === RED.state.IMPORT_DRAGGING) {
            clearSelection();
            RED.history.pop();
            mouse_mode = 0;
        } else if (activeGroup) {
            exitActiveGroup()
        } else {
            clearSelection();
        }
        redraw();
    }
    function selectAll() {
        if (mouse_mode === RED.state.SELECTING_NODE && selectNodesOptions.single) {
            return;
        }

        if (activeGroup) {
            var ag = activeGroup;
            clearSelection();
            enterActiveGroup(ag);

            var groupNodes = RED.group.getNodes(ag,false);
            groupNodes.forEach(function(n) {
                if (n.type === 'group') {
                    selectGroup(n,true,true);
                } else {
                    movingSet.add(n)
                    n.selected = true;
                    n.dirty = true;
                }
            })
            activeGroup.selected = true;
        } else {

            clearSelection();
            exitActiveGroup();
            activeGroups.forEach(function(g) {
                if (!g.g) {
                    selectGroup(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);
                }
            });

            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);
                    }
                }
            }
        }
        selected_link = null;
        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();
        selected_link = null;
        if (activeGroup) {
            activeGroup.active = false
            activeGroup.dirty = true;
            activeGroup = null;
        }
        activeGroups.forEach(function(g) {
            g.selected = false;
            g.dirty = true;
        })
    }

    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 && selected_link !== null && selected_link.link) {
                    activeLinks.push(selected_link);
                    activeLinkNodes[selected_link.source.id] = selected_link.source;
                    selected_link.source.dirty = true;
                    activeLinkNodes[selected_link.target.id] = selected_link.target;
                    selected_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;
            }
            return value;
        });
        if (selectionJSON !== lastSelection) {
            lastSelection = selectionJSON;
            RED.events.emit("view:selection-changed",selection);
        }
    }

    function editSelection() {
        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() {
        if (mouse_mode === RED.state.SELECTING_NODE) {
            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: [],
                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);
            }
            RED.history.push(historyEvent);
            RED.nodes.dirty(true);
            updateActiveNodes();
            updateSelection();
            redraw();
        } else if (movingSet.length() > 0 || selected_link != null) {
            var result;
            var node;
            var removedNodes = [];
            var removedLinks = [];
            var removedGroups = [];
            var removedSubflowOutputs = [];
            var removedSubflowInputs = [];
            var removedSubflowStatus;
            var subflowInstances = [];

            var startDirty = RED.nodes.dirty();
            var startChanged = false;
            var selectedGroups = [];
            if (movingSet.length() > 0) {

                for (var i=0;i<movingSet.length();i++) {
                    node = movingSet.get(i).n;
                    if (node.type === "group") {
                        selectedGroups.push(node);
                    }
                }
                // Make sure we have identified all groups about to be deleted
                for (i=0;i<selectedGroups.length;i++) {
                    selectedGroups[i].nodes.forEach(function(n) {
                        if (n.type === "group" && selectedGroups.indexOf(n) === -1) {
                            selectedGroups.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") {
                        if (node.x < 0) {
                            node.x = 25
                        }
                        var removedEntities = RED.nodes.remove(node.id);
                        removedNodes.push(node);
                        removedNodes = removedNodes.concat(removedEntities.nodes);
                        removedLinks = removedLinks.concat(removedEntities.links);
                        if (node.g) {
                            var group = RED.nodes.group(node.g);
                            if (selectedGroups.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 = selectedGroups.length-1; i>=0; i--) {
                    var g = selectedGroups[i];
                    removedGroups.push(g);
                    RED.nodes.removeGroup(g);
                }
                if (removedSubflowOutputs.length > 0) {
                    result = RED.subflow.removeOutput(removedSubflowOutputs);
                    if (result) {
                        removedLinks = removedLinks.concat(result.links);
                    }
                }
                // Assume 0/1 inputs
                if (removedSubflowInputs.length == 1) {
                    result = RED.subflow.removeInput();
                    if (result) {
                        removedLinks = removedLinks.concat(result.links);
                    }
                }
                if (removedSubflowStatus) {
                    result = RED.subflow.removeStatus();
                    if (result) {
                        removedLinks = removedLinks.concat(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) {
                    RED.nodes.dirty(true);
                }
            }
            var historyEvent;

            if (selected_link && selected_link.link) {
                var sourceId = selected_link.source.id;
                var targetId = selected_link.target.id;
                var sourceIdIndex = selected_link.target.links.indexOf(sourceId);
                var targetIdIndex = selected_link.source.links.indexOf(targetId);

                historyEvent = {
                    t:"multi",
                    events: [
                        {
                            t: "edit",
                            node: selected_link.source,
                            changed: selected_link.source.changed,
                            changes: {
                                links: $.extend(true,{},{v:selected_link.source.links}).v
                            }
                        },
                        {
                            t: "edit",
                            node: selected_link.target,
                            changed: selected_link.target.changed,
                            changes: {
                                links: $.extend(true,{},{v:selected_link.target.links}).v
                            }
                        }

                    ],
                    dirty:RED.nodes.dirty()
                }
                RED.nodes.dirty(true);
                selected_link.source.changed = true;
                selected_link.target.changed = true;
                selected_link.target.links.splice(sourceIdIndex,1);
                selected_link.source.links.splice(targetIdIndex,1);
                selected_link.source.dirty = true;
                selected_link.target.dirty = true;

            } else {
                if (selected_link) {
                    RED.nodes.removeLink(selected_link);
                    removedLinks.push(selected_link);
                }
                RED.nodes.dirty(true);
                historyEvent = {
                    t:"delete",
                    nodes:removedNodes,
                    links:removedLinks,
                    groups: removedGroups,
                    subflowOutputs:removedSubflowOutputs,
                    subflowInputs:removedSubflowInputs,
                    subflow: {
                        id: activeSubflow?activeSubflow.id:undefined,
                        instances: subflowInstances
                    },
                    dirty:startDirty
                };
                if (removedSubflowStatus) {
                    historyEvent.subflow.status = removedSubflowStatus;
                }
            }
            RED.history.push(historyEvent);

            selected_link = null;
            updateActiveNodes();
            updateSelection();
            redraw();
        }
    }

    function copySelection() {
        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 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 {
                        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);
            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 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;
        if (activeHoverGroup) {
            activeHoverGroup.hovered = false;
            activeHoverGroup = null;
        }
        d3.select(".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)
        if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91) {
            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); }
        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) {
            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) {
                var found = false;
                RED.nodes.eachNode(function(n) {
                    if (n.z == RED.workspaces.active()) {
                        var hw = n.w/2;
                        var hh = n.h/2;
                        if (n.x-hw<mouse_position[0] && n.x+hw> mouse_position[0] &&
                            n.y-hh<mouse_position[1] && n.y+hh>mouse_position[1]) {
                                found = true;
                                mouseup_node = n;
                                portType = mouseup_node.inputs>0?PORT_TYPE_INPUT:PORT_TYPE_OUTPUT;
                                portIndex = 0;
                        }
                    }
                });
                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++) {
                        var n = subflowPorts[i];
                        var hw = n.w/2;
                        var hh = n.h/2;
                        if (n.x-hw<mouse_position[0] && n.x+hw> mouse_position[0] &&
                            n.y-hh<mouse_position[1] && n.y+hh>mouse_position[1]) {
                                found = true;
                                mouseup_node = n;
                                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) {
                    var drag_line = drag_lines[i];
                    var src,dst,src_port;
                    if (drag_line.portType === PORT_TYPE_OUTPUT) {
                        src = drag_line.node;
                        src_port = drag_line.port;
                        dst = mouseup_node;
                    } else if (drag_line.portType === PORT_TYPE_INPUT) {
                        src = mouseup_node;
                        dst = drag_line.node;
                        src_port = portIndex;
                    }
                    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 = $.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;
                                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")
                        )) {
                            var existingLink = RED.nodes.filterLinks({source:src,target:dst,sourcePort: src_port}).length !== 0;
                            if (!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();
                    }
                    selected_link = select_link;
                    mousedown_link = select_link;
                    if (select_link) {
                        updateSelection();
                    }
                }
                redraw();
                return;
            }

            resetMouseVars();
            hideDragLines();
            selected_link = 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 ($.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() {
                var tooltip = getPortLabel(d,portType,portIndex);
                if (!tooltip) {
                    return;
                }
                var pos = getElementPosition(port.node());
                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 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];
        }

        mouse_offset = d3.mouse(document.body);
        if (isNaN(mouse_offset[0])) {
            mouse_offset = d3.touches(document.body)[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 (d.type != "subflow") {
                if (/^subflow:/.test(d.type) && (d3.event.ctrlKey || d3.event.metaKey)) {
                    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();

                selectGroup(RED.nodes.group(d.g), false);
                enterActiveGroup(RED.nodes.group(d.g))

                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();
        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) {
            var historyEvent = RED.history.peek();
            if (activeSpliceLink) {
                // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp
                var spliceLink = d3.select(activeSpliceLink).data()[0];
                RED.nodes.removeLink(spliceLink);
                var link1 = {
                    source:spliceLink.source,
                    sourcePort:spliceLink.sourcePort,
                    target: movingSet.get(0).n
                };
                var link2 = {
                    source:movingSet.get(0).n,
                    sourcePort:0,
                    target: spliceLink.target
                };
                RED.nodes.addLink(link1);
                RED.nodes.addLink(link2);

                historyEvent.links = [link1,link2];
                historyEvent.removedLinks = [spliceLink];
                updateActiveNodes();
            }

            if (activeHoverGroup) {
                for (var j=0;j<movingSet.length();j++) {
                    var n = movingSet.get(j);
                    RED.group.addToGroup(activeHoverGroup,n.n);
                }
                historyEvent.addedToGroup = activeHoverGroup;

                activeHoverGroup.hovered = false;
                enterActiveGroup(activeHoverGroup)
                // TODO: check back whether this should add to moving_set
                activeGroup.selected = true;
                activeHoverGroup = null;
            }


            updateSelection();
            RED.nodes.dirty(true);
            redraw();
            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 (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
            )
            lastClickNode = mousedown_node;

        var i;

        if (!d.selected && d.g /*&& !RED.nodes.group(d.g).selected*/) {
            var nodeGroup = RED.nodes.group(d.g);

            if (nodeGroup !== activeGroup && (d3.event.ctrlKey || d3.event.metaKey)) {
                if (activeGroup && nodeGroup.g === activeGroup.id) {
                    // Clicked on a node in a non-active group, inside the activeGroup, with ctrl pressed
                    // - add/remove the group from the current selection
                    groupNodeSelectPrimed = true;
                     if (nodeGroup.selected) {
                         deselectGroup(nodeGroup);
                     } else {
                         selectGroup(nodeGroup,true);
                     }
                } else {
                    // Clicked on a node in a non-active group with ctrl pressed
                    //  - exit active group
                    //  - toggle the select state of the group
                    exitActiveGroup();
                    groupNodeSelectPrimed = true;
                    if (nodeGroup.selected) {
                        deselectGroup(nodeGroup);
                    } else {
                        selectGroup(nodeGroup,true);
                    }
                }
            } else if (nodeGroup === activeGroup ) {
                if (d3.event.shiftKey) {
                    if (!d3.event.ctrlKey && !d3.event.metaKey) {
                        var ag = activeGroup;
                        clearSelection();
                        enterActiveGroup(ag);
                        activeGroup.selected = true;
                    }
                    console.log(d3.event);
                    var cnodes = RED.nodes.getAllFlowNodes(mousedown_node);
                    for (var n=0;n<cnodes.length;n++) {
                        if (!cnodes[n].selected) {
                            cnodes[n].selected = true;
                            cnodes[n].dirty = true;
                            movingSet.add(cnodes[n]);
                        }
                    }
                } else {
                    // Clicked on a node in the active group
                    if (!d3.event.ctrlKey && !d3.event.metaKey) {
                        // Ctrl not pressed so clear selection
                        var ag = activeGroup;
                        clearSelection();
                        deselectGroup(nodeGroup);
                        selectGroup(nodeGroup,false,false);
                        if (ag) {
                            enterActiveGroup(ag);
                            activeGroup.selected = true;
                        }
                    }

                    // Select this node
                    mousedown_node.selected = true;
                    movingSet.add(mousedown_node);
                }
            } else {
                // Clicked on a node in a group
                //  - if this group is not selected, clear current selection
                //    and select this group
                //  - if this group is not the active group, exit the active group
                //    and select the group
                //  - if this group is the active group, keep it active and
                //    change node selection

                // Set groupNodeSelectPrimed to true as this is a (de)select of the
                // group and NOT meant to trigger going into the group - see nodeMouseUp
                groupNodeSelectPrimed = !nodeGroup.selected;
                var ag = activeGroup;
                if (!nodeGroup.selected) {
                    clearSelection();
                }
                if (ag) {
                    if (ag !== nodeGroup && ag.id !== nodeGroup.g) {
                        ag.active = false;
                        ag.dirty = true;
                    } else {
                        activeGroup = ag;
                        activeGroup.active = true;
                    }
                } else {
                    dblClickPrimed = false;
                }
                selectGroup(nodeGroup, !(activeGroup && activeGroup === nodeGroup), !!groupNodeSelectPrimed);
                if (activeGroup && activeGroup === nodeGroup) {
                    mousedown_node.selected = true;
                    movingSet.add(mousedown_node);
                }
            }


            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);
            }
        } else if (d.selected && (d3.event.ctrlKey||d3.event.metaKey)) {
            mousedown_node.selected = false;
            movingSet.remove(mousedown_node);
        } else {

            // if (d.g && !RED.nodes.group(d.g).selected) {
            //     selectGroup(RED.nodes.group(d.g), false);
            // }


            // if (!d.selected && d.g) {
            //     if (!RED.nodes.group(d.g).selected) {// && !RED.nodes.group(d.g).selected) {
            //         clearSelection();
            //         selectGroup(RED.nodes.group(d.g));
            //         d.selected = true;
            //         console.log(d.id,"Setting selected")
            //         d.gSelected = true;
            //     }
            // } else
            if (d3.event.shiftKey) {
                clearSelection();
                var clickPosition = (d3.event.offsetX/scaleFactor - mousedown_node.x)
                var edgeDelta = (mousedown_node.w/2) - Math.abs(clickPosition);
                var cnodes;
                var targetEdgeDelta = mousedown_node.w > 30 ? 25 : 8;
                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();
                } else {
                    exitActiveGroup();
                }
                mousedown_node.selected = true;
                movingSet.add(mousedown_node);
            }
            selected_link = null;
            if (d3.event.button != 2) {
                mouse_mode = RED.state.MOVING;
                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() {
                    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 linkMouseDown(d) {
        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }
         mousedown_link = d;
         clearSelection();
         selected_link = mousedown_link;
         updateSelection();
         redraw();
         focusView();
         d3.event.stopPropagation();
         if (d3.event.metaKey || d3.event.ctrlKey) {
             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:selected_link, group:clickedGroup});
         }
    }
    function linkTouchStart(d) {
        if (mouse_mode === RED.state.SELECTING_NODE) {
            d3.event.stopPropagation();
            return;
        }
        mousedown_link = d;
        clearSelection();
        selected_link = 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 (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
        // }

        focusView();
        if (d3.event.button === 1) {
            return;
        }

        if (mouse_mode == RED.state.QUICK_JOINING) {
            d3.event.stopPropagation();
            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 && (d3.event.ctrlKey||d3.event.metaKey)) {
            if (g === activeGroup) {
                exitActiveGroup();
            }
            deselectGroup(g);
            d3.event.stopPropagation();
        } else {
            if (!g.selected) {
                if (!d3.event.ctrlKey && !d3.event.metaKey) {
                    var ag = activeGroup;
                    clearSelection();
                    if (ag && g.g === ag.id) {
                        enterActiveGroup(ag);
                        activeGroup.selected = true;
                    }
                }
                if (activeGroup) {
                    if (!RED.group.contains(activeGroup,g)) {
                        // Clicked on a group that is outside the activeGroup
                        exitActiveGroup();
                    } else {
                    }
                }
                selectGroup(g,true);//!wasSelected);
            } else if (activeGroup && g.g !== activeGroup.id){
                exitActiveGroup();
            }


            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 selectGroup(g, includeNodes, addToMovingSet) {
        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.selected = true;
                }
                n.dirty = true;
            })
        }
    }
    function enterActiveGroup(group) {
        if (activeGroup) {
            exitActiveGroup();
        }
        group.active = true;
        group.dirty = true;
        activeGroup = group;
        movingSet.remove(group);
    }
    function exitActiveGroup() {
        if (activeGroup) {
            activeGroup.active = false;
            activeGroup.dirty = true;
            deselectGroup(activeGroup);
            selectGroup(activeGroup,true);
            activeGroup = null;
        }
    }
    function deselectGroup(g) {
        if (g.selected) {
            g.selected = false;
            g.dirty = true;
        }
        var nodeSet = new Set(g.nodes);
        nodeSet.add(g);
        for (var i = movingSet.length()-1; i >= 0; i -= 1) {
            var 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)
            }
        }
    }
    function getGroupAt(x,y) {
        // 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 (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) {
            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) {
            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 (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 = [];
        options.push({name:"delete",disabled:(movingSet.length()===0 && selected_link === null),onselect:function() {deleteSelection();}});
        options.push({name:"cut",disabled:(movingSet.length()===0),onselect:function() {copySelection();deleteSelection();}});
        options.push({name:"copy",disabled:(movingSet.length()===0),onselect:function() {copySelection();}});
        options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard, {generateIds: true, touchImport: true});}});
        options.push({name:"edit",disabled:(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",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;
                    var height = img.height * scaleFactor;
                    icon.attr("width",width);
                    icon.attr("height",height);
                    icon.attr("x",15-width/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";
                var fill = status_colours[d.status.fill]; // Only allow our colours for now
                if (d.status.shape == null && fill == null) {
                    nodeEl.__statusShape__.style.display = "none";
                    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);
                }
                if (d.status.hasOwnProperty('text')) {
                    nodeEl.__statusLabel__.textContent = d.status.text;
                } else {
                    nodeEl.__statusLabel__.textContent = "";
                }
            }
            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").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"});
                outGroup.each(function(d,i) {
                    d.w=40;
                    d.h=40;
                });
                outGroup.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)

                outGroup.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);});

                outGroup.append("svg:text").attr("class","red-ui-flow-port-label").attr("x",20).attr("y",12).style("font-size","10px").text("output");
                outGroup.append("svg:text").attr("class","red-ui-flow-port-label red-ui-flow-port-index").attr("x",20).attr("y",28).text(function(d,i){ return i+1});

                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 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;
                    }
                });
                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","rect");
                    icon_shade.setAttribute("x",0);
                    icon_shade.setAttribute("y",0);
                    icon_shade.setAttribute("class","red-ui-flow-node-icon-shade")
                    icon_shade.setAttribute("width",30);
                    icon_shade.setAttribute("height",Math.min(50,d.h-4));
                    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 statusRect = document.createElementNS("http://www.w3.org/2000/svg","rect");
                statusRect.setAttribute("class","red-ui-flow-node-status");
                statusRect.setAttribute("x",6);
                statusRect.setAttribute("y",1);
                statusRect.setAttribute("width",9);
                statusRect.setAttribute("height",9);
                statusRect.setAttribute("rx",2);
                statusRect.setAttribute("ry",2);
                statusRect.setAttribute("stroke-width","3");
                statusEl.appendChild(statusRect);
                node[0][0].__statusShape__ = statusRect;

                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;});
                            this.__iconShade__.setAttribute("height", d.h );
                            this.__iconShadeBorder__.setAttribute("d",
                                                                  "M " + (((!d._def.align && d.inputs !== 0 && d.outputs === 0) || "right" === d._def.align) ? 0 : 30) + " 1 l 0 " + (d.h - 2)
                                                                 );
                            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);

                            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 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)

                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_link || 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;
                    d.y1 = d.source.y+y;
                    d.x2 = d.target.x-d.target.w/2;
                    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);
                    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_link||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 selectGroup = groupSelectLayer.append('g').attr("class", "red-ui-flow-group").attr("id","group_select_"+d.id);
                selectGroup.append('rect').classed("red-ui-flow-group-outline-select",true)
                    .classed("red-ui-flow-group-outline-select-background",true)
                    .attr('rx',groupBorderRadius).attr('ry',groupBorderRadius)
                    .attr("x",-4)
                    .attr("y",-4);


                selectGroup.append('rect').classed("red-ui-flow-group-outline-select",true)
                    .attr('rx',groupBorderRadius).attr('ry',groupBorderRadius)
                    .attr("x",-4)
                    .attr("y",-4)
                selectGroup.on("mousedown", function() {groupMouseDown.call(g[0][0],d)});
                selectGroup.on("mouseup", function() {groupMouseUp.call(g[0][0],d)});
                selectGroup.on("touchstart", function() {groupMouseDown.call(g[0][0],d); d3.event.preventDefault();});
                selectGroup.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) {
                    if (a._root === b._root) {
                        return a._depth - b._depth;
                    } else {
                        return a._index - b._index;
                    }
                })
            }
            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.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)
                                }
                            });

                            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")
                    }
                    var selectGroupRect = selectGroup.children[0];
                    selectGroupRect.setAttribute("width",d.w+8)
                    selectGroupRect.setAttribute("height",d.h+8)
                    selectGroupRect.style.strokeOpacity = (d.active || d.selected || d.highlighted)?0.8:0;
                    selectGroupRect.style.strokeDasharray = (d.active)?"10 4":"";
                    selectGroupRect = selectGroup.children[1];
                    selectGroupRect.setAttribute("width",d.w+8)
                    selectGroupRect.setAttribute("height",d.h+8)
                    selectGroupRect.style.strokeOpacity = (d.active || d.selected || d.highlighted)?0.8:0;
                    selectGroupRect.style.strokeDasharray = (d.active)?"10 4":"";

                    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
     */
    function importNodes(newNodesObj,options) {
        options = options || {
            addFlow: false,
            touchImport: false,
            generateIds: false
        }
        var addNewFlow = options.addFlow
        var touchImport = options.touchImport;

        if (mouse_mode === RED.state.SELECTING_NODE) {
            return;
        }

        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 (!$.isArray(nodesToImport)) {
            nodesToImport = [nodesToImport];
        }


        try {
            var activeSubflowChanged;
            if (activeSubflow) {
                activeSubflowChanged = activeSubflow.changed;
            }
            var result = RED.nodes.import(nodesToImport,{generateIds:options.generateIds, addFlow: addNewFlow, importMap: options.importMap});
            if (result) {
                var new_nodes = result.nodes;
                var new_links = result.links;
                var new_groups = result.groups;
                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()}))
                var new_node_ids = new_nodes.map(function(n){ n.changed = true; 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];
                        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;
                        spliceActive = false;
                        if (movingSet.length() === 1) {
                            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))


                        }
                    }

                }

                var historyEvent = {
                    t:"add",
                    nodes:new_node_ids,
                    links:new_links,
                    groups:new_groups,
                    workspaces:new_workspaces,
                    subflows:new_subflows,
                    dirty:RED.nodes.dirty()
                };
                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
                        ]
                    }
                }

                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;
                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 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;
        }
        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;
                        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);
                }
            });
        }
        var selectedGroups = activeGroups.filter(function(g) { return g.selected && !g.active });
        if (selectedGroups.length > 0) {
            if (selectedGroups.length === 1 && selectedGroups[0].active) {
                // Let nodes be nodes
            } else {
                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 (selected_link != null) {
            selection.link = selected_link;
        }
        return selection;
    }

    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;
    }

    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 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 {
                                selectGroup(n,true);
                            }
                        })
                    }
                }
            }
            updateSelection();
            redraw(true);
        },
        selection: getSelection,

        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 activeGroup },
        reveal: function(id,triggerHighlight) {
            if (RED.nodes.workspace(id) || RED.nodes.subflow(id)) {
                RED.workspaces.show(id);
            } 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);

                        var screenSize = [chart.width()/scaleFactor,chart.height()/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) {
                            node.highlighted = true;
                            if (!node._flashing) {
                                node._flashing = true;
                                var flash = 22;
                                var flashFunc = function() {
                                    flash--;
                                    node.dirty = true;
                                    if (flash >= 0) {
                                        node.highlighted = !node.highlighted;
                                        setTimeout(flashFunc,100);
                                    } else {
                                        node.highlighted = false;
                                        delete node._flashing;
                                    }
                                    RED.view.redraw();
                                }
                                flashFunc();
                            }
                        }
                    } 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) {
            chart.scrollLeft(chart.scrollLeft()+x);
            chart.scrollTop(chart.scrollTop()+y)
        },
        clickNodeButton: function(n) {
            if (n._def.button) {
                nodeButtonClicked(n);
            }
        },
        clipboard: function() {
            return clipboard
        },
        redrawStatus: redrawStatus,
        showQuickAddDialog:showQuickAddDialog,
        calculateNodeDimensions: calculateNodeDimensions,
        getElementPosition:getElementPosition,
        showTooltip:showTooltip
    };
})();
;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__;
                }
                var badgeDX = 0;
                var controlDX = 0;
                for (var i=0,l=evt.el.__annotations__.length;i<l;i++) {
                    var annotation = evt.el.__annotations__[i];
                    if (annotations.hasOwnProperty(annotation.id)) {
                        var opts = annotations[annotation.id];
                        var showAnnotation = true;
                        var isBadge = opts.type === 'badge';
                        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) {
                                var rect = annotation.element.getBoundingClientRect();
                                badgeDX += rect.width;
                                annotation.element.setAttribute("transform", "translate("+(evt.node.w-3-badgeDX)+", -8)");
                                badgeDX += 4;
                            }
                        } 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,
            element: annotationGroup
        });
        var annotation = opts.element(evt.node);
        if (opts.tooltip) {
            annotation.addEventListener("mouseenter", getAnnotationMouseEnter(annotation,evt.node,opts.tooltip));
            annotation.addEventListener("mouseleave", annotationMouseLeave);
        }
        annotationGroup.appendChild(annotation);
        evt.el.appendChild(annotationGroup);
    }


    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 = 25;
     var nav_width = 5000/nav_scale;
     var nav_height = 5000/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() {

    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() {
        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 (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 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;
                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) {
        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) {
        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) {
        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) {
        var selection = RED.view.selection();
        if (selection.nodes) {
            var nodesToMove = [];
            selection.nodes.forEach(function(n) {
                if (n.type === "group") {
                    nodesToMove = nodesToMove.concat(RED.group.getNodes(n, true).filter(function(n) {
                        return n.type !== "group";
                    }))
                } 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);
                }
            }
        }
    }

    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: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
    }

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

    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) {
        if (id === ":first") {
            id = 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 (!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();
                }
            },
            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");
        showSidebar();
        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;

    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,
            close: function() {
                catDiv.removeClass("red-ui-palette-open");
                catDiv.addClass("red-ui-palette-closed");
                $("#red-ui-palette-base-category-"+category).slideUp();
                $("#red-ui-palette-header-"+category+" i").removeClass("expanded");
            },
            open: function() {
                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");
            },
            toggle: function() {
                if (catDiv.hasClass("red-ui-palette-open")) {
                    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;

                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)
                }

                var safeType = type.replace(/'/g,"\\'");

                $('<button type="button" onclick="RED.search.show(\'type:'+safeType+'\'); 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(/[ /.]/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 = nt;///^(.*?)([ -]in|[ -]out)?$/.exec(nt)[1];
            if (typeof def.paletteLabel !== "undefined") {
                try {
                    label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
                } catch(err) {
                    console.log("Definition error: "+nt+".paletteLabel",err);
                }
            }

            $('<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;
            $(d).draggable({
                helper: 'clone',
                appendTo: '#red-ui-editor',
                revert: 'invalid',
                revertDuration: 200,
                containment:'#red-ui-main-container',
                start: function() {
                    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() {
                    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;
                    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) {
                categoryContainers[rootCategory].open();
            }

        }
    }

    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");
            }
        }
    }

    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) {
            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");
                }
            }
        }

        paletteNode.css("backgroundColor", sf.color);
    }

    function filterChange(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 (var category in categoryContainers) {
            if (categoryContainers.hasOwnProperty(category)) {
                if (categoryContainers[category].container
                        .find(".red-ui-palette-node")
                        .filter(function() { return $(this).css('display') !== 'none'}).length === 0) {
                    categoryContainers[category].close();
                    categoryContainers[category].container.slideUp();
                } else {
                    categoryContainers[category].open();
                    categoryContainers[category].container.show();
                }
            }
        }
    }

    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('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() {
                filterChange($(this).val());
            }
        })

        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);
            }
        });
    }
    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;
    }
    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 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);
        propertiesPanelHeaderHelp = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-book"></button>').css({
            position: 'absolute',
            top: '12px',
            right: '32px'
        }).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 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":"scroll",
        }).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();
            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("Selection");
            propertiesPanelHeaderReveal.hide();
            propertiesPanelHeaderHelp.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).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();

            } else if (node.type === "group") {
                propertiesPanelHeaderHelp.hide();

                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 {
                propertiesPanelHeaderHelp.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).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);
            })
    }
    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(userCountBadge,function() { return RED._('editor.nodesUse',{count:n.users.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);
                }
            });
        }
        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)
        }
        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: [
                {label:RED._("sidebar.info.search.configNodes"), value:"is:config"},
                {label:RED._("sidebar.info.search.unusedConfigNodes"), value:"is:config is:unused"},
                {label:RED._("sidebar.info.search.invalidNodes"), value: "is:invalid"},
                {label:RED._("sidebar.info.search.uknownNodes"), value: "type:unknown"},
                {label:RED._("sidebar.info.search.unusedSubflows"), value:"is:subflow is:unused"},
                {label:RED._("sidebar.info.search.hiddenFlows"), value:"is:hidden"},
            ]
        });

        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);
            if (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:[]})
                }
            }
        })
        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)
        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)
        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]
            }
        }
        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 helpTopics = [];
    var treeList;
    var tocPanel;
    var helpIndex = {};

    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,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": "scroll"
        }).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() {
                var val = $(this).val().toLowerCase();
                if (val) {
                    showTOC();
                    var c = treeList.treeList('filter',function(item) {
                        if (item.depth === 0) {
                            return true;
                        }
                        return (item.nodeType &&  item.nodeType.indexOf(val) > -1) ||
                               (item.subflowLabel && item.subflowLabel.indexOf(val) > -1)
                    },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.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);

    }

    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);
        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() {
        helpTopics = [];
        var modules = RED.nodes.registry.getModuleList();
        var moduleNames = Object.keys(modules);
        moduleNames.sort();

        var nodeHelp = {
            label: RED._("sidebar.help.nodeHelp"),
            children: [],
            expanded: true
        }
        var helpData = [
            {
                id: 'changelog',
                label: "Node-RED v"+RED.settings.version,
                content: getChangelog
            },
            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) {
            var module = modules[moduleName];
            var nodeTypes = [];

            var setNames = Object.keys(module.sets);
            setNames.forEach(function(setName) {
                module.sets[setName].types.forEach(function(nodeType) {
                    if ($("script[data-help-name='"+nodeType+"']").length) {
                        nodeTypes.push({
                            id: "node-type:"+nodeType,
                            nodeType: nodeType,
                            element:getNodeLabel({_def:RED.nodes.getType(nodeType),type:nodeType})
                        })
                    }
                })
            })
            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 getNodeLabel(n) {
        var div = $('<div>',{class:"red-ui-node-list-item"});
        var icon = RED.utils.createNodeIcon(n).appendTo(div);
        var 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) {
            }
        }
        label = label || n.type;
        $('<div>',{class:"red-ui-node-label"}).text(n.name||n.type).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();
    }

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

    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'){
                    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() {


    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) {
        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);
            if (label) {
                $('<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,
                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 (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 label = 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;
                }

                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);
                var label = $('<div class="red-ui-palette-label"></div>').text(label).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");
                    }
                }
                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);
        })
        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()
                }
                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.showAllUnusedConfigNodes"));
        RED.popover.tooltip($('#red-ui-sidebar-config-filter-unused'), RED._("sidebar.config.showAllUnusedConfigNodes"));

    }
    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);
                }
                var flash = 21;
                var flashFunc = function() {
                    if ((flash%2)===0) {
                        node.removeClass('node_highlighted');
                    } else {
                        node.addClass('node_highlighted');
                    }
                    flash--;
                    if (flash >= 0) {
                        setTimeout(flashFunc,100);
                    }
                }
                flashFunc();
            },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 localCache = {};

    var flowAutoRefresh;
    var nodeAutoRefresh;
    var nodeSection;
    // var subflowSection;
    var flowSection;
    var globalSection;

    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) {
                    var k = keys[i];
                    var l2 = sortedData[k].length;
                    var propRow = $('<tr class="red-ui-help-info-row"><td class="red-ui-sidebar-context-property"></td><td></td></tr>').appendTo(container);
                    var obj = $(propRow.children()[0]);
                    obj.text(k);
                    var tools = $('<span class="button-group"></span>');

                    var 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+"/"+k+"?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), {
                                    typeHint: data.format,
                                    sourceId: id+"."+k,
                                    tools: tools,
                                    path: ""
                                }).appendTo(propRow.children()[1]);
                            }
                        })
                    });
                    RED.popover.tooltip(refreshItem,RED._("sidebar.context.refrsh"));
                    var 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() {
                                var 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();
                                    $.ajax({
                                        url: baseUrl+"/"+k+"?store="+v.store,
                                        type: "DELETE"
                                    }).done(function(data,textStatus,xhr) {
                                        $.getJSON(baseUrl+"/"+k+"?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();
                                                }
                                            } else {
                                                payload = data.msg;
                                                format = data.format;
                                                tools.detach();
                                                $(propRow.children()[1]).empty();
                                                RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
                                                    typeHint: data.format,
                                                    sourceId: id+"."+k,
                                                    tools: tools,
                                                    path: ""
                                                }).appendTo(propRow.children()[1]);
                                            }
                                        });
                                    }).fail(function(xhr,textStatus,err) {

                                    })
                                });
                                return content.i18n();
                            }
                        });
                        popover.open();

                    });
                    RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete"));
                    var payload = v.msg;
                    var format = v.format;
                    RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
                        typeHint: v.format,
                        sourceId: id+"."+k,
                        tools: tools,
                        path: ""
                    }).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;

    var editorTabs;
    var filterInput;
    var searchInput;
    var nodeList;
    var packageList;
    var loadedList = [];
    var filteredList = [];
    var 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);
        });
    }
    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) {
                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;
                        var set = moduleInfo.sets[setName];
                        var 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);
                            var swatch = setElements.swatches[t];
                            if (set.enabled) {
                                var def = RED.nodes.getType(t);
                                if (def && def.color) {
                                    swatch.css({background:RED.utils.getNodeColor(t,def)});
                                    swatch.css({border: "1px solid "+getContrastingBorder(swatch.css('backgroundColor'))})
                                }
                            }
                        }
                        nodeEntries[module].setUseCount[setName] = inUseCount;
                        nodeEntries[module].totalUseCount += inUseCount;

                        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) {
        catalogueLoadStatus.push(err||v);
        if (!err) {
            if (v.modules) {
                var a = false;
                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();
                        return true;
                    }
                    return false;
                })
                loadedList = loadedList.concat(v.modules);
            }
            searchInput.searchBox('count',loadedList.length);
        } 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: catalog}),"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) {
            loadedList = [];
            loadedIndex = {};
            packageList.editableList('empty');

            $(".red-ui-palette-module-shade-status").text(RED._('palette.editor.loading'));
            var catalogues = RED.settings.theme('palette.catalogues')||['https://catalogue.nodered.org/catalogue.json'];
            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;
            catalogues.forEach(function(catalog,index) {
                $.getJSON(catalog, {_: new Date().getTime()},function(v) {
                    handleCatalogResponse(null,catalog,index,v);
                    refreshNodeModuleList();
                }).fail(function(jqxhr, textStatus, error) {
                    console.warn("Error loading catalog",catalog,":",error);
                    handleCatalogResponse(jqxhr,catalog,index);
                }).always(function() {
                    handled++;
                    if (handled === catalogueCount) {
                        searchInput.searchBox('change');
                    }
                })
            });
        }
    }

    function refreshFilteredItems() {
        packageList.editableList('empty');
        var currentFilter = searchInput.searchBox('value').trim();
        if (currentFilter === ""){
            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() {
        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);
                }
            }
        })
    }

    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", style:"position: absolute;top: 35px;bottom: 0;left: 0;right: 0px;"}).appendTo(modulesTab).editableList({
            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,
                        container: container,
                        shade: shade,
                        versionSpan: versionSpan,
                        sets: {}
                    }
                    setButton.on("click", function(evt) {
                        evt.preventDefault();
                        if (container.hasClass('expanded')) {
                            container.removeClass('expanded');
                            contentRow.slideUp();
                        } else {
                            container.addClass('expanded');
                            contentRow.slideDown();
                        }
                    })

                    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 = {};
                        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);
                            $('<span>',{class:"red-ui-palette-module-type-node"}).text(t).appendTo(typeDiv);
                        })
                        var 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}));
                                        }
                                    }
                                });
                            }
                        })

                        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) {
        var installTab = $('<div>',{class:"red-ui-palette-editor-tab hide"}).appendTo(content);

        editorTabs.addTab({
            id: 'install',
            label: RED._('palette.editor.tab-install'),
            content: installTab
        })

        var toolBar = $('<div>',{class:"red-ui-palette-editor-toolbar"}).appendTo(installTab);

        var 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) {
                        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});

                    }
                }
            });

        $('<span>').text(RED._("palette.editor.sort")+' ').appendTo(toolBar);
        var sortGroup = $('<span class="button-group"></span>').appendTo(toolBar);
        var 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);
        var sortAZ = $('<a href="#" class="red-ui-palette-editor-install-sort-option red-ui-sidebar-header-button-toggle" data-i18n="palette.editor.sortAZ"></a>').appendTo(sortGroup);
        var sortRecent = $('<a href="#" class="red-ui-palette-editor-install-sort-option red-ui-sidebar-header-button-toggle" data-i18n="palette.editor.sortRecent"></a>').appendTo(sortGroup);


        var 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(toolBar);
        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>',{style:"position: absolute;top: 79px;bottom: 0;left: 0;right: 0px;"}).appendTo(installTab).editableList({
            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);

                    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(toolBar);
            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");
                                                }
                                            }
                                        ]
                                    });                                }
                            }
                        })
                        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) {
                    spinner.remove();
                     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));
            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)) {
                if (!validateNodeProperty(node, definition, prop, properties[prop])) {
                    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} whether the node proprty is valid
     */
    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;
        }
        if ("required" in definition[property] && definition[property].required) {
            valid = value !== "";
        }
        if (valid && "validate" in definition[property]) {
            try {
                valid = definition[property].validate.call(node,value);
            } catch(err) {
                console.log("Validation error:",node.type,node.id,"property: "+property,"value:",value,err);
            }
        }
        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;
            } else {
                var configNode = RED.nodes.node(value);
                valid = (configNode && (configNode.valid == null || configNode.valid));
            }
        }
        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();
            }
            if (!validateNodeProperty(node, defaults, property,value)) {
                input.addClass("input-error");
            } else {
                input.removeClass("input-error");
            }
        }
    }

    /**
     * Called when the node's properties have changed.
     * Marks the node as dirty and needing a size check.
     * Removes any links to non-existant outputs.
     * @param node - the node that has been updated
     * @param outputMap - (optional) a map of old->new port numbers if wires should be moved
     * @returns {array} the links that were removed due to this update
     */
    function updateNodeProperties(node, outputMap) {
        node.resize = true;
        node.dirty = true;
        node.dirtyStatus = true;
        var removedLinks = [];
        if (outputMap) {
            RED.nodes.eachLink(function(l) {
                if (l.source === node) {
                    if (outputMap.hasOwnProperty(l.sourcePort)) {
                        if (outputMap[l.sourcePort] === "-1") {
                            removedLinks.push(l);
                        } else {
                            l.sourcePort = outputMap[l.sourcePort];
                        }
                    }
                }
            });
        }
        if (node.hasOwnProperty("__outputs")) {
            if (node.outputs < node.__outputs) {
                RED.nodes.eachLink(function(l) {
                    if (l.source === node && l.sourcePort >= node.outputs && removedLinks.indexOf(l) === -1) {
                        removedLinks.push(l);
                    }
                });
            }
            delete node.__outputs;
        }
        node.inputs = Math.min(1,Math.max(0,parseInt(node.inputs)));
        if (isNaN(node.inputs)) {
            node.inputs = 0;
        }
        if (node.inputs === 0) {
            removedLinks = removedLinks.concat(RED.nodes.filterLinks({target:node}));
        }
        for (var l=0;l<removedLinks.length;l++) {
            RED.nodes.removeLink(removedLinks[l]);
        }
        return removedLinks;
    }

    /**
     * Create a config-node select box 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 prepareConfigNodeSelect(node,property,type,prefix,filter) {
        var input = $("#"+prefix+"-"+property);
        if (input.length === 0 ) {
            return;
        }
        var newWidth = input.width();
        var attrStyle = input.attr('style');
        var m;
        if ((m = /(^|\s|;)width\s*:\s*([^;]+)/i.exec(attrStyle)) !== null) {
            newWidth = m[2].trim();
        } else {
            newWidth = "70%";
        }
        var outerWrap = $("<div></div>").css({
            width: newWidth,
            display:'inline-flex'
        });
        var select = $('<select id="'+prefix+'-'+property+'"></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,node[property],prefix,filter);
        $('<a id="'+prefix+'-lookup-'+property+'" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
            .css({"margin-left":"10px"})
            .appendTo(outerWrap);
        $('#'+prefix+'-lookup-'+property).on("click", function(e) {
            showEditConfigNodeDialog(property,type,select.find(":selected").val(),prefix,node);
            e.preventDefault();
        });
        var label = "";
        var configNode = RED.nodes.node(node[property]);
        var node_def = RED.nodes.getType(type);

        if (configNode) {
            label = RED.utils.getNodeLabel(configNode,configNode.id);
        }
        input.val(label);
    }

    /**
     * Create a config-node button for this property
     * @param node - the node being edited
     * @param property - the name of the field
     * @param type - the type of the config-node
     */
    function prepareConfigNodeButton(node,property,type,prefix) {
        var input = $("#"+prefix+"-"+property);
        input.val(node[property]);
        input.attr("type","hidden");

        var button = $("<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) {
        var input = $("#"+prefix+"-"+property);
        if (definition !== undefined && "format" in definition[property] && definition[property].format !== "" && input[0].nodeName === "DIV") {
            $("#"+prefix+"-"+property).on('change keyup', function(event) {
                if (!$(this).attr("skipValidation")) {
                    validateNodeEditor(node,prefix);
                }
            });
        } else {
            $("#"+prefix+"-"+property).on("change", function(event) {
                if (!$(this).attr("skipValidation")) {
                    validateNodeEditor(node,prefix);
                }
            });
        }
    }

    /**
     * Assign the value to each credential field
     * @param node
     * @param credDef
     * @param credData
     * @param prefix
     */
    function populateCredentialsInputs(node, credDef, credData, prefix) {
        var cred;
        for (cred in credDef) {
            if (credDef.hasOwnProperty(cred)) {
                if (credDef[cred].type == 'password') {
                    if (credData[cred]) {
                        $('#' + prefix + '-' + cred).val(credData[cred]);
                    } else if (credData['has_' + cred]) {
                        $('#' + prefix + '-' + cred).val('__PWRD__');
                    }
                    else {
                        $('#' + prefix + '-' + cred).val('');
                    }
                } else {
                    preparePropertyEditor(credData, cred, prefix, credDef);
                }
                attachPropertyChangeHandler(node, credDef, cred, prefix);
            }
        }
    }

    /**
     * Prepare all of the editor dialog fields
     * @param trayBody - the tray body to populate
     * @param nodeEditPanes - array of edit pane ids to add to the dialog
     * @param node - the node being edited
     * @param definition - the node definition
     * @param prefix - the prefix to use in the input element ids (node-input|node-config-input)
     * @param default - the id of the tab to show by default
     */
    function prepareEditDialog(trayBody, nodeEditPanes, node, definition, prefix, defaultTab, done) {
        var finishedBuilding = false;
        var completePrepare = function() {

            var editorTabEl = $('<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 {
                        oldValues[d] = $.extend(true,{},{v:editing_node[d]}).v;
                    }
                }
            }

            try {
                var rc = editing_node._def.oneditsave.call(editing_node);
                if (rc === true) {
                    editState.changed = true;
                }
            } 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;
                        }
                    }
                }
            }
        }
    }

    function defaultConfigNodeSort(A,B) {
        if (A.__label__ < B.__label__) {
            return -1;
        } else if (A.__label__ > B.__label__) {
            return 1;
        }
        return 0;
    }

    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 {

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

                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);
                        }
                    }
                });
                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) {
                    $('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select);
                    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);
                    }
                }

                select.append('<option value="_ADD_"'+(value===""?" selected":"")+'>'+RED._("editor.addNewType", {type:label})+'</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;
        var editing_node = node;
        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
                        }

                        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);
                                    }
                                });
                            }
                            var 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
                                }
                            }
                            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)

                $('<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');
                }
                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 && !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);
                }
            }
        }
        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 = [];

        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)

                $('<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);
                }
                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(RED._("editor.nodesUse", {count:editing_config_node.users.length})).parent().show();
                    }
                    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() {
                    var editState = {
                        changes: {},
                        changed: false,
                        outputMap: null
                    };
                    var configProperty = name;
                    var configId = editing_config_node.id;
                    var configType = type;
                    var configAdding = adding;
                    var configTypeDef = RED.nodes.getType(configType);
                    var d;
                    var input;

                    if (configTypeDef.oneditsave) {
                        try {
                            configTypeDef.oneditsave.call(editing_config_node);
                        } catch(err) {
                            console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
                        }
                    }

                    for (d in configTypeDef.defaults) {
                        if (configTypeDef.defaults.hasOwnProperty(d)) {
                            var newValue;
                            input = $("#node-config-input-"+d);
                            if (input.attr('type') === "checkbox") {
                                newValue = input.prop('checked');
                            } else if ("format" in configTypeDef.defaults[d] && configTypeDef.defaults[d].format !== "" && input[0].nodeName === "DIV") {
                                newValue = input.text();
                            } else {
                                newValue = input.val();
                            }
                            if (newValue != null && newValue !== editing_config_node[d]) {
                                if (editing_config_node._def.defaults[d].type) {
                                    if (newValue == "_ADD_") {
                                        newValue = "";
                                    }
                                    // Change to a related config node
                                    var configNode = RED.nodes.node(editing_config_node[d]);
                                    if (configNode) {
                                        var users = configNode.users;
                                        users.splice(users.indexOf(editing_config_node),1);
                                        RED.events.emit("nodes:change",configNode);
                                    }
                                    configNode = RED.nodes.node(newValue);
                                    if (configNode) {
                                        configNode.users.push(editing_config_node);
                                        RED.events.emit("nodes:change",configNode);
                                    }
                                }
                                editing_config_node[d] = newValue;
                            }
                        }
                    }

                    activeEditPanes.forEach(function(pane) {
                        if (pane.apply) {
                            pane.apply.call(pane, editState);
                        }
                    })

                    editing_config_node.label = configTypeDef.label;

                    var scope = $("#red-ui-editor-config-scope").val();
                    editing_config_node.z = scope;

                    if ($("#node-config-input-node-disabled").prop('checked')) {
                        if (editing_config_node.d !== true) {
                            editing_config_node.d = true;
                        }
                    } else {
                        if (editing_config_node.d === true) {
                            delete editing_config_node.d;
                        }
                    }

                    if (scope) {
                        // Search for nodes that use this one that are no longer
                        // in scope, so must be removed
                        editing_config_node.users = editing_config_node.users.filter(function(n) {
                            var keep = true;
                            for (var d in n._def.defaults) {
                                if (n._def.defaults.hasOwnProperty(d)) {
                                    if (n._def.defaults[d].type === editing_config_node.type &&
                                        n[d] === editing_config_node.id &&
                                        n.z !== scope) {
                                            keep = false;
                                            // Remove the reference to this node
                                            // and revalidate
                                            n[d] = null;
                                            n.dirty = true;
                                            n.changed = true;
                                            validateNode(n);
                                    }
                                }
                            }
                            return keep;
                        });
                    }

                    if (configAdding) {
                        RED.nodes.add(editing_config_node);
                    }

                    validateNode(editing_config_node);
                    var validatedNodes = {};
                    validatedNodes[editing_config_node.id] = true;

                    var userStack = editing_config_node.users.slice();
                    while(userStack.length > 0) {
                        var user = userStack.pop();
                        if (!validatedNodes[user.id]) {
                            validatedNodes[user.id] = true;
                            if (user.users) {
                                userStack = userStack.concat(user.users);
                            }
                            validateNode(user);
                        }
                    }
                    RED.nodes.dirty(true);
                    RED.view.redraw(true);
                    if (!configAdding) {
                        RED.events.emit("editor:save",editing_config_node);
                        RED.events.emit("nodes:change",editing_config_node);
                    }
                    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,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;
                                }
                            });
                        }

                        if (!isSameObj(old_env, new_env)) {
                            editing_node.env = new_env;
                            editState.changes.env = editing_node.env;
                            editState.changed = true;
                        }



                        if (editState.changed) {
                            var wasChanged = editing_node.changed;
                            editing_node.changed = true;
                            validateNode(editing_node);
                            var subflowInstances = [];
                            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;
                                    updateNodeProperties(n);
                                    validateNode(n);
                                }
                            });
                            RED.events.emit("subflows:change",editing_node);
                            RED.nodes.dirty(true);
                            var historyEvent = {
                                t:'edit',
                                node:editing_node,
                                changes:editState.changes,
                                dirty:wasDirty,
                                changed:wasChanged,
                                subflow: {
                                    instances:subflowInstances
                                }
                            };

                            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;
        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);
                            if (workspace.id === RED.workspaces.active()) {
                                $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!workspace.disabled);
                            }
                        }

                        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 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
                })
                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);
        }
    }

    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();
        },
        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) },
        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;
        }
    }
})();
;;(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 (icon !== node.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-btn" 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,
                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!== "";
            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;
        }
        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);
                RED.e = this.editor;
            },
            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',
            value: ""
        });
        if (node.info) {
            nodeInfoEditor.getSession().setValue(node.info, -1);
        }
        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)) {
                    new_env = RED.subflow.exportSubflowInstanceEnv(node);
                }

                // 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;
                        }
                    });
                }
                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);
                $("#red-ui-workspace").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 (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 (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) {
                    var credDefinition = node._def.credentials;
                    var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass);
                    editState.changed = editState.changed || credsChanged;
                }
            }
        }
    });

    /**
     * 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 {boolean} whether anything has changed
     */
    function updateNodeCredentials(node, credDefinition, prefix) {
        var changed = false;
        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') {
                        node.credentials['has_' + cred] = (value !== "");
                        if (value == '__PWRD__') {
                            continue;
                        }
                        changed = true;

                    }
                    node.credentials[cred] = value;
                    if (value != node.credentials._[cred]) {
                        changed = true;
                    }
                }
            }
        }
        return changed;
    }


})();
;(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 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,
                width: "inherit",
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            RED.tray.close();
                        }
                    },
                    {
                        id: "node-dialog-ok",
                        text: RED._("common.label.done"),
                        class: "primary",
                        click: function() {
                            onComplete(JSON.stringify(bufferBinValue));
                            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');
                    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: "",
                        mode:"ace/mode/text"
                    });
                    bufferStringEditor.getSession().setValue(value||"",-1);

                    bufferBinEditor = RED.editor.createEditor({
                        id: 'red-ui-editor-type-buffer-bin',
                        value: "",
                        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(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 = ACE;
    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();
        }
    }

    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 = {};
        }

        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;" />');
            }
            return this.editor.create(options);
        } else {
            return this.editor.create(options);//fallback to ACE
        }
    }
 
    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();
                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';
    var DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
    var DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred'];

    /**
     * Create env var edit interface
     * @param container - container
     * @param node - subflow node
     */
    function buildPropertiesList(envContainer, node) {

        var 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)
                    valueField.typedInput({default:'str',types:isTemplateNode?DEFAULT_ENV_TYPE_LIST:DEFAULT_ENV_TYPE_LIST_INC_CRED});
                    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 === '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";

                        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.ui, nameField, valueField);
                        nameField.trigger('change');
                    }
                },
                sortable: ".red-ui-editableList-item-handle",
                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, 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>').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},
             '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></div>').appendTo(row);
        var inputCellInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
        if (ui.type === "input") {
            inputCellInput.val(ui.opts.types.join(","));
        }
        var checkbox;
        var selectBox;

        inputCellInput.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;
                                    inputCellInput.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;
                                    }
                                    inputCellInput.typedInput('value',Date.now())
                                }
                            }
                        }
                    }
                },
                {
                    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.
                inputCellInput.typedInput('value',ui.opts.types.join(","))
            } else {
                // No other type cares about `value`, but doing this will
                // force a refresh of the label now that `ui.opts` has
                // been updated.
                inputCellInput.typedInput('value',Date.now())
            }

            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) {
            if (ui.type === 'input') {
                var types = inputCellInput.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
        inputCellInput.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 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,
                width: "inherit",
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            RED.tray.close();
                        }
                    },
                    {
                        id: "node-dialog-ok",
                        text: RED._("common.label.done"),
                        class: "primary",
                        click: function() {
                            $("#red-ui-editor-type-expression-help").text("");
                            onComplete(expressionEditor.getValue());
                            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",
                        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}',
                        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 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;
                            });
                        } 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 {
                            var result = expr.evaluate(legacyMode?{msg:parsedData}:parsedData);
                            if (usesContext) {
                                testResultEditor.setValue(RED._("expressionEditor.errors.context-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: "",
                        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 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 changeTimer;

            var trayOptions = {
                title: options.title,
                width: options.width||"inherit",
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            RED.tray.close();
                        }
                    },
                    {
                        id: "node-dialog-ok",
                        text: RED._("common.label.done"),
                        class: "primary",
                        click: function() {
                            onComplete(expressionEditor.getValue(),expressionEditor.getCursorPosition());
                            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 trayBody = tray.find('.red-ui-tray-body');
                    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',
                        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
                        }
                    });
                    if (options.cursor) {
                        expressionEditor.gotoLine(options.cursor.row+1,options.cursor.column,false);
                    }
                    dialogForm.i18n();
                    setTimeout(function() {
                        expressionEditor.focus();
                    },300);
                },
                close: function() {
                    expressionEditor.destroy();
                    if (options.onclose) {
                        options.onclose();
                    }
                },
                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;">'+
                '<button id="node-input-json-reformat" class="red-ui-button red-ui-button-small"><span data-i18n="jsonEditor.format"></span></button>'+
            '</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) {
        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);
        parent.treeList.insertChildAt(newItem, index, true);
        parent.treeList.expand();
    }
    function showObjectMenu(button,item) {
        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);
            }});
            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);
            }});
        }
        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);
            }});
        }
        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);
                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) {
        var result = [];
        for (var prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                result.push(handleItem(prop,obj[prop],depth,parent));
            }
        }
        return result;
    }
    function parseArray(obj,depth,parent) {
        var result = [];
        var l = obj.length;
        for (var i=0;i<l;i++) {
            result.push(handleItem(i,obj[i],depth,parent));
        }
        return result;
    }
    function handleItem(key,val,depth,parent) {
        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")
            }

            keyLabel.on("click", function(evt) {
                if (item.parent.type === 'array') {
                    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);
        } else if (val !== null && item.type === "object") {
            item.expanded = depth < 2;
            item.children = parseObject(val,depth+1,item);
            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);
        valueLabel.on("click", function(evt) {
            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.png"},
                    {value:"object",label:RED._("common.type.object"),hasValue:false,icon:"red/images/typedInput/json.png"}
                ],
                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 (parent) {//red-ui-editor-type-json-editor-item-handle
            $('<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);
        });
        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 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,
                width: options.width||700,
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            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();
                            }
                            if (onComplete) { onComplete(result) }
                            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 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({
                        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: "",
                        mode:"ace/mode/json"
                    });
                    expressionEditor.getSession().setValue(value||"",-1);
                    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);
                                        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: RED._('jsonEditor.rawMode'),
                        content: $("#red-ui-editor-type-json-tab-raw")
                    });
                    tabs.addTab({
                        id: 'json-ui',
                        label: 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() {

    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 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,
                width: options.width||Infinity,
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            RED.tray.close();
                        }
                    },
                    {
                        id: "node-dialog-ok",
                        text: RED._("common.label.done"),
                        class: "primary",
                        click: function() {
                            onComplete(expressionEditor.getValue(),expressionEditor.getCursorPosition());
                            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,
                        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);
                        },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()));
                    }
                    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 (options.cursor) {
                        expressionEditor.gotoLine(options.cursor.row+1,options.cursor.column,false);
                    }

                    dialogForm.i18n();
                },
                close: function() {
                    expressionEditor.destroy();
                    if (options.onclose) {
                        options.onclose();
                    }
                },
                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:" * ", 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;
        }
    }
    RED.editor.registerTypeEditor("_markdown", 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="_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 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 changeTimer;

            var trayOptions = {
                title: options.title,
                width: options.width||"inherit",
                buttons: [
                    {
                        id: "node-dialog-cancel",
                        text: RED._("common.label.cancel"),
                        click: function() {
                            RED.tray.close();
                        }
                    },
                    {
                        id: "node-dialog-ok",
                        text: RED._("common.label.done"),
                        class: "primary",
                        click: function() {
                            onComplete(expressionEditor.getValue(),expressionEditor.getCursorPosition());
                            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");
                    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');
                    expressionEditor = RED.editor.createEditor({
                        id: 'node-input-text',
                        value: "",
                        mode:"ace/mode/"+(options.mode||"text")
                    });
                    expressionEditor.getSession().setValue(value||"",-1);
                    if (options.cursor) {
                        expressionEditor.gotoLine(options.cursor.row+1,options.cursor.column,false);
                    }
                },
                close: function() {
                    expressionEditor.destroy();
                    if (options.onclose) {
                        options.onclose();
                    }
                },
                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.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",
                        cursor: editor.getCursorPosition(),
                        complete: function(v,cursor) {
                            editor.setValue(v, -1);
                            editor.gotoLine(cursor.row+1,cursor.column,false);
                            setTimeout(function() {
                                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 {
                this._destroy();
            } catch (e) { }
            $(el).remove();
            $(toolbarRow).remove();
        }
        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" },
        "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" },
        "constants": {package: "node", module: "constants", path: "node/constants.d.ts" },
        "crypto": {package: "node", module: "crypto", path: "node/crypto.d.ts" },
        "dgram": {package: "node", module: "dgram", path: "node/dgram.d.ts" },
        "dns": {package: "node", module: "dns", path: "node/dns.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" },
        "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" },
        "string_decoder": {package: "node", module: "string_decoder", path: "node/string_decoder.d.ts" },
        "timers": {package: "node", module: "timers", path: "node/timers.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"] ];

    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() {
            let models = window.monaco ? monaco.editor.getModels() : null;
            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 = 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", err)
            }
        }

        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", err)
            }
        }

        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", err)
            }
        }

        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:name}");','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'
                        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":
                    mode = "shell";
                    break;
                //TODO: add other compatability types.
            }
            return mode;
        }

        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 {
            ed._standaloneKeybindingService.addDynamicKeybinding(
                '-editor.action.insertLineAfter', // command ID prefixed by '-'
                null, // keybinding
                () => {} // need to pass an empty handler
            );
        } catch (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()) {
                    m.dispose();
                }
                this.setModel(null);
            } catch (e) { }

            $(el).remove();
            $(toolbarRow).remove();
        }

        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() {
            var aceCompatibleMarkers = [];
            try {
                var _model = ed.getModel();
                if (_model !== null) {
                    var id = _model.getModeId(); // e.g. javascript
                    var ra = _model._associatedResource.authority;  //e.g.  model
                    var rp = _model._associatedResource.path;   //e.g.      /18
                    var rs = _model._associatedResource.scheme; //e.g.      inmemory
                    var modelMarkers = monaco.editor.getModelMarkers(_model) || [];
                    var thisEditorsMarkers = modelMarkers.filter(function (marker) {
                        var _ra = marker.resource.authority;  //e.g.  model
                        var _rp = marker.resource.path;   //e.g.      /18
                        var _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
        if (options.cursor) {
            var row = options.cursor.row || options.cursor.lineNumber;
            var col = options.cursor.column || options.cursor.col;
            ed.gotoLine(row, col);
        }
        if (options.focus) {
            ed.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() {
            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) {
            if(visible) {
                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 || 50);
                }
            } 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();
                    RED.editor.editMarkdown({
                        value: value,
                        width: "Infinity",
                        cursor: ed.getCursorPosition(),
                        complete: function (v, cursor) {
                            ed.setValue(v, -1);
                            ed.gotoLine(cursor.row + 1, cursor.column, false);
                            setTimeout(function () {
                                ed.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
            });
        }

        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);
                    body.find(":focusable:first").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);
                },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");
                    }
                },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 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:text/plain;charset=utf-8"
            });
            navigator.msSaveBlob(blob, file);
        }
        else {
            var element = document.createElement('a');
            element.setAttribute('href', 'data:text/plain;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-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-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"});
                            } 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.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-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" );
                        }
                    }
                ],
                open: function( event, ui ) {
                    RED.keyboard.disable();
                },
                close: function(e) {
                    RED.keyboard.enable();
                    if (popover) {
                        popover.close(true);
                        currentPopoverError = null;
                    }
                }
            });

        dialogContainer = dialog.children(".dialog-form");

        exportNodesDialog =
            '<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="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% - 40px)">'+
                                '<textarea readonly id="red-ui-clipboard-dialog-export-text"></textarea>'+
                            '</div>'+
                            '<div class="form-row" style="text-align: right;">'+
                                '<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-small toggle" href="#" data-i18n="clipboard.export.compact"></a>'+
                                    '<a id="red-ui-clipboard-dialog-export-fmt-full" class="red-ui-button red-ui-button-small toggle" href="#" data-i18n="clipboard.export.formatted"></a>'+
                                '</span>'+
                            '</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);
    }

    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 {
                    if (!/^\[[\s\S]*\]$/m.test(v)) {
                        throw new Error(RED._("clipboard.import.errors.notArray"));
                    }
                    var res = JSON.parse(v);
                    for (var i=0;i<res.length;i++) {
                        if (typeof res[i] !== "object") {
                            throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i}));
                        }
                        if (!res[i].hasOwnProperty('id')) {
                            throw new Error(RED._("clipboard.import.errors.missingId",{index:i}));
                        }
                        if (!res[i].hasOwnProperty('type')) {
                            throw new Error(RED._("clipboard.import.errors.missingType",{index:i}));
                        }
                    }
                    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(mode) {
        if (disabled) {
            return;
        }
        mode = mode || "clipboard";

        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-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-"+mode);
        if (mode === '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: ""
        });
    }

    function showExportNodes(mode) {
        if (disabled) {
            return;
        }

        mode = mode || "clipboard";

        dialogContainer.empty();
        dialogContainer.append($(exportNodesDialog));

        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");

        var 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")
        });

        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";

        $("#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');
                if (format === 'red-ui-clipboard-dialog-export-fmt-full') {
                    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-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.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(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 (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-"+mode);

        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,false),
                    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";
        }
        $("#red-ui-clipboard-hidden").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();
        }
        $("#red-ui-clipboard-hidden").val("");
        if (currentFocus) {
            $(currentFocus).focus();
        }
        return result;
    }


    function importNodes(nodesStr,addFlow) {
        var newNodes = nodesStr;
        if (typeof nodesStr === 'string') {
            try {
                nodesStr = nodesStr.trim();
                if (nodesStr.length === 0) {
                    return;
                }
                newNodes = JSON.parse(nodesStr);
            } catch(err) {
                var 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, isSelected);
        } else {
            element = getNodeLabel(n, isConflicted, isSelected);
        }
        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) {
        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"});
        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);
        // A conflicted flow should not be imported by default.
        return div;
    }

    function getNodeLabel(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-node-list-item"});
        RED.utils.createNodeIcon(n,true).appendTo(div);
        return div;
    }

    return {
        init: function() {
            setupDialogs();

            $('<textarea type="text" id="red-ui-clipboard-hidden" tabIndex="-1">').appendTo("#red-ui-editor");

            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 ($.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) {
                    event.preventDefault();
                }
            })
            .on("dragleave",function(event) {
                hideDropTarget();
            })
            .on("drop",function(event) {
                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) {
                    // 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;
                        }
                        listing.push({
                            library: lib.id,
                            type: options.url,
                            icon: lib.icon || 'fa fa-hdd-o',
                            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;
                        }
                        listing.push({
                            library: lib.id,
                            type: options.url,
                            icon: lib.icon || 'fa fa-hdd-o',
                            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 = $("<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);
        if (options.folderTools) {
            dirList.on('treelistselect', function(event, item) {
                if (item.writable !== false && item.treeList) {
                    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>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>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;
                    }

                    if (!fixed || newOptions.fixed === false) {
                        newTimeout = (newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout)||5000;
                    }
                    if (newOptions.buttons) {
                        var buttonSet = $('<div style="margin-top: 20px;" 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);
        }
        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 index = {};
    var currentResults = [];
    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'];
        if (n._def && n._def.defaults) {
            properties = properties.concat(Object.keys(n._def.defaults));
        }
        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 search(val) {
        var results = [];
        var keys = [];
        var typeFilter;
        var m = /(?:^| )type:([^ ]+)/.exec(val);
        if (m) {
            val = val.replace(/(?:^| )type:[^ ]+/,"");
            typeFilter = m[1];
        }
        var 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);
        // uses:<node-id>
        val = extractValue(val,"uses",flags);

        var hasFlags = Object.keys(flags).length > 0;

        val = val.trim();

        if (val.length > 0 || typeFilter || hasFlags) {
            val = val.toLowerCase();
            var i;
            var j;
            var list = [];
            var nodes = {};
            if (flags.uses) {
                keys = flags.uses;
            } else {
                keys = Object.keys(index);
            }
            for (i=0;i<keys.length;i++) {
                var key = keys[i];
                var kpos = keys[i].indexOf(val);
                if (kpos > -1) {
                    var 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")) {
                            var 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("hidden")) {
                            // Only tabs can be hidden
                            if (node.node.type !== 'tab') {
                                continue
                            }
                            if (!RED.workspaces.isHidden(node.node.id)) {
                                continue
                            }
                        }
                        if (flags.hasOwnProperty("unused")) {
                            var isUnused = (node.node.type === 'subflow' && node.node.instances.length === 0) ||
                                           (isConfigNode && node.node.users.length === 0)
                            if (flags.unused !== isUnused) {
                                continue;
                            }
                        }
                        if (!typeFilter || node.node.type === typeFilter) {
                            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,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 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;
                currentResults = search($(this).val());
                if (currentResults.length > 0) {
                    for (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',{});
                }


            }
        });
        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
                                    }
                                })
                            }
                        }
                    } else {
                        if (currentResults.length > 0) {
                            reveal(currentResults[Math.max(0,selected)].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.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();
                        reveal(node);
                    });
                }
            },
            scrollOnAdd: false
        });

    }

    function reveal(node) {
        hide();
        RED.view.reveal(node.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)
            RED.events.emit("search:open");
            visible = true;
        }
        searchInput.trigger("focus");
    }

    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("search:close");
            if (previousActiveElement) {
                $(previousActiveElement).trigger("focus");
                previousActiveElement = null;
            }
        }
    }

    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 init() {
        RED.actions.add("core:search",show);

        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-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.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
    };

})();
;/**
 * 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();
                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) {
                return A.id.localeCompare(B.id);
            });
            actions.forEach(function(action) {
                action.label = action.id.replace(/:/,": ").replace(/-/g," ").replace(/(^| )./g,function() { return arguments[0].toUpperCase()});
                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() {
        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');
                        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);
                var colour = RED.utils.getNodeColor(object.type,def);
                var icon_url = RED.utils.getNodeIcon(def);
                nodeDiv.css('backgroundColor',colour);

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

                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();
        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 - 150 < 0) {
            opts.y = opts.y - 235;
        }
        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) {
        return !filter ||
            (
                (!filter.type || type === filter.type) &&
                (!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'
        ].filter(function(t) { return applyFilter(opts.filter,t,RED.nodes.getType(t)); });

        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.category !== 'config' && t !== 'unknown' && t !== 'tab') {
                items.push({type:t,def: def, label:getTypeLabel(t,def)});
            }
        });
        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 (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) {
        var pos = {x:50,y: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) {
                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);
            });
            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();
            var subflow = RED.nodes.subflow(RED.workspaces.active());
            if (subflow.instances.length > 0) {
                var msg = $('<div>')
                $('<p>').text(RED._("subflow.subflowInstances",{count: subflow.instances.length})).appendTo(msg);
                $('<p>').text(RED._("subflow.confirmDelete")).appendTo(msg);
                var 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() {
                var startDirty = RED.nodes.dirty();
                var historyEvent = removeSubflow(RED.workspaces.active());
                historyEvent.t = 'delete';
                historyEvent.dirty = startDirty;
                RED.history.push(historyEvent);
            }

        });

        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 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 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,
            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.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 convertToSubflow() {
        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("Cannot create subflow across multiple groups","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].y,
            nodeList[0].x,
            nodeList[0].y];

        for (i=0;i<nodeList.length;i++) {
            n = nodeList[i];
            nodes[n.id] = {n:n,outputs:{}};
            boundingBox = [
                Math.min(boundingBox[0],n.x),
                Math.min(boundingBox[1],n.y),
                Math.max(boundingBox[2],n.x),
                Math.max(boundingBox[3],n.y)
            ]
        }
        var offsetX = snapToGrid(boundingBox[0] - 200);
        var offsetY = snapToGrid(boundingBox[1] - 80);


        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);
        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: RED.nodes.node(wire.id) }
                new_links.push(link);
                RED.nodes.addLink(link);
            });
        });
        subflow.out.forEach(function(output,i) {
            output.wires.forEach(function(wire) {
                var link = {source: RED.nodes.node(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 = RED.nodes.node(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);
    }


    /**
     * Create interface for controlling env var UI definition
     */
    function buildEnvControl(envList,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 = envList.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);
    }


    function buildEnvUIRow(row, tenv, ui, node) {
        ui.label = ui.label||{};
        if ((tenv.type === "cred" || (tenv.parent && tenv.parent.type === "cred")) && !ui.type) {
            ui.type = "cred";
            ui.opts = {};
        } 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;
        }
        switch(ui.type) {
            case "input":
                input = $('<input type="text">').css('width','70%').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)
                } else {
                    input.val(val.value)
                }
                break;
            case "select":
                input = $('<select>').css('width','70%').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">').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%').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%').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;
        }
        if (input) {
            input.attr('id',getSubflowEnvPropertyName(tenv.name))
        }
    }

    /**
     * Create environment variable input UI
     * @param uiContainer - container for UI
     * @param envList - env var definitions of template
     */
    function buildEnvUI(uiContainer, envList, node) {
        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, tenv.ui || {}, 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;
                                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) {
                    var item = {
                        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 {
                        switch(typeof nodeProp) {
                            case "string": nodePropType = "str"; break;
                            case "number": nodePropType = "num"; break;
                            case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break;
                            default:
                            nodePropType = nodeProp.type;
                            nodePropValue = nodeProp.value;
                        }
                    }
                    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) {
        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.val();
                        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;
                }
                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,"_");
    }

    // Called by subflow.oneditprepare for both instances and templates
    function 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: removeSubflow,
        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",
                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",
                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",
                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;
            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", !activateGroup);
            RED.menu.setDisabled("menu-item-group-ungroup", !activateUngroup);
            RED.menu.setDisabled("menu-item-group-merge", !activateMerge);
            RED.menu.setDisabled("menu-item-group-remove", !activateRemove);
            RED.menu.setDisabled("menu-item-edit-copy-group-style", !singleGroupSelected);
            RED.menu.setDisabled("menu-item-edit-paste-group-style", !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.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.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);
            }
        }
    }
    function ungroupSelection() {
        if (RED.view.state() !== RED.state.DEFAULT) { return }
        var selection = RED.view.selection();
        if (selection.nodes) {
            var newSelection = [];
            groups = selection.nodes.filter(function(n) { return n.type === "group" });

            var historyEvent = {
                t:"ungroup",
                groups: [ ],
                dirty: RED.nodes.dirty()
            }
            RED.history.push(historyEvent);


            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);
        }
    }

    function ungroup(g) {
        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 {
                RED.events.emit("nodes:change",n)
            }
        })
        RED.nodes.removeGroup(g);
        return nodes;
    }

    function mergeSelection() {
        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;

            // 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;
                    }
                    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;
                }
                RED.view.select({nodes:[group]})
            }
            historyEvent.events.push({
                t:"createGroup",
                groups: [ group ],
                dirty: RED.nodes.dirty()
            });
            RED.history.push(historyEvent);
            RED.nodes.dirty(true);
        }
    }

    function removeSelection() {
        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})
        }
    }
    function createGroup(nodes) {
        if (nodes.length === 0) {
            return;
        }
        if (nodes.filter(function(n) { return n.type === "subflow" }).length > 0) {
            RED.notify(RED._("group.errors.cannotAddSubflowPorts"),"error");
            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
        }

        group.z = nodes[0].z;
        RED.nodes.addGroup(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 nooes 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. The assumption is they should be
        // wrapped in the newly provided group, and that group added to in their
        // place to the existing containing group.
        if (g) {
            g = RED.nodes.group(g);
            g.nodes.push(group);
            g.dirty = true;
            group.g = g.id;
        }
        // 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 {
                    RED.events.emit("nodes:change",n)
                }
            }
        }
        if (g) {
            RED.events.emit("groups:change",group)
        }
        markDirty(group);
    }
    function removeFromGroup(group, nodes, reparent) {
        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 && group.g) {
                n.g = group.g
                parentGroup.nodes.push(n);
            } else {
                delete n.g;
            }
            if (n.type === 'group') {
                RED.events.emit("groups:change",n)
            } else {
                RED.events.emit("nodes:change",n)
            }
        }
        markDirty(group);
    }

    function getNodes(group,recursive) {
        var nodes = [];
        group.nodes.forEach(function(n) {
            nodes.push(n);
            if (recursive && n.type === 'group') {
                nodes = nodes.concat(getNodes(n,recursive))
            }
        })
        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.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 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)||"flow.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)||"flow_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 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 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 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 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 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-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("flow.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"> <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 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') {
                                        return switchProject(selectedProject.name,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,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}).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 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 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) - 180);
        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 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,
            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>" );
    }

    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)) {
                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) && !modulesInUse.hasOwnProperty(m)) {
                    var installed = !!RED.nodes.registry.getModule(m);
                    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: 'Delete branch',
                                        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.sha+"~1";
                                result.newRev = entry.sha;
                                result.oldRevTitle = 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);

            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);
                }
            }
        })
    }

    return {
        load: loadTour,
        run: run,
        reset: function() {
            RED.settings.set("editor.tours.welcome",'');
        }
    }


})();

:: Command execute ::

Enter:
 
Select:
 

:: Search ::
  - regexp 

:: Upload ::
 
[ Read-Only ]

:: Make Dir ::
 
[ Read-Only ]
:: Make File ::
 
[ Read-Only ]

:: Go Dir ::
 
:: Go File ::
 

--[ c99shell v. 2.5 [PHP 8 Update] [24.05.2025] | Generation time: 0.0516 ]--