UNPKG

node-red-dashboard

Version:
2,311 lines 125 kB
<style>
    .nr-db-sb {
        position: absolute;
        top: 1px;
        bottom: 2px;
        left: 1px;
        right: 1px;
        overflow-y: scroll;
        padding: 10px;
    }
    .nr-db-sb .form-row label {
        display: block;
        width: auto;
    }
    .nr-db-sb .form-row input,
    .nr-db-sb .form-row select {
        width: calc(100% - 100px);
        margin-bottom:0;
    }
    .nr-db-sb .compact {
        margin-bottom: 8px !important;
    }
    .nr-db-sb .red-ui-editableList-container {
        padding: 0;
        min-height: 250px;
        height: auto;
    }
    .nr-db-sb-tab-list {
        min-height: 250px;
        height: auto;
    }
    .nr-db-sb-tab-list li {
        padding: 0;
    }
    .nr-db-sb-tab-list-item {
        border-radius: 4px;
        color: #333;
    }
    .nr-db-sb-list-header {
        cursor: pointer;
        position:relative;
        color: #666;
        padding:3px;
        white-space: nowrap;
    }
    .nr-db-sb-list-header:hover {
        color: #333;
    }
    .nr-db-sb-tab-list-header {
        background: #f3f3f3;
        padding:5px;
    }
    .nr-db-sb-group-list-header:hover,
    .nr-db-sb-widget-list-header:hover {
        background: #f9f9f9;
    }
    .nr-db-sb-list-chevron {
        width: 15px;
        text-align: center;
        margin: 3px 5px 3px 5px;
    }
    .nr-db-sb-tab-list-item .red-ui-editableList-container {
        border-radius: 0;
        border: none;
        height: auto !important;
        min-height: unset;
    }
    .nr-db-sb-list-handle {
        vertical-align: top;
        opacity: 0;
        cursor: move;
    }
    .nr-db-sb-list-header:hover>.nr-db-sb-list-handle,
    .nr-db-sb-list-header:hover>.nr-db-sb-list-header-button-group {
        opacity: 1;
    }
    .nr-db-sb-list-header-button-group {
        opacity: 0;
    }
    .nr-db-sb-list-handle  {
        color: #bbb;
        padding:5px;
    }
    .nr-db-sb-tab-list-header>.nr-db-sb-list-chevron  {
        margin-left: 0px;
        transition: transform 0.2s ease-in-out;
    }
    .nr-db-sb-group-list-header>.nr-db-sb-list-chevron {
        margin-left: 20px;
        transition: transform 0.2s ease-in-out;
    }
    .nr-db-sb-group-list {
        min-height: 10px;
    }
    .nr-db-sb-group-list li {
        border-bottom-color: #eee;
    }
    .nr-db-sb-group-list>li.ui-sortable-helper {
        border-top: 1px solid #eee;
    }
    .nr-db-sb-group-list>li:last-child {
        border-bottom: none;
    }
    .nr-db-sb-widget-list>li {
        border: none !important;
    }
    .nr-db-sb-group-list>li>.red-ui-editableList-item-handle {
        left: 10px;
    }
    .nr-db-sb-list-button-group {
        position: absolute;
        right: 3px;
        top: 0px;
        z-index: 2;
    }
    .nr-db-sb-list-header-button-group {
        position: absolute;
        right: 3px;
        top: 4px;
    }
    .nr-db-sb-list-header-button {
        margin-left: 5px;
    }
    .nr-db-sb li.ui-sortable-helper {
        opacity: 0.9;
    }
    .nr-db-sb-widget-icon {
        margin-left: 56px;
    }
    .nr-db-sb-icon {
        margin-right: 10px;
    }
    .nr-db-sb-link {
        display: inline-block;
        padding-left: 20px;
    }
    .nr-db-sb-link-name-container .fa-external-link {
      margin-right: 10px;
    }
    .nr-db-sb-link-url {
        font-size: 0.8em;
        color: #999;
    }
    span.nr-db-color-pick-container {
        max-width: 50px;
        border-radius: 3px;
        margin-left: 15px;
    }
    input.nr-db-field-themeColor {
        width: 60px !important;
        padding: 0px;
        height: 20px;
        border: none;
        box-shadow: none;
        position: absolute;
        right: 36px;
        border-radius: 3px !important;
        border: solid 1px #ccc;
        -webkit-appearance: none;
        font-size: smaller;
        text-align: center;
    }
    input.nr-db-field-themeColor::-webkit-color-swatch {
        border: none;
    }
    .red-ui-tabs {
        margin-bottom: 15px;
    }
    .red-ui-tab.hidden {
        display: none;;
    }
    #dashboard-tabs-list li a:hover {
        cursor: pointer;
    }
    #dash-link-button {
        background: none;
        border: none;
        margin-top: 3px;
        display: inline-block;
        margin: 3px 0px 0px 3px;
        height: 32px;
        line-height: 29px;
        max-width: 200px;
        overflow: hidden;
        white-space: nowrap;
        position: relative;
        padding: 0px 7px 0px 7px;
    }
    /*#dash-link-button:hover {
        background-color: #eee;
    }*/
    ul.red-ui-dashboard-theme-styles {
        list-style: none;
    }
    ul.red-ui-dashboard-theme-styles li {
        margin-bottom: 10px;
    }
    .nr-db-resetIcon {
        margin: 3px 6px 0px 6px;
        float: right;
        color: grey;
        opacity: 0.8;
        display: block;
    }
    .nr-db-resetIcon:hover {
        cursor: pointer;
    }
    #nr-db-field-font {
        margin-left: 2em;
        width: calc(100% - 81px);
    }
    .nr-db-theme-label {
        font-weight: bold;
    }
    #custom-theme-library-container .btn-group {
        margin-bottom: 10px;
    }
</style>

<script type="text/javascript">
(function($) {
    var editSaveEventHandler;
    var nodesAddEventHandler;
    var nodesRemoveEventHandler;
    var uip = 'ui';
    var attemptedVendorLoad = false;
    var loadTinyColor = function(path) {
        $.ajax({ url: path,
            success: function (data) {
                var jsScript = document.createElement("script");
                jsScript.type = "application/javascript";
                jsScript.src = path;
                document.body.appendChild(jsScript);
                //console.log('Tiny Color Loaded:',path);
            },
            error: function (xhr, ajaxOptions, thrownError) {
                if (xhr.status === 404 && !attemptedVendorLoad) {
                    loadTinyColor('/'+uip+'/vendor/tinycolor2/dist/tinycolor-min.js');
                    attemptedVendorLoad = true;
                }
            }
        });
    }

    // convert to i18 text
    function c_(x) {
        return RED._("node-red-dashboard/ui_base:ui_base."+x);
    }

    // Try to load dist version first
    // then if fails, load non dist version
    loadTinyColor('ui_base/js/tinycolor-min.js');
    //loadTinyColor('ui_base/tinycolor2/dist/tinycolor-min.js');

    RED.nodes.registerType('ui_base',{
            category: 'config',
            defaults: {
                name: {},
                theme: {},
                site: {}
            },
            hasUsers: false,
            paletteLabel: 'Dashboard',
            label: function() { return this.name || 'Node-RED Dashboard'; },
            labelStyle: function() { return this.name?"node_label_italic":""; },
            onpaletteremove: function() {
                RED.sidebar.removeTab("dashboard");
                RED.events.off("editor:save",editSaveEventHandler);
                RED.events.off("nodes:add",nodesAddEventHandler);
                RED.events.off("nodes:remove",nodesRemoveEventHandler);
            },
            onpaletteadd: function() {
                var globalDashboardNode = null;
                var editor;
                var baseStyles = ['base-color'];
                var configurableStyles = ['page-titlebar-backgroundColor', 'page-backgroundColor', 'page-sidebar-backgroundColor',
                'group-textColor', 'group-borderColor', 'group-backgroundColor',
                'widget-textColor', 'widget-backgroundColor','widget-borderColor'];
                var baseFontName = "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif";
                var aTheme = {primary:"indigo", accents:"blue", warn:"red", background:"grey"};
                // tiny colour implementation
                var colours = {
                    leastReadable: function(base, colours) {
                        var least = tinycolor.readability(base, colours[0]);
                        var leastColor = colours[0];
                        for (var i=1; i<colours.length; i++) {
                            var readability = tinycolor.readability(base, colours[i]);
                            if (readability < least) {
                                least = readability;
                                leastColor = colours[i];
                            }
                        }
                        return leastColor;
                    },
                    whiteGreyMostReadable: function (base) {
                        var rgb = tinycolor(base).toRgb();
                        var level = ((rgb.r*299) + (rgb.g*587) + (rgb.b*114))/1000;
                        var readable = (level >= 128) ? '#111111' : '#eeeeee';
                        return readable;
                    },
                    whiteBlackLeastReadable: function(base) {
                        return this.leastReadable(base, ["#000000", "#ffffff"]);
                    },
                    calculate_page_backgroundColor: function(base) {
                        var pageBackground = "#fafafa";
                        var theme = "light";
                        if (globalDashboardNode && globalDashboardNode.hasOwnProperty("theme") && globalDashboardNode.theme.hasOwnProperty("name")) {
                            theme = globalDashboardNode.theme.name.split('-')[1];
                        }
                        if (theme === "dark") {
                            pageBackground = "#111111";
                        }
                        else if (theme === "custom") {
                            var whiteOrBlack = this.whiteBlackLeastReadable(base);
                            if (whiteOrBlack === "#000000") { pageBackground = "#111111"; }
                        }
                        return pageBackground;
                    },
                    calculate_page_sidebar_backgroundColor: function(base) {
                        return this.whiteBlackLeastReadable(base);
                    },
                    calculate_page_titlebar_backgroundColor: function(base) {
                        return base;
                    },
                    calculate_group_textColor: function(base) {
                        var groupTextColour = tinycolor(base).lighten(15).toHexString();
                        //if (this.whiteBlackLeastReadable(base) === "#ffffff") { groupTextColour = "#000000"; }
                        return groupTextColour;
                    },
                    calculate_group_backgroundColor: function(base) {
                        var groupBackground = "#ffffff";
                        var theme = "light";
                        if (globalDashboardNode && globalDashboardNode.hasOwnProperty("theme") && globalDashboardNode.theme.hasOwnProperty("name")) {
                            theme = globalDashboardNode.theme.name.split('-')[1];
                        }
                        if (theme === "dark") {
                            groupBackground = "#333333";
                        }
                        else if (theme === "custom") {
                            var whiteOrBlack = this.whiteBlackLeastReadable(base);
                            if (whiteOrBlack === "#000000") { groupBackground = "#333333"; }
                        }
                        return groupBackground;
                    },
                    calculate_group_borderColor: function(base) {
                        var groupBackground = this.calculate_group_backgroundColor(base);
                        return this.leastReadable(groupBackground, ["#ffffff", "#555555"]);
                    },
                    calculate_widget_textColor: function(base) {
                        //most readable against group background
                        var groupBackground = this.calculate_group_backgroundColor(base);
                        return tinycolor.mostReadable(groupBackground, ["#111111", "#eeeeee"]).toHexString();
                    },
                    calculate_widget_backgroundColor: function(base) {
                        //return tinycolor(base).darken(5).toHexString()
                        return tinycolor(base).toHexString();
                    },
                    calculate_widget_borderColor: function(base) {
                        var widgetBorder = "#ffffff";
                        var theme = "light";
                        if (globalDashboardNode && globalDashboardNode.hasOwnProperty("theme") && globalDashboardNode.theme.hasOwnProperty("name")) {
                            theme = globalDashboardNode.theme.name.split('-')[1];
                        }
                        if (theme === "dark") {
                            widgetBorder = "#333333";
                        }
                        else if (theme === "custom") {
                            var whiteOrBlack = this.whiteBlackLeastReadable(base);
                            if (whiteOrBlack === "#000000") { widgetBorder = "#333333"; }
                        }
                        return widgetBorder;
                    },
                    calculate_base_font: function(base) {
                        return baseFontName;
                    }
                }
                var sizes = {
                    sx: 48, // width of <1> grid square
                    sy: 48, // height of <1> grid square
                    gx: 6, // gap between groups
                    gy: 6, // gap between groups
                    cx: 6, // gap between components
                    cy: 6, // gap between components
                    px: 0, // padding of group's cards
                    py: 0  // padding of group's cards
                };

                function ensureDashboardNode(createMissing) {
                    if (globalDashboardNode !== null) {
                        // Check if it has been deleted beneath us
                        var n = RED.nodes.node(globalDashboardNode.id);
                        if (n === null) { globalDashboardNode = null; }
                    }

                    // Find the old dashboard node
                    if (globalDashboardNode === null) {
                        var bases = [];
                        RED.nodes.eachConfig(function(n) {
                            if (n.type === 'ui_base') { bases.push(n); }
                        });

                        // make sure we only have one ui_base node
                        // at the moment this will just use our existing one - deleting any new base node and theme
                        // at some point we may want to make this an option to select one or the other.
                        while (bases.length > 1) {
                            var n = bases.pop();
                            RED.nodes.remove(n.id);
                            RED.nodes.dirty(true);
                        }

                        if (bases.length === 1) { globalDashboardNode = bases[0]; }

                        // If there is no dashboard node, ensure we create it after
                        // initialising
                        var noDashboardNode = (globalDashboardNode === null);

                        // set up theme state
                        var themeState = {};
                        var baseColor = "#0094CE"
                        for (var i=0; i<baseStyles.length; i++) {
                            themeState[baseStyles[i]] = { default:baseColor, value:baseColor, edited:false };
                        }
                        for (var j = 0; j < configurableStyles.length; j++) {
                            var underscore = configurableStyles[j].split("-").join("_");
                            var colour = colours['calculate_'+underscore](baseColor);
                            themeState[configurableStyles[j]] = {value:colour, edited:false};
                        }
                        themeState["base-font"] = {value:baseFontName};

                        var missingFields = (!globalDashboardNode || !globalDashboardNode.theme || !globalDashboardNode.site || !globalDashboardNode.site.sizes );

                        if (missingFields && createMissing) {
                            var lightTheme = {
                                default: baseColor,
                                baseColor: baseColor,
                                baseFont: baseFontName,
                                edited: false
                            }
                            var darkTheme = {
                                default: "#097479",
                                baseColor: "#097479",
                                baseFont: baseFontName,
                                edited: false
                            }
                            var customTheme = {
                                name: 'Untitled Theme 1',
                                default: "#4B7930",
                                baseColor: "#4B7930",
                                baseFont: baseFontName
                            }
                            var oldThemeName;
                            if (globalDashboardNode && typeof(globalDashboardNode.theme === 'string')) { oldThemeName = globalDashboardNode.theme; }

                            var theme = {
                                name: oldThemeName || "theme-light",
                                lightTheme: lightTheme,
                                darkTheme: darkTheme,
                                customTheme: customTheme,
                                themeState: themeState,
                                angularTheme: aTheme
                            }

                            var site_name = c_("site.title");
                            var site_date_format = c_("site.date-format");
                            var site = { name:site_name, hideToolbar:"false", allowSwipe:"false", lockMenu:"false", allowTempTheme:"true", dateFormat:site_date_format, sizes:sizes };
                            if (globalDashboardNode !== null) {
                                if (typeof globalDashboardNode.site !== "undefined") {
                                    site = {
                                        name: globalDashboardNode.site.name || globalDashboardNode.name,
                                        hideToolbar: globalDashboardNode.site.hideToolbar,
                                        lockMenu: globalDashboardNode.site.lockMenu,
                                        allowSwipe: globalDashboardNode.site.allowSwipe,
                                        allowTempTheme: globalDashboardNode.site.allowTempTheme,
                                        dateFormat: globalDashboardNode.site.dateFormat,
                                        sizes: globalDashboardNode.site.sizes
                                    }
                                }
                                if (globalDashboardNode.theme.hasOwnProperty("angularTheme")) {
                                    aTheme = globalDashboardNode.theme.angularTheme;
                                }
                                else { globalDashboardNode.theme.angularTheme = aTheme; }
                            }

                            if (noDashboardNode) {
                                globalDashboardNode = {
                                    id: RED.nodes.id(),
                                    _def: RED.nodes.getType("ui_base"),
                                    type: "ui_base",
                                    site: site,
                                    theme: theme,
                                    users: []
                                }
                                RED.nodes.add(globalDashboardNode);
                            }
                            else {
                                globalDashboardNode["_def"] = RED.nodes.getType("ui_base");
                                globalDashboardNode.site = site;
                                globalDashboardNode.theme = theme;
                                delete globalDashboardNode.name;
                            }
                            $("#nr-db-field-font").val(baseFontName);
                            RED.nodes.dirty(true);
                        }
                    }
                }

                var content = $("<div>").css({"position":"relative","height":"100%"});
                var mainContent = $("<div>",{class:"nr-db-sb"}).appendTo(content);
                var form = $('<form class="dialog-form">').appendTo(mainContent);

                // Dashboard Tabs markup
                var divTab = $('<div class="red-ui-tabs">').appendTo(form);
                var ulDashboardTabs = $('<ul id="dashboard-tabs-list"></ul>').appendTo(divTab);
                var layout_label = c_("label.layout");
                var site_label = c_("label.site");
                var theme_label = c_("label.theme");
                var angular_label = c_("label.angular");
                var liLayoutTab = $('<li class="red-ui-tab" style="width:70px;"><a class="red-ui-tab-label" title="Layout"><span>'+layout_label+'</span></a></li>').appendTo(ulDashboardTabs);
                var liSiteTab = $('<li class="red-ui-tab" style="width:70px;"><a class="red-ui-tab-label" title="Site" style="width:60px;"><span>'+site_label+'</span></a></li>').appendTo(ulDashboardTabs);
                var liThemeTab = $('<li class="red-ui-tab" style="width:70px;"><a class="red-ui-tab-label" title="Theme" style="width:80px;"><span>'+theme_label+'</span></a></li>').appendTo(ulDashboardTabs);
                var liAngularTab = $('<li class="red-ui-tab" style="width:70px;"><a class="red-ui-tab-label" title="Angular" style="width:80px;"><span>'+angular_label+'</span></a></li>').appendTo(ulDashboardTabs);

                // Link out to dashboard
                $.getJSON('uisettings',function(data) {
                    if (data.hasOwnProperty("path")) { uip = data.path; }
                    var lnk = document.location.host+RED.settings.httpNodeRoot+"/"+uip;
                    var re = new RegExp('\/{1,}','g');
                    lnk = lnk.replace(re,'/');
                    if (!RED.hasOwnProperty("actions")) {
                        RED.keyboard.add("*",/* d */ 68,{ctrl:true, shift:true},function() { window.open(document.location.protocol+"//"+lnk) });
                    }
                    else {
                        RED.keyboard.add("*","ctrl-shift-d","dashboard:show-dashboard");
                        RED.actions.add("dashboard:show-dashboard",function() { window.open(document.location.protocol+"//"+lnk) });
                    }
                    $('<span id="dash-link-button" class="editor-button" style="position:absolute; right:0px;"><i class="fa fa-external-link"></i></span>')
                    .click(function(evt) {
                        window.open(document.location.protocol+"//"+lnk);
                        evt.preventDefault();
                    })
                    .appendTo(ulDashboardTabs);
                });

                // Dashboard Tab containers
                var layoutTab = $('<div id="dashboard-layout" style="height:calc(100% - 48px)">').appendTo(form);
                var siteTab = $('<div id="dashboard-site" style="display:none;">').appendTo(form);
                var themeTab = $('<div id="dashboard-theme" style="display:none;">').appendTo(form);
                var angularTab = $('<div id="dashboard-angular" style="display:none;">').appendTo(form);

                ulDashboardTabs.children().first().addClass("active");

                // Tab logic
                var onTabClick = function() {
                    //Toggle tabs
                    ulDashboardTabs.children().removeClass("active");
                    ulDashboardTabs.children().css({"transition": "width 100ms"});
                    $(this).parent().addClass("active");

                    var selectedTab = $(this)[0].title;
                    if (selectedTab === 'Layout') {
                        themeTab.hide();
                        siteTab.hide();
                        angularTab.hide();
                        layoutTab.show();
                    }
                    else if (selectedTab === 'Angular') {
                        themeTab.hide();
                        siteTab.hide();
                        angularTab.show();
                        layoutTab.hide();
                    }
                    else if (selectedTab === 'Theme') {
                        layoutTab.hide();
                        siteTab.hide();
                        angularTab.hide();
                        themeTab.show();
                        if ($("#nr-db-field-theme option:selected").val() === 'theme-custom') { themeSettingsContainer.show(); }
                        else { themeSettingsContainer.hide(); }
                    }
                    else {
                        layoutTab.hide();
                        themeTab.hide();
                        angularTab.hide();
                        siteTab.show();
                    }
                }

                ulDashboardTabs.find("li.red-ui-tab a").on("click",onTabClick)

                // Site Tab
                var divTitle = $('<div>',{class:"form-row compact"}).appendTo(siteTab);
                $('<div>').html('<b>'+c_("label.title")+'</b>').appendTo(divTitle);
                $('<input type="text" id="nr-db-field-title">').val(site_name).css("width","100%")
                .change(function() {
                    if (!globalDashboardNode || globalDashboardNode.site.name !== $(this).val()) {
                        ensureDashboardNode(true);
                        globalDashboardNode.site.name = $(this).val();
                        RED.nodes.dirty(true);
                    }
                })
                .appendTo(divTitle);

                var divHideToolbar = $('<div>',{class:"form-row compact"}).appendTo(siteTab);
                $('<div>').html('<b>'+c_("label.options")+'</b>').appendTo(divHideToolbar);
                $('<select id="nr-db-field-hideToolbar">')
                .css("width","100%")
                .append($('<option>', { value:"false", text:c_("title-bar.show"), selected:true }))
                .append($('<option>', { value:"true", text:c_("title-bar.hide") }))
                .val("false")
                .change(function() {
                    if (!globalDashboardNode || globalDashboardNode.site.hideToolbar !== $(this).val()) {
                        ensureDashboardNode(true);
                        globalDashboardNode.site.hideToolbar = $(this).val();
                        RED.nodes.dirty(true);
                    }
                })
                .appendTo(divHideToolbar);

                var divLockMenu = $('<div>',{class:"form-row compact"}).appendTo(siteTab);
                $('<select id="nr-db-field-lockMenu">')
                .css("width","100%")
                .append($('<option>', { value:"false", text:c_("lock.clicked"), selected:true }))
                .append($('<option>', { value:"true", text:c_("lock.locked") }))
                .val("false")
                .change(function() {
                    if (!globalDashboardNode || globalDashboardNode.site.lockMenu !== $(this).val()) {
                        ensureDashboardNode(true);
                        globalDashboardNode.site.lockMenu = $(this).val();
                        RED.nodes.dirty(true);
                    }
                })
                .appendTo(divLockMenu);

                var divAllowSwipe = $('<div>',{class:"form-row compact"}).appendTo(siteTab);
                $('<select id="nr-db-field-allowSwipe">')
                .css("width","100%")
                .append($('<option>', { value:"false", text:c_("swipe.no-swipe"), selected:true }))
                .append($('<option>', { value:"true", text:c_("swipe.allow-swipe") }))
                .val("false")
                .change(function() {
                    if (!globalDashboardNode || globalDashboardNode.site.allowSwipe !== $(this).val()) {
                        ensureDashboardNode(true);
                        globalDashboardNode.site.allowSwipe = $(this).val();
                        RED.nodes.dirty(true);
                    }
                })
                .appendTo(divAllowSwipe);

                var divAllowTempTheme = $('<div>',{class:"form-row compact"}).appendTo(siteTab);
                $('<select id="nr-db-field-allowTempTheme">')
                .css("width","100%")
                .append($('<option>', { value:"true", text:c_("temp.allow-theme"), selected:true }))
                .append($('<option>', { value:"false", text:c_("temp.no-theme") }))
                .append($('<option>', { value:"none", text:c_("temp.none") }))
                .val("true")
                .change(function() {
                    if (!globalDashboardNode || globalDashboardNode.site.allowTempTheme !== $(this).val()) {
                        ensureDashboardNode(true);
                        globalDashboardNode.site.allowTempTheme = $(this).val();
                        RED.nodes.dirty(true);
                    }
                    if ($('#nr-db-field-allowTempTheme').val() === "none") {
                        ulDashboardTabs.children().eq(2).addClass("hidden");
                        ulDashboardTabs.children().eq(3).removeClass("hidden");
                    }
                    else {
                        ulDashboardTabs.children().eq(2).removeClass("hidden");
                        ulDashboardTabs.children().eq(3).addClass("hidden");
                    }
                })
                .appendTo(divAllowTempTheme);

                var site_name = c_("site.title");
                var site_date_format = c_("site.date-format");
                var divDateFormat = $('<div>',{class:"form-row"}).appendTo(siteTab);
                $('<div>').html('<b>'+c_("label.date-format")+'</b>')
                .css("width","80%")
                .css("display","inline-block")
                .appendTo(divDateFormat);
                $('<div>').html("<a href='https://momentjs.com/docs/#/displaying/format/' target='_new'><i class='fa fa-info-circle' style='color:grey;'></i></a>")
                .css("display","inline-block")
                .css("margin-right","6px")
                .css("float","right")
                .appendTo(divDateFormat);
                $('<input type="text" id="nr-db-field-dateFormat">').val(site_date_format).css("width","100%")
                .change(function() {
                    if (!globalDashboardNode || globalDashboardNode.site.dateFormat !== $(this).val()) {
                        ensureDashboardNode(true);
                        globalDashboardNode.site.dateFormat = $(this).val();
                        RED.nodes.dirty(true);
                    }
                })
                .appendTo(divDateFormat);

                var divSetSizes = $('<div>',{class:"form-row"}).appendTo(siteTab);
                $('<span style="width:45%; display:inline-block">').html('<b>'+c_("label.sizes")+'</b>').appendTo(divSetSizes);
                $('<span style="width:25%; display:inline-block; font-size:smaller">').html(c_("label.horizontal")).appendTo(divSetSizes);
                $('<span style="width:20%; display:inline-block; font-size:smaller">').html(c_("label.vertical")).appendTo(divSetSizes);
                $('<i id="sizes-reset" class="fa fa-undo nr-db-resetIcon"></i>')
                .css({opacity:1.0})
                .click(function(e) {
                    $("#nr-db-field-sx").val(sizes.sx); globalDashboardNode.site.sizes.sx = sizes.sx;
                    $("#nr-db-field-sy").val(sizes.sy); globalDashboardNode.site.sizes.sy = sizes.sy;
                    $("#nr-db-field-px").val(sizes.px); globalDashboardNode.site.sizes.px = sizes.px;
                    $("#nr-db-field-py").val(sizes.py); globalDashboardNode.site.sizes.py = sizes.py;
                    $("#nr-db-field-gx").val(sizes.gx); globalDashboardNode.site.sizes.gx = sizes.gx;
                    $("#nr-db-field-gy").val(sizes.gy); globalDashboardNode.site.sizes.gy = sizes.gy;
                    $("#nr-db-field-cx").val(sizes.cx); globalDashboardNode.site.sizes.cx = sizes.cx;
                    $("#nr-db-field-cy").val(sizes.cy); globalDashboardNode.site.sizes.cy = sizes.cy;
                    RED.nodes.dirty(true);
                })
                .appendTo(divSetSizes);

                $('<br/><span style="width:45%; display:inline-block">').html(c_("label.widget-size")).appendTo(divSetSizes);
                $('<input type="number" name="sx" min="24" id="nr-db-field-sx">').val(48).css("width","20%")
                .change(function() { ensureDashboardNode(true); globalDashboardNode.site.sizes.sx=Number($(this).val()); RED.nodes.dirty(true); } )
                .appendTo(divSetSizes);
                $('<span style="width:5%; display:inline-block">').text(' ').appendTo(divSetSizes);
                $('<input type="number" name="sy" min="24" id="nr-db-field-sy">').val(48).css("width","20%")
                .change(function() { ensureDashboardNode(true); globalDashboardNode.site.sizes.sy=Number($(this).val()); RED.nodes.dirty(true); } )
                .appendTo(divSetSizes);

                $('<br/><span style="width:45%; display:inline-block">').html(c_("label.widget-spacing")).appendTo(divSetSizes);
                $('<input type="number" name="cx" min="0" id="nr-db-field-cx">').val(6).css("width","20%")
                .change(function() { ensureDashboardNode(true); globalDashboardNode.site.sizes.cx=Number($(this).val()); RED.nodes.dirty(true); } )
                .appendTo(divSetSizes);
                $('<span style="width:5%; display:inline-block">').text(' ').appendTo(divSetSizes);
                $('<input type="number" name="cy" min="0" id="nr-db-field-cy">').val(6).css("width","20%")
                .change(function() { ensureDashboardNode(true); globalDashboardNode.site.sizes.cy=Number($(this).val()); RED.nodes.dirty(true); } )
                .appendTo(divSetSizes);

                $('<br/><span style="width:45%; display:inline-block">').html(c_("label.group-padding")).appendTo(divSetSizes);
                $('<input type="number" name="px" min="0" id="nr-db-field-px">').val(0).css("width","20%")
                .change(function() { ensureDashboardNode(true); globalDashboardNode.site.sizes.px=Number($(this).val()); RED.nodes.dirty(true); } )
                .appendTo(divSetSizes);
                $('<span style="width:5%; display:inline-block">').text(' ').appendTo(divSetSizes);
                $('<input type="number" name="py" min="0" id="nr-db-field-py">').val(0).css("width","20%")
                .change(function() { ensureDashboardNode(true); globalDashboardNode.site.sizes.py=Number($(this).val()); RED.nodes.dirty(true); } )
                .appendTo(divSetSizes);

                $('<br/><span style="width:45%; display:inline-block">').html(c_("label.group-spacing")).appendTo(divSetSizes);
                $('<input type="number" name="gx" min="0" id="nr-db-field-gx">').val(6).css("width","20%")
                .change(function() { ensureDashboardNode(true); globalDashboardNode.site.sizes.gx=Number($(this).val()); RED.nodes.dirty(true); } )
                .appendTo(divSetSizes);
                $('<span style="width:5%; display:inline-block">').text(' ').appendTo(divSetSizes);
                $('<input type="number" name="gy" min="0" id="nr-db-field-gy">').val(6).css("width","20%")
                .change(function() { ensureDashboardNode(true); globalDashboardNode.site.sizes.gy=Number($(this).val()); RED.nodes.dirty(true); } )
                .appendTo(divSetSizes);


                // Angular Theme Tab
                var changed = function() {
                    ensureDashboardNode(true);
                    globalDashboardNode.theme.angularTheme = aTheme;
                    RED.nodes.dirty(true);
                }

                var angColorList = ["red", "pink", "purple", "deep-purple", "indigo", "blue", "light-blue", "cyan", "teal", "green", "light-green", "lime", "yellow", "amber", "orange", "deep-orange", "brown", "grey", "blue-grey"];
                var angColors = "";
                angColorList.forEach(function(c) { angColors += '<option value="' + c + '">' + c + '</option>'; });

                var divPrimStyle = $('<div>',{class:"form-row"}).appendTo(angularTab);
                $('<span style="width:45%; display:inline-block">')
                    .html('<b>'+c_("style.primary")+'</b>')
                    .appendTo(divPrimStyle);
                $('<i id="ang-reset" class="fa fa-undo nr-db-resetIcon"></i>')
                    .css({opacity:1.0})
                    .click(function(e) {
                        $("#nr-db-field-angPrimary").val("indigo"); globalDashboardNode.theme.angularTheme.primary = "indigo";
                        RED.nodes.dirty(true);
                    })
                    .appendTo(divPrimStyle);
                $('<select id="nr-db-field-angPrimary">'+angColors+'</select>')
                    .css("width","100%")
                    .val(aTheme.primary)
                    .change(function() { aTheme.primary = $(this).val(); changed(); })
                    .appendTo(divPrimStyle);

                var divAccStyle = $('<div>',{class:"form-row"}).appendTo(angularTab);
                $('<span style="width:45%; display:inline-block">')
                    .html('<b>'+c_("style.accents")+'</b>')
                    .appendTo(divAccStyle);
                $('<i id="ang-reset" class="fa fa-undo nr-db-resetIcon"></i>')
                    .css({opacity:1.0})
                    .click(function(e) {
                        $("#nr-db-field-angAccents").val("blue"); globalDashboardNode.theme.angularTheme.accents = "blue";
                        RED.nodes.dirty(true);
                    })
                    .appendTo(divAccStyle);
                $('<select id="nr-db-field-angAccents">'+angColors+'</select>')
                    .css("width","100%")
                    .val(aTheme.accents)
                    .change(function() { aTheme.accents = $(this).val(); changed(); })
                    .appendTo(divAccStyle);

                var divWarnStyle = $('<div>',{class:"form-row"}).appendTo(angularTab);
                $('<span style="width:45%; display:inline-block">')
                    .html('<b>'+c_("style.warnings")+'</b>')
                    .appendTo(divWarnStyle);
                $('<i id="ang-reset" class="fa fa-undo nr-db-resetIcon"></i>')
                    .css({opacity:1.0})
                    .click(function(e) {
                        $("#nr-db-field-angWarn").val("red"); globalDashboardNode.theme.angularTheme.warn = "red";
                        RED.nodes.dirty(true);
                    })
                    .appendTo(divWarnStyle);
                $('<select id="nr-db-field-angWarn">'+angColors+'</select>')
                    .css("width","100%")
                    .val(aTheme.warn)
                    .change(function() { aTheme.warn = $(this).val(); changed(); })
                    .appendTo(divWarnStyle);

                var divBackStyle = $('<div>',{class:"form-row"}).appendTo(angularTab);
                $('<span style="width:45%; display:inline-block">')
                    .html('<b>'+c_("style.background")+'</b>')
                    .appendTo(divBackStyle);
                $('<i id="ang-reset" class="fa fa-undo nr-db-resetIcon"></i>')
                    .css({opacity:1.0})
                    .click(function(e) {
                        $("#nr-db-field-angBackground").val("grey"); globalDashboardNode.theme.angularTheme.background = "grey";
                        RED.nodes.dirty(true);
                    })
                    .appendTo(divBackStyle);
                $('<select id="nr-db-field-angBackground">'+angColors+'</select>')
                    .css("width","100%")
                    .val(aTheme.background)
                    .change(function() { aTheme.background = $(this).val(); changed(); })
                    .appendTo(divBackStyle);

                var divPalStyle = $('<div>',{class:"form-row"}).appendTo(angularTab);
                $('<span style="width:45%; display:inline-block">')
                    .html('<b>'+c_("style.palette")+'</b>')
                    .appendTo(divPalStyle);
                var lightdark = '<option value="light">' +c_("style.light")+ '</option>';
                lightdark += '<option value="dark">' +c_("style.dark")+ '</option>';
                $('<select id="nr-db-field-angLook">'+lightdark+'</select>')
                    .css("width","100%")
                    .val(aTheme.palette || "light")
                    .change(function() { aTheme.palette = $(this).val(); changed(); })
                    .appendTo(divPalStyle);

                // Theme Tab
                // For all customisable styles, generate and apply the css
                var generateColours = function(base) {
                    var theme = globalDashboardNode.theme.name.split('-')[1];
                    if (!globalDashboardNode.theme.themeState.hasOwnProperty["base-font"]) {
                        if (globalDashboardNode.theme[theme+"Theme"].baseFont === "Helvetica Neue") {
                            globalDashboardNode.theme[theme+"Theme"].baseFont = baseFontName;
                        }
                        globalDashboardNode.theme.themeState["base-font"] = {value:globalDashboardNode.theme[theme+"Theme"].baseFont};
                        $("#nr-db-field-font").val(globalDashboardNode.theme[theme+"Theme"].baseFont);
                    }
                    for (var i=0; i<configurableStyles.length; i++) {
                        var styleID = configurableStyles[i];
                        var underscore = styleID.split("-").join("_");
                        if (!globalDashboardNode.theme.themeState.hasOwnProperty(styleID)) {
                            globalDashboardNode.theme.themeState[styleID] = {value:"#fff",edited:false};
                        }
                        if (!globalDashboardNode.theme.themeState[styleID].edited || globalDashboardNode.theme[theme+'Theme'].reset) {
                            var colour = colours['calculate_'+underscore](base);
                            globalDashboardNode.theme.themeState[styleID].value = colour;
                        }
                        setColourPickerColour(styleID, globalDashboardNode.theme.themeState[styleID].value, globalDashboardNode.theme.themeState[styleID].edited);
                    }
                    globalDashboardNode.theme[theme+'Theme'].reset = false;
                }

                var divThemeStyle = $('<div>',{class:"form-row"}).appendTo(themeTab);
                $('<label class="nr-db-theme-label">').html(c_("theme.style")).appendTo(divThemeStyle);
                var themeSelection = $('<select id="nr-db-field-theme">'+
                    '<option value="theme-light">'+c_("style.light")+'</option>'+
                    '<option value="theme-dark">'+c_("style.dark")+'</option>'+
                    '<option value="theme-custom">'+c_("style.custom")+'</option>'+
                    '</select>')
                .css("width","100%")
                .change(function() {
                    if (!globalDashboardNode || globalDashboardNode.theme.name !== $(this).val()) {
                        ensureDashboardNode(true);
                        var theme = globalDashboardNode.theme.name.split('-')[1];
                        var baseColour = globalDashboardNode.theme[theme+'Theme'].baseColor;
                        var baseFont = globalDashboardNode.theme[theme+'Theme'].baseFont;
                        globalDashboardNode.theme.name = $(this).val();
                        theme = globalDashboardNode.theme.name.split('-')[1];
                        if (theme !== "custom") {
                            baseColour = globalDashboardNode.theme[theme+'Theme'].default;
                        }
                        else { baseColour = globalDashboardNode.theme[theme+'Theme'].baseColor; }
                        setColourPickerColour("base-color", baseColour);
                        globalDashboardNode.theme.themeState['base-color'].value = baseColour;
                        globalDashboardNode.theme.themeState['base-color'].default = baseColour;
                        globalDashboardNode.theme.themeState['base-font'] = {value:baseFont};
                        $("#nr-db-field-font").val(baseFont);
                        globalDashboardNode.theme[theme+'Theme'].reset = true;
                        //generate colours for all colour settings from base colour
                        generateColours(baseColour);
                        RED.nodes.dirty(true);
                    }
                    $('#base-color-reset').remove();
                    if ($(this).val() === 'theme-custom') {
                        $("#custom-theme-library-container").show(); //TODO undo this at some point
                        $("#custom-theme-settings").show();
                        //addResetButton('base-color', baseSettingsUl.children());
                    }
                    else {
                        $("#custom-theme-library-container").hide();
                        $("#custom-theme-settings").hide();
                        addLightAndDarkResetButton('base-color', baseSettingsUl.children().first());
                    }
                })
                .appendTo(divThemeStyle);

                var customThemeLibraryContainer = $('<div id="custom-theme-library-container">').appendTo(themeTab);
                $('<label class="nr-db-theme-label">').html(c_("theme.custom-profile")).appendTo(customThemeLibraryContainer);
                $('<input type="text" id="ui-sidebar-name" placeholder="profile name (not blank)">')
                .val(c_("theme.custom-profile-name"))
                .change(function() {
                    if (!globalDashboardNode || globalDashboardNode.theme.customTheme.name !== $(this).val()) {
                        ensureDashboardNode(true);
                        globalDashboardNode.theme.customTheme.name = $(this).val();
                        if (editor) {
                            editor.setValue(JSON.stringify({theme:globalDashboardNode.theme.themeState, site:globalDashboardNode.site}),1);
                            RED.nodes.dirty(true);
                        }
                    }
                })
                .keyup(function() {
                    if ($(this).val().length === 0) {
                        $("#custom-theme-library-container div").css("pointer-events","none");
                    }
                    else { $("#custom-theme-library-container div").css("pointer-events","inherit"); }
                })
                .appendTo(customThemeLibraryContainer);
                $('<input type="hidden" id="nr-db-field-format">').appendTo(customThemeLibraryContainer);
                $('<div style="display:none;" class="node-text-editor" id="nr-db-field-format-editor"></div>').appendTo(customThemeLibraryContainer);

                var baseThemeSettingsContainer = $('<div id="base-theme-settings">').appendTo(themeTab);

                var baseSettings = $('<div>',{class:"form-row"}).appendTo(baseThemeSettingsContainer);
                $('<label class="nr-db-theme-label">').html(c_("theme.base-settings")).appendTo(baseSettings);
                var baseSettingsUl = $('<ul id="base-settings-ul" class="red-ui-dashboard-theme-styles"></ul>').appendTo(baseSettings);

                var baseColourItem = $('<li class="red-ui-dashboard-theme-item"><span>'+c_("base.colour")+'</span></li>').appendTo(baseSettingsUl);
                var spanColorContainer = $('<span class="nr-db-color-pick-container"></span>').appendTo(baseColourItem);

                $('<input id="base-color" class="nr-db-field-themeColor" type="color" value="#ffffff"/>')
                .change(function() {
                    ensureDashboardNode(true);
                    var value = $(this).val();
                    var lightThemeMatch = globalDashboardNode.theme.lightTheme.baseColor === value;
                    var darkThemeMatch = globalDashboardNode.theme.darkTheme.baseColor === value;
                    var customThemeMatch = globalDashboardNode.theme.customTheme.baseColor === value;
                    if (!globalDashboardNode || !lightThemeMatch || !darkThemeMatch || !customThemeMatch) {
                        var theme = globalDashboardNode.theme.name.split('-')[1];
                        globalDashboardNode.theme[theme+'Theme'].baseColor = value;
                        if (globalDashboardNode.theme.name === 'theme-light' || globalDashboardNode.theme.name === 'theme-dark') {
                            //for light and dark themes, reset the colours
                            globalDashboardNode.theme[theme+'Theme'].reset = true;
                        }
                        generateColours(value);
                        editor.setValue(JSON.stringify({theme:globalDashboardNode.theme.themeState, site:globalDashboardNode.site}),1);
                        colourPickerChangeHandler($(this).attr('id'), value);
                    }
                })
                .appendTo(spanColorContainer);

                var baseFontItem = $('<li class="red-ui-dashboard-theme-item"><span>'+c_("base.font")+'</span></li>').appendTo(baseSettingsUl);
                var fontSelector = $('<select id="nr-db-field-font">'+
                    '<option value="'+baseFontName+'" style="font-family:'+baseFontName+'">'+c_("font.system")+'</option>'+
                    '<option value="Arial,Arial,Helvetica,sans-serif" style="font-family:Arial,Arial,Helvetica,sans-serif">Arial</option>'+
                    '<option value="Arial Black,Arial Black,Gadget,sans-serif" style="font-family:Arial Black,Arial Black,Gadget,sans-serif">Arial Black</option>'+
                    '<option value="Arial Narrow,Nimbus Sans L,sans-serif" style="font-family:Arial Narrow,Nimbus Sans L,sans-serif">Arial Narrow</option>'+
                    '<option value="Century Gothic,CenturyGothic,AppleGothic,sans-serif" style="font-family:Century Gothic,CenturyGothic,AppleGothic,sans-serif">Century Gothic</option>'+
                    '<option value="Copperplate,Copperplate Gothic Light,fantasy" style="font-family:Copperplate,Copperplate Gothic Light,fantasy;">Copperplate</option>'+
                    '<option value="Courier,monospace" style="font-family:Courier,monospace;">Courier</option>'+
                    '<option value="Georgia,Georgia,serif" style="font-family:Georgia,Georgia,serif">Georgia</option>'+
                    '<option value="Gill Sans,Geneva,sans-serif" style="font-family:Gill Sans,Geneva,sans-serif;">Gill Sans</option>'+
                    //'<option value="Helvetica Neue,Helvetica,sans-serif" style="font-family:Helvetica Neue,Helvetica,sans-serif">Helvetica Neue</option>'+
                    '<option value="Impact,Impact,Charcoal,sans-serif" style="font-family:Impact,Impact,Charcoal,sans-serif">Impact</option>'+
                    '<option value="Lucida Sans Typewriter,Lucida Console,Monaco,monospace" style="font-family:Lucida Console,Monaco,monospace">Lucida Console</option>'+
                    '<option value="Lucida Sans Unicode,Lucida Grande,sans-serif" style="font-family:Lucida Sans Unicode,Lucida Grande,sans-serif">Lucida Sans</option>'+
                    '<option value="Palatino Linotype,Palatino,Book Antiqua,serif" style="font-family:Palatino Linotype,Palatino,Book Antiqua,serif">Palatino Linotype</option>'+
                    '<option value="Tahoma,Geneva,sans-serif" style="font-family:Tahoma,Geneva,sans-serif">Tahoma</optionstyle="font-family:>'+
                    '<option value="Times New Roman,Times,serif" style="font-family:Times New Roman,Times,serif">Times New Roman</option>'+
                    '<option value="Trebuchet MS,Helvetica,sans-serif" style="font-family:Trebuchet MS,Helvetica,sans-serif">Trebuchet MS</option>'+
                    '<option value="Verdana,Verdana,Geneva,sans-serif" style="font-family:Verdana,Verdana,Geneva,sans-serif">Verdana</option>'+
                    '</select>')
                    .change(function() {
                        ensureDashboardNode(true);
                        var theme = globalDashboardNode.theme.name.split('-')[1];
                        globalDashboardNode.theme[theme+'Theme'].baseFont = $(this).val();
                        globalDashboardNode.theme.themeState['base-font'] = {value:$(this).val()};
                        RED.nodes.dirty(true);
                    })
                    .appendTo(baseFontItem);

                var themeSettingsContainer = $('<div id="custom-theme-settings">').appendTo(themeTab);

                // Markup
                // Page styles
                var divPageStyle = $('<div>',{class:"form-row"}).appendTo(themeSettingsContainer);
                $('<label class="nr-db-theme-label">').html(c_("theme.page-settings")).appendTo(divPageStyle);
                var pageStyles = $('<ul class="red-ui-dashboard-theme-styles"></ul>').appendTo(themeSettingsContainer);
                addCustomisableStyle('page-titlebar-backgroundColor', c_("theme.page.title"), pageStyles);
                addCustomisableStyle('page-backgroundColor', c_("theme.page.page"), pageStyles);
                addCustomisableStyle('page-sidebar-backgroundColor', c_("theme.page.side"), pageStyles);

                // Group styles
                var divGroupStyle = $('<div>',{class:"form-row"}).appendTo(themeSettingsContainer);
                $('<label class="nr-db-theme-label">').html(c_("theme.group-settings")).appendTo(divGroupStyle);
                var groupStyles = $('<ul class="red-ui-dashboard-theme-styles"></ul>').appendTo(themeSettingsContainer);
                addCustomisableStyle('group-textColor', c_("theme.group.text"), groupStyles);
                addCustomisableStyle('group-borderColor', c_("theme.group.border"), groupStyles);
                addCustomisableStyle('group-backgroundColor', c_("theme.group.background"), groupStyles);

                // Widget styles
                var divWidgetStyle = $('<div>',{class:"form-row"}).appendTo(themeSettingsContainer);
                $('<label class="nr-db-theme-label">').html(c_("theme.widget-settings")).appendTo(divWidgetStyle);
                var widgetStyles = $('<ul class="red-ui-dashboard-theme-styles"></ul>').appendTo(themeSettingsContainer);
                addCustomisableStyle('widget-textColor', c_("theme.widget.text"), widgetStyles);
                addCustomisableStyle('widget-backgroundColor', c_("theme.widget.colour"), widgetStyles);
                addCustomisableStyle('widget-borderColor', c_("theme.widget.background"), widgetStyles);

                function addCustomisableStyle(id, name, parentUl) {
                    var styleLi = $('<li class="red-ui-dashboard-theme-item"><span>'+name+'</span></li>').appendTo(parentUl);
                    var spanColorContainer = $('<span class="nr-db-color-pick-container"></span>').appendTo(styleLi);
                    $('<input id="'+id+'" class="nr-db-field-themeColor" type="color" value="#ffffff"/>')
                    .change(function() {
                        colourPickerChangeHandler($(this).attr('id'), $(this).val());
                    })
                    .appendTo(spanColorContainer);
                    addResetButton(id, styleLi);
                }

                function colourPickerChangeHandler(id, value) {
                    $("#"+id).css("background-color", value);
                    $("#"+id+"-reset").css({opacity:1});
                    globalDashboardNode.theme.themeState[id].edited = true;
                    globalDashboardNode.theme.themeState[id].value = value;
                    if (editor) {
                        editor.setValue(JSON.stringify({theme:globalDashboardNode.theme.themeState, site:globalDashboardNode.site}),1);
                    }
                    RED.nodes.dirty(true);
                }

                function addResetButton(id, parent) {
                    var resetToDefault = $('<i id="'+id+'-reset" class="fa fa-undo nr-db-resetIcon"></i>')
                    .css({opacity:0.2})
                    .click(function(e) { resetClick(e); })
                    .appendTo(parent);
                }

                function addLightAndDarkResetButton(id, parent) {
                    if ($("#" + id + "-reset").length === 0) {
                        var resetToDefault = $('<i id="'+id+'-reset" class="fa fa-undo nr-db-resetIcon"></i>')
                        .css({opacity:1})
                        .click(function(e) { lightAndDarkResetClick(e); })
                        .appendTo(parent);
                        globalDashboardNode.theme[globalDashboardNode.theme.name.split('-')[1] + 'Theme'].edited = true;
                    }
                }

                function lightAndDarkResetClick(e) {
                    var elementID = e.target.id.split('-reset')[0];
                    var key = globalDashboardNode.theme.name.split('-')[1] + 'Theme';
                    //sanity check - light and dark only allow base-color-reset
                    if (elementID === 'base-color') { // && globalDashboardNode.theme[key].edited) {
                        var defaultColor = globalDashboardNode.theme[key].default;
                        globalDashboardNode.theme[key].reset = true;
                        generateColours(defaultColor);
                        setColourPickerColour(elementID, defaultColor);
                        $("#"+elementID+"-reset").css({opacity:0.2});
                        globalDashboardNode.theme.themeState[elementID].value = defaultColor;
                        globalDashboardNode.theme[key].baseColor = defaultColor;
                        globalDashboardNode.theme[key].edited = false;
                        RED.nodes.dirty(true);
                    }
                }

                function resetClick(e) {
                    //take off -reset
                    var elementID = e.target.id.split('-reset')[0];
                    if (globalDashboardNode.theme.themeState[elementID].edited) {
                        var defaultColor = globalDashboardNode.theme.themeState['base-color'].value;
                        var colour;
                        //set colour
                        if (elementID === 'base-color') {
                            colour = defaultColor;
                            generateColours(colour);
                        }
                        else {
                            var underscore = elementID.split('-').join('_');
                            colour = colours['calculate_'+underscore](defaultColor);
                        }
                        setColourPickerColour(elementID, colour);
                        $("#"+elementID+"-reset").css({opacity:0.2});
                        globalDashboardNode.theme.themeState[elementID].edited = false;
                        globalDashboardNode.theme.themeState[elementID].value = colour;
                        RED.nodes.dirty(true);
                    }
                }

                function setColourPickerColour(id, val, ed) {
                    $("#"+id).val(val);
                    $("#"+id).css("background-color", val);
                    //call mostReadableGreyWhite to set text colour
                    var textColor = colours.whiteGreyMostReadable(val);
                    $("#"+id).css("color", textColor);
                    if (ed === true) { $("#"+id+"-reset").css({opacity:1}); }
                    else { $("#"+id+"-reset").css({opacity:0.2}); }
                }

                //Layout Tab
                var divTabs = $('<div>',{class:"form-row",style:"position:relative"}).appendTo(layoutTab);
                $('<label>').html('<b>'+c_("layout.tab-and-link")+'</b>').appendTo(divTabs);

                var buttonGroup = $('<div>',{class:"nr-db-sb-list-button-group"}).appendTo(divTabs);

                //Toggle expand buttons
                $('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-angle-double-up"></i></a>')
                .click(function(evt) {
                    tabContainer.find(".nr-db-sb-group-list-container").slideUp().addClass('nr-db-sb-collapsed');
                    tabContainer.find(".nr-db-sb-tab-list-header>.nr-db-sb-list-chevron").css({"transform":"rotate(-90deg)"});
                    evt.preventDefault();
                })
                .appendTo(buttonGroup);
                $('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-angle-double-down"></i></a>')
                .click(function(evt) {
                    tabContainer.find(".nr-db-sb-group-list-container").slideDown().removeClass('nr-db-sb-collapsed');
                    tabContainer.find(".nr-db-sb-tab-list-header>.nr-db-sb-list-chevron").css({"transform":""});
                    evt.preventDefault();
                })
                .appendTo(buttonGroup);

                //Add item button
                $('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-plus"></i> '+c_("layout.tab")+'</a>')
                .click(function(evt) {
                    tabContainer.editableList('addItem',{type: 'ui_tab'});
                    evt.preventDefault();
                })
                .appendTo(buttonGroup);
                $('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-plus"></i> '+c_("layout.link")+'</a>')
                .click(function(evt) {
                    tabContainer.editableList('addItem',{type: 'ui_link'});
                    evt.preventDefault();
                })
                .appendTo(buttonGroup);

                var tabLists = {};
                var groupLists = {};
                var linkNodes = {};

                // toggle slide tab group content
                var titleToggle = function (id,content,chevron) {
                    return function(evt) {
                        if (content.is(":visible")) {
                            content.slideUp();
                            chevron.css({"transform":"rotate(-90deg)"});
                            content.addClass('nr-db-sb-collapsed');
                            listStates[id] = false;
                        }
                        else {
                            content.slideDown();
                            chevron.css({"transform":""});
                            content.removeClass('nr-db-sb-collapsed');
                            listStates[id] = true;
                        }
                    };
                }

                var addTabOrLinkItem = function(container,i,item) {
                    // create node if needed
                    if (!item.node) {
                        var defaultItem = {
                            'ui_tab': {
                                _def: RED.nodes.getType('ui_tab'),
                                type: 'ui_tab',
                                users: [],
                                icon: 'dashboard',
                                name: 'Tab'
                            },
                            'ui_link': {
                                _def: RED.nodes.getType('ui_link'),
                                type: 'ui_link',
                                users: [],
                                icon: 'open_in_browser',
                                name: 'Link',
                                target: 'newtab'
                            }
                        }
                        item.node = defaultItem[item.type]
                        item.node.id = RED.nodes.id()
                        item.node.order = i+1
                        item.node.name += ' '+item.node.order
                        listElements[item.node.id] = container;
                        if (item.type === 'ui_tab') {
                            item.groups = [];
                        }
                        RED.nodes.add(item.node);
                        RED.nodes.dirty(true);
                    }
                    else if (item.type === undefined) {
                        item.type = item.node.type
                    }

                    listElements[item.node.id] = container;
                    if (RED.nodes.hasOwnProperty('updateConfigNodeUsers')) {
                        RED.nodes.updateConfigNodeUsers(item.node);
                    }

                    // title
                    var titleRow = $('<div>',{class:"nr-db-sb-list-header nr-db-sb-tab-list-header"}).appendTo(container);
                    switch (item.type) {
                        case 'ui_tab': {
                            container.addClass("nr-db-sb-tab-list-item");
                            $('<i class="nr-db-sb-list-handle nr-db-sb-tab-list-handle fa fa-bars"></i>').appendTo(titleRow);
                            var chevron = $('<i class="fa fa-angle-down nr-db-sb-list-chevron">',{style:"width:10px;"}).appendTo(titleRow);
                            $('<i class="nr-db-sb-icon nr-db-sb-tab-icon fa fa-file-o"></i>').appendTo(titleRow);
                            var title = $('<span class="nr-db-sb-title">').html(item.node.name||"").appendTo(titleRow);
                            break;
                        }
                        case 'ui_link': {
                            $('<i class="nr-db-sb-list-handle fa fa-bars"></i>').appendTo(titleRow);
                            var title = $('<div class="nr-db-sb-link">').appendTo(titleRow);
                            var nameContainer = $('<div class="nr-db-sb-link-name-container">').appendTo(title);
                            $('<i class="fa fa-external-link"></i>').appendTo(nameContainer);
                            $('<span class="nr-db-sb-link-name">').html(item.node.name||"untitled").appendTo(nameContainer);
                            $('<div class="nr-db-sb-link-url">').html(item.node.link||"http://").appendTo(title);
                            break;
                        }
                    }
                    // buttons
                    var buttonGroup = $('<div>',{class:"nr-db-sb-list-header-button-group"}).appendTo(titleRow);
                    if (item.type === 'ui_tab') {
                        var addGroupButton = $('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-plus"></i> '+c_("layout.group")+'</a>').appendTo(buttonGroup);
                    }
                    var editButton = $('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-pencil"></i> '+c_("layout.edit")+'</a>').appendTo(buttonGroup);
                    editButton.on('click',function(evt) {
                        RED.editor.editConfig("", item.type, item.node.id);
                        evt.stopPropagation();
                        evt.preventDefault();
                    });

                    if (item.type === 'ui_tab') {
                        var content = $('<div>',{class:"nr-db-sb-group-list-container"}).appendTo(container);

                        // ui_tab group chevron
                        if (listStates.hasOwnProperty(item.node.id) && !listStates[item.node.id]) {
                            content.hide();
                            chevron.css({"transform":"rotate(-90deg)"});
                            content.addClass('nr-db-sb-collapsed');
                            listStates[item.node.id] = false;
                        }
                        else {
                            listStates[item.node.id] = true;
                        }
                        titleRow.click(titleToggle(item.node.id,content,chevron));

                        // ui_tab group list
                        var ol = $('<ol>',{class:"nr-db-sb-group-list"}).appendTo(content).editableList({
                            sortable:".nr-db-sb-group-list-header",
                            addButton: false,
                            height: 'auto',
                            connectWith: ".nr-db-sb-group-list",
                            addItem: function(container,i,group) {
                                if (!group.node) {
                                    group.node = {
                                        id: RED.nodes.id(),
                                        _def: RED.nodes.getType("ui_group"),
                                        type: "ui_group",
                                        users: [],
                                        tab: item.node.id,
                                        order: i+1,
                                        name: "Group "+(i+1),
                                        width: 6,
                                        disp: true
                                    };
                                    listElements[group.node.id] = container;
                                    RED.nodes.add(group.node);
                                    group.widgets = [];
                                    RED.nodes.dirty(true);
                                    if (RED.nodes.hasOwnProperty('updateConfigNodeUsers')) {
                                        RED.nodes.updateConfigNodeUsers(group.node);
                                    }
                                }
                                var groupNode = group.node;
                                elementParents[groupNode] = item.node.id;
                                var titleRow = $('<div>',{class:"nr-db-sb-list-header nr-db-sb-group-list-header"}).appendTo(container);
                                $('<i class="nr-db-sb-list-handle nr-db-sb-group-list-handle fa fa-bars"></i>').appendTo(titleRow);
                                var chevron = $('<i class="fa fa-angle-down nr-db-sb-list-chevron">',{style:"width:10px;"}).appendTo(titleRow);
                                $('<i class="nr-db-sb-icon nr-db-sb-group-icon fa fa-table"></i>').appendTo(titleRow);
                                var title = $('<span class="nr-db-sb-title">').html(groupNode.name||groupNode.id||"").appendTo(titleRow);
                                listElements[groupNode.id] = container;
                                var buttonGroup = $('<div>',{class:"nr-db-sb-list-header-button-group"}).appendTo(titleRow);
                                var editButton = $('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-pencil"></i> '+c_("layout.edit")+'</a>').appendTo(buttonGroup);
                                var content = $('<div>',{class:"nr-db-sb-widget-list-container"}).appendTo(container);
                                if (!listStates.hasOwnProperty(groupNode.id) || !listStates[groupNode.id]) {
                                    content.hide();
                                    chevron.css({"transform":"rotate(-90deg)"});
                                    content.addClass('nr-db-sb-collapsed');
                                    listStates[groupNode.id] = false;
                                }
                                else {
                                    listStates[groupNode.id] = true;
                                }

                                var ol = $('<ol>',{class:"nr-db-sb-widget-list"}).appendTo(content).editableList({
                                    sortable:".nr-db-sb-widget-list-header",
                                    addButton: false,
                                    height: 'auto',
                                    connectWith: ".nr-db-sb-widget-list",
                                    addItem: function(container,i,widgetNode) {
                                        elementParents[widgetNode.id] = groupNode.id;
                                        var titleRow = $('<div>',{class:"nr-db-sb-list-header nr-db-sb-widget-list-header"}).appendTo(container);
                                        $('<i class="nr-db-sb-list-handle nr-db-sb-widget-list-handle fa fa-bars"></i>').appendTo(titleRow);
                                        $('<i class="nr-db-sb-icon nr-db-sb-widget-icon fa fa-picture-o"></i>').click(function(e) { e.preventDefault(); RED.search.show(widgetNode.id); }).appendTo(titleRow);
                                        var l = widgetNode._def.label;
                                        try {
                                            l = (typeof l === "function" ? l.call(widgetNode) : l)||"";
                                        }
                                        catch(err) {
                                            console.log("Definition error: "+d.type+".label",err);
                                            l = d.type;
                                        }
                                        var title = $('<span class="nr-db-sb-title">').html(l).appendTo(titleRow);
                                        listElements[widgetNode.id] = container;
                                        var buttonGroup = $('<div>',{class:"nr-db-sb-list-header-button-group"}).appendTo(titleRow);
                                        var editButton = $('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-pencil"></i> '+c_("layout.edit")+'</a>').appendTo(buttonGroup);
                                        container.on('mouseover',function() {
                                            widgetNode.highlighted = true;
                                            widgetNode.dirty = true;
                                            RED.view.redraw();
                                        });
                                        container.on('mouseout',function() {
                                            widgetNode.highlighted = false;
                                            widgetNode.dirty = true;
                                            RED.view.redraw();
                                        });
                                        editButton.on('click',function(evt) {
                                            RED.editor.edit(widgetNode);
                                            evt.stopPropagation();
                                            evt.preventDefault();
                                        });
                                    },
                                    sortItems: function(items) {
                                        items.each(function(i,el) {
                                            var node = el.data('data');
                                            var changed = false;
                                            if (node.order !== i+1) {
                                                node.order = i+1;
                                                changed = true;
                                            }
                                            if (node.group !== group.node.id) {
                                                var oldGroupNode = RED.nodes.node(node.group);
                                                if (oldGroupNode) {
                                                    var index = oldGroupNode.users.indexOf(node);
                                                    oldGroupNode.users.splice(index,1);
                                                }
                                                node.group = group.node.id;
                                                group.node.users.push(node);
                                                changed = true;
                                            }
                                            if (changed) {
                                                node.dirty = true;
                                                node.changed = true;
                                            }
                                        })
                                        RED.nodes.dirty(true);
                                        RED.view.redraw();
                                    }
                                });
                                ol.css("min-height","5px");
                                if (groupNode.id) {
                                    groupLists[groupNode.id] = ol;
                                }
                                titleRow.click(titleToggle(groupNode.id,content,chevron));
                                editButton.on('click',function(evt) {
                                    RED.editor.editConfig("", groupNode.type, groupNode.id);
                                    evt.stopPropagation();
                                    evt.preventDefault();
                                });
                                group.widgets.forEach(function(widget) {
                                    ol.editableList('addItem',widget);
                                })
                            },
                            sortItems: function(items) {
                                items.each(function(i,el) {
                                    var groupData = el.data('data');
                                    var node = groupData.node;
                                    var changed = false;
                                    if (node.order !== i+1) {
                                        node.order = i+1;
                                        changed = true;
                                    }
                                    if (changed) {
                                        node.dirty = true;
                                        node.changed = true;
                                    }
                                    if (node.tab !== item.node.id) {
                                        var oldTabNode = RED.nodes.node(node.tab);
                                        if (oldTabNode) {
                                            var index = oldTabNode.users.indexOf(node);
                                            oldTabNode.users.splice(index,1);
                                        }
                                        node.tab = item.node.id;
                                        item.node.users.push(node);
                                        changed = true;
                                    }
                                })
                                RED.nodes.dirty(true);
                                RED.view.redraw();
                            }
                        })
                        tabLists[item.node.id] = ol;

                        addGroupButton.click(function(evt) {
                            ol.editableList('addItem',{});
                            evt.stopPropagation();
                            evt.preventDefault();
                        });
                        item.groups.forEach(function(group) {
                            ol.editableList('addItem',group);
                        });
                    }
                }

                var tabContainer = $('<ol>',{class:"nr-db-sb-tab-list"}).appendTo(divTabs).editableList({
                    sortable:".nr-db-sb-tab-list-header",
                    addButton: false,
                    addItem: addTabOrLinkItem,
                    sortItems: function(items) {
                        items.each(function(i,el) {
                            var itemData = el.data('data');
                            var node = itemData.node;
                            var changed = false;
                            if (node.order !== i+1) {
                                node.order = i+1;
                                changed = true;
                            }
                            if (changed) {
                                node.dirty = true;
                                node.changed = true;
                            }
                        })
                        RED.nodes.dirty(true);
                        RED.view.redraw();
                    }
                });

                var orphanedWidgets = $('<div>',{class:"form-row"}).appendTo(layoutTab);
                $('<span><i class="fa fa-info-circle"></i> There <span id="nr-db-missing-group-count"></span> not in a group. Click <a id="nr-db-add-missing-groups" href="#">here</a> to create the missing groups</span>').appendTo(orphanedWidgets);
                orphanedWidgets.find('a').click(function(event) {
                    var unknownGroups = {};
                    RED.nodes.eachNode(function(node) {
                        if (/^ui_/.test(node.type) && node.type !== 'ui_link' && node.type !== 'ui_toast' && node.type !== 'ui_ui_control') {
                            if (!RED.nodes.node(node.group)) {
                                var g = node.group || "_BLANK_";
                                unknownGroups[g] = unknownGroups[g] || [];
                                unknownGroups[g].push(node);
                            }
                        }
                    });
                    var tab = null;
                    var tabs = tabContainer.editableList('items');
                    tabs.first().each(function(i,el) {
                        var tabData = el.data('data');
                        tab = tabData.node;
                    });

                    if (tab === null) {
                        tab = {
                            id: RED.nodes.id(),
                            _def: RED.nodes.getType("ui_tab"),
                            type: "ui_tab",
                            users: [],
                            order: 0,
                            name: "Tab",
                            icon: "dashboard"
                        };
                        RED.nodes.add(tab);
                    }
                    for (var groupId in unknownGroups) {
                        if (unknownGroups.hasOwnProperty(groupId)) {
                            var groupNode = {
                                id: RED.nodes.id(),
                                _def: RED.nodes.getType("ui_group"),
                                type: "ui_group",
                                users: [],
                                tab: tab.id,
                                order: i+1,
                                name: (groupId==="_BLANK_"?"Group":groupId),
                                width: 6,
                                disp: true
                            };
                            RED.nodes.add(groupNode);
                            RED.editor.validateNode(groupNode);
                            if (RED.nodes.hasOwnProperty('updateConfigNodeUsers')) {
                                RED.nodes.updateConfigNodeUsers(groupNode);
                            }
                            var widgets = unknownGroups[groupId];
                            for (var i=0; i<widgets.length; i++) {
                                widgets[i].group = groupNode.id;
                                widgets[i].changed = true;
                                widgets[i].dirty = true;
                                if (RED.nodes.hasOwnProperty('updateConfigNodeUsers')) {
                                    RED.nodes.updateConfigNodeUsers(widgets[i]);
                                }
                                RED.editor.validateNode(widgets[i]);
                            }
                        }
                    }
                    RED.nodes.dirty(true);
                    refresh();
                    refreshOrphanedWidgets();
                    RED.view.redraw();
                    event.preventDefault();
                });


                var listElements = {};
                var dashboard = [];
                var listStates = {};
                var elementParents = {};
                var awaitingGroups = {};
                var awaitingTabs = {};

                function getCurrentList() {
                    var currentList = [];
                    var tabs = tabContainer.editableList('items');
                    var open = false;
                    tabs.each(function(i,el) {
                        var tabData = el.data('data');
                        var tab = [];
                        var groups = el.find('.nr-db-sb-group-list').editableList('items');
                        groups.each(function(j,el) {
                            var group = [];
                            var groupData = el.data('data');
                            var widgets = el.find('.nr-db-sb-widget-list').editableList('items');
                            widgets.each(function(k,el) {
                                var widgetData = el.data('data');
                                group.push(widgetData.id);
                            })
                            tab.push({id:groupData.node.id, widgets:group});
                        });
                        currentList.push({id:tabData.node.id,groups:tab});
                    });
                    return currentList;
                }
                function refreshOrphanedWidgets() {
                    var unknownGroups = {};
                    var count = 0;
                    RED.nodes.eachNode(function(node) {
                        if (/^ui_/.test(node.type) && node.type !== 'ui_link' && node.type !== 'ui_toast' && node.type !== 'ui_ui_control' && (node.type === 'ui_template' && node.templateScope !== 'global')) {
                            if (!RED.nodes.node(node.group)) {
                                var g = node.group || "_BLANK_";
                                unknownGroups[g] = unknownGroups[g] || [];
                                unknownGroups[g].push(node);
                                count++;
                            }
                        }
                    });

                    if (count > 0) {
                        orphanedWidgets.show();
                        $("#nr-db-missing-group-count").text((count===1?"is ":"are ")+count+" widget"+(count === 1?"":"s"))
                    }
                    else {
                        orphanedWidgets.hide();
                    }
                }

                function refresh() {
                    var currentList = getCurrentList();
                    dashboard = [];
                    var tabs = {};
                    var groups = {};
                    var elements = [];
                    var groupElements = {};
                    var tabGroups = {};
                    var groupId;
                    var group;
                    var tabId;
                    var tab;
                    var unknownGroups = 0;
                    // Find all the tabs and groups
                    RED.nodes.eachConfig(function(node) {
                        switch (node.type) {
                            case 'ui_tab':
                            case 'ui_link': {
                                tabs[node.id] = node;
                                //tabContainer.editableList('addItem',node);
                                break;
                            }
                            case 'ui_group': {
                                groups[node.id] = node;
                                break;
                            }
                        }
                    });
                    for (groupId in groups) {
                        if (groups.hasOwnProperty(groupId)) {
                            group = groups[groupId];
                            if (tabs.hasOwnProperty(group.tab)) {
                                // This group belongs to a tab
                                tabGroups[group.tab] = tabGroups[group.tab]||[];
                                tabGroups[group.tab].push(group);
                            }
                            else {
                                unknownGroups++;
                            }
                        }
                    }
                    // Find all ui widgets - list them by their group id
                    RED.nodes.eachNode(function(node) {
                        if (/^ui_/.test(node.type)) {
                            if (groups.hasOwnProperty(node.group)) {
                                groupElements[node.group] = groupElements[node.group]||[];
                                groupElements[node.group].push(node);
                            }
                            else if ((node.type !== 'ui_toast')&&(node.type !== 'ui_ui_control')&&(node.type === 'ui_template' && node.templateScope !== 'global')) {
                                unknownGroups++;
                            }
                        }
                    });
                    if (unknownGroups > 0) {
                        $("#nr-db-missing-group-count").text((unknownGroups===1?"is ":"are ")+unknownGroups+" widget"+(unknownGroups === 1?"":"s"))
                        orphanedWidgets.show();
                    }
                    else {
                        orphanedWidgets.hide();
                    }
                    // Sort each group's array of widgets
                    for (groupId in groupElements) {
                        if (groupElements.hasOwnProperty(groupId)) {
                            group = groupElements[groupId];
                            groupElements[groupId] = group.map(function(v,i) { return {n:v,i:i} }).sort(function(A,B) {
                                if (A.n.order < B.n.order) { return A.n.order!==0?-1:1;}
                                if (A.n.order > B.n.order) { return B.n.order!==0?1:-1;}
                                return A.i - B.i;
                            }).map(function(v) { return v.n})
                        }
                    }
                    // Sort each tabs's array of groups
                    for (tabId in tabGroups) {
                        if (tabGroups.hasOwnProperty(tabId)) {
                            tab = tabGroups[tabId];
                            tabGroups[tabId] = tab.map(function(v,i) { return {n:v,i:i} }).sort(function(A,B) {
                                if (A.n.order < B.n.order) { return -1;}
                                if (A.n.order > B.n.order) { return 1;}
                                return A.i - B.i;
                            }).map(function(v) { return v.n})
                        }
                    }
                    var tabIds = Object.keys(tabs).map(function(v,i) { return {n:tabs[v],i:i} }).sort(function(A,B) {
                        if (A.n.order < B.n.order) { return -1;}
                        if (A.n.order > B.n.order) { return 1;}
                        return A.i - B.i;
                    }).map(function(v) { return v.n.id});
                    tabIds.forEach(function(tabId) {
                        var tab = {node:tabs[tabId],groups:[]};
                        if (tabGroups[tabId]) {
                            tabGroups[tabId].forEach(function(groupNode) {
                                var group = {node:groupNode,widgets:[]};
                                if (groupElements[groupNode.id]) {
                                    group.widgets = groupElements[groupNode.id];
                                }
                                tab.groups.push(group);
                            });
                        }
                        dashboard.push(tab);
                    });
                    var newList = dashboard.map(function(t) {
                        return {
                            id: t.node.id,
                            groups: t.groups.map(function(g) {
                                return {
                                    id: g.node.id,
                                    widgets: g.widgets.map(function(w) {
                                        return w.id;
                                    })
                                }
                            })
                        }
                    });
                    if (JSON.stringify(newList)!=JSON.stringify(currentList)) {
                        listElements = {};
                        groupLists = {};
                        tabLists = {};
                        tabs = {};
                        groups = {};
                        elementParents = {};
                        tabContainer.empty();
                        dashboard.forEach(function(tab) {
                            tabContainer.editableList('addItem',tab);
                        });
                    }
                    ensureDashboardNode(true);
                    if (globalDashboardNode) {
                        $("#nr-db-field-title").val(globalDashboardNode.site.name);
                        $("#nr-db-field-allowSwipe").val(globalDashboardNode.site.allowSwipe || "false");
                        $("#nr-db-field-allowTempTheme").val(globalDashboardNode.site.allowTempTheme || "true");
                        $("#nr-db-field-hideToolbar").val(globalDashboardNode.site.hideToolbar || "false");
                        $("#nr-db-field-dateFormat").val(globalDashboardNode.site.dateFormat);

                        if (typeof globalDashboardNode.site.sizes !== "object") {
                            globalDashboardNode.site.sizes = sizes;
                        }
                        $("#nr-db-field-sx").val(globalDashboardNode.site.sizes.sx);
                        $("#nr-db-field-sy").val(globalDashboardNode.site.sizes.sy);
                        $("#nr-db-field-px").val(globalDashboardNode.site.sizes.px);
                        $("#nr-db-field-py").val(globalDashboardNode.site.sizes.py);
                        $("#nr-db-field-cx").val(globalDashboardNode.site.sizes.cx);
                        $("#nr-db-field-cy").val(globalDashboardNode.site.sizes.cy);
                        $("#nr-db-field-gx").val(globalDashboardNode.site.sizes.gx);
                        $("#nr-db-field-gy").val(globalDashboardNode.site.sizes.gy);

                        if (typeof globalDashboardNode.theme.angularTheme !== "object") {
                            globalDashboardNode.theme.angularTheme = aTheme;
                        }
                        $("#nr-db-field-angPrimary").val(globalDashboardNode.theme.angularTheme.primary || "indigo");
                        $("#nr-db-field-angAccents").val(globalDashboardNode.theme.angularTheme.accents || "blue");
                        $("#nr-db-field-angWarn").val(globalDashboardNode.theme.angularTheme.warn || "red");
                        $("#nr-db-field-angBackground").val(globalDashboardNode.theme.angularTheme.background || "grey");

                        $("#nr-db-field-theme").val(globalDashboardNode.theme.name);
                        $("#ui-sidebar-name").val(globalDashboardNode.theme.customTheme.name);
                        if (globalDashboardNode.theme.name === 'theme-custom') {
                            $("#custom-theme-library-container").show();
                            $("#custom-theme-settings").show();
                        }
                        else {
                            $("#custom-theme-library-container").hide();
                            $("#custom-theme-settings").hide();
                        }
                        if ($('#nr-db-field-allowTempTheme').val() === "none") {
                            ulDashboardTabs.children().eq(2).addClass("hidden");
                            ulDashboardTabs.children().eq(3).removeClass("hidden");
                        }
                        else {
                            ulDashboardTabs.children().eq(2).removeClass("hidden");
                            ulDashboardTabs.children().eq(3).addClass("hidden");
                        }

                        //set colour start
                        if (typeof globalDashboardNode.theme.name !== "string") {
                            globalDashboardNode.theme.name = "theme-light";
                        }
                        var currentTheme = globalDashboardNode.theme.name.split("-")[1];
                        var startingValue = globalDashboardNode.theme[currentTheme+"Theme"].baseColor;
                        setColourPickerColour("base-color", startingValue);
                        $("#nr-db-field-font").val(globalDashboardNode.theme[currentTheme+"Theme"].baseFont);
                        generateColours(startingValue);
                        if (globalDashboardNode.theme.name === 'theme-light' || globalDashboardNode.theme.name === 'theme-dark') {
                            addLightAndDarkResetButton('base-color', $('#base-settings-ul').children().first());
                        }


                        if (editor === undefined) {
                            editor = RED.editor.createEditor({
                                id: 'nr-db-field-format-editor',
                                mode: 'ace/mode/javascript',
                                value: JSON.stringify({theme:globalDashboardNode.theme.themeState, site:globalDashboardNode.site})
                            });

                            RED.library.create({
                                url:"themes", // where to get the data from
                                type:"theme", // the type of object the library is for
                                editor: editor, // the field name the main text body goes to
                                mode:"ace/mode/javascript",
                                fields:['name'],
                                elementPrefix:"ui-sidebar-"
                            });
                        }

                        editor.on('input', function() {
                            // Check for any changes on the editor object
                            // i.e. has the theme been customised compared
                            // to what is stored
                            var editorObject = JSON.parse(editor.getValue());
                            // var objectsEqual = true;
                            // var keysEqual = true;
                            // var editorKeys = Object.keys(editorObject);
                            // var themeKeys = Object.keys(editorObject.theme);
                            // var siteKeys = Object.keys(editorObject.site);
                            // var dashNodeKeys = Object.keys(globalDashboardNode.theme.themeState);

                            // // Test equality
                            // for (var i=0; i<themeKeys.length; i++) {
                            //     if (themeKeys[i] !== dashNodeKeys[i]) {
                            //         keysEqual = false;
                            //         break;
                            //     }
                            // }
                            // if (keysEqual) {
                            //     for (var j=0; j<themeKeys.length; j++) {
                            //         var key = themeKeys[j];
                            //         if (editorObject.theme[key] !== globalDashboardNode.theme.themeState[key]) {
                            //             objectsEqual = false;
                            //             break;
                            //         }
                            //     }
                            // }
                            //
                            // if (!objectsEqual) {
                            //     globalDashboardNode.theme.themeState = editorObject.theme;
                            // }
                            //
                            // //If custom theme is chosen, update the elements and current state
                            // if (globalDashboardNode.theme.name === 'theme-custom') {
                            //     //globalDashboardNode.theme.customTheme.baseColor = globalDashboardNode.theme.themeState['base-color'].default;
                            //     generateColours(globalDashboardNode.theme.customTheme.baseColor);
                            //     //setColourPickerColour("base-color", globalDashboardNode.theme.customTheme.baseColor);
                            //     if (!objectsEqual) { RED.nodes.dirty(true); }
                            // }

                            //Update theme object if necessary
                            if (JSON.stringify(editorObject.theme) !== JSON.stringify(globalDashboardNode.theme.themeState)) {
                                globalDashboardNode.theme.themeState = editorObject.theme;
                                if ($("#ui-sidebar-name").val() !== globalDashboardNode.theme.customTheme.name) {
                                    globalDashboardNode.theme.customTheme.name = $("#ui-sidebar-name").val();
                                    globalDashboardNode.theme.customTheme.baseColor = globalDashboardNode.theme.themeState["base-color"].value;
                                    setColourPickerColour("base-color", globalDashboardNode.theme.customTheme.baseColor);
                                    generateColours(globalDashboardNode.theme.themeState["base-color"].value);
                                    RED.nodes.dirty(true);
                                }
                            }
                            if (JSON.stringify(aTheme) !== JSON.stringify(globalDashboardNode.theme.angularTheme)) {
                                globalDashboardNode.theme.angularTheme = aTheme;
                            }

                            //Update site object if necessary
                            if (JSON.stringify(editorObject.site) !== JSON.stringify(globalDashboardNode.site)) {
                                globalDashboardNode.site = editorObject.site;
                                $("#nr-db-field-title").val(globalDashboardNode.site.name);
                                $("#nr-db-field-hideToolbar").val(globalDashboardNode.site.hideToolbar);
                                $("#nr-db-field-allowSwipe").val(globalDashboardNode.site.allowSwipe);
                                $("#nr-db-field-allowTempTheme").val(globalDashboardNode.site.allowTempTheme);
                                $("#nr-db-field-dateFormat").val(globalDashboardNode.site.dateFormat);
                                $("#nr-db-field-sx").val(globalDashboardNode.site.sizes.sx);
                                $("#nr-db-field-sy").val(globalDashboardNode.site.sizes.sy);
                                $("#nr-db-field-px").val(globalDashboardNode.site.sizes.px);
                                $("#nr-db-field-py").val(globalDashboardNode.site.sizes.py);
                                $("#nr-db-field-gx").val(globalDashboardNode.site.sizes.gx);
                                $("#nr-db-field-gy").val(globalDashboardNode.site.sizes.gy);
                                $("#nr-db-field-cx").val(globalDashboardNode.site.sizes.cx);
                                $("#nr-db-field-cy").val(globalDashboardNode.site.sizes.cy);
                                RED.nodes.dirty(true);
                            }
                        });
                    }
                    awaitingGroups = {};
                    awaitingTabs = {};
                }

                RED.sidebar.addTab({
                    id: "dashboard",
                    label: c_("label.dashboard"),
                    name: "Dashboard",
                    content: content,
                    closeable: true,
                    pinned: true,
                    iconClass: "fa fa-bar-chart",
                    disableOnEdit: true,
                    onchange: function() { refresh(); }
                });
                editSaveEventHandler = function(node) {
                    if (/^ui_/.test(node.type)) {
                        if (node.type === "ui_tab" || node.type === "ui_group") {
                            if (listElements[node.id]) {
                                // Existing element
                                listElements[node.id].children(".nr-db-sb-list-header").find(".nr-db-sb-title").html(node.name||node.id);
                                if (node.type === "ui_group") {
                                    refresh();
                                }
                            }
                            else if (node.type === "ui_tab") {
                                // Adding a tab
                                tabContainer.editableList('addItem',{node:node,groups:[]})
                            }
                            else {
                                // Adding a group
                                if (tabLists[node.tab]) {
                                    tabLists[node.tab].editableList('addItem',{node:node,widgets:[]})
                                }
                            }
                        }
                        else if (node.type === "ui_link") {
                            if (listElements[node.id]) {
                                var container = listElements[node.id];
                                container.find(".nr-db-sb-link-name").html(node.name||"untitled");
                                container.find(".nr-db-sb-link-url").html(node.link);
                            }
                        }
                        else {
                            refreshOrphanedWidgets();
                            if (listElements[node.id]) {
                                if (node.group != elementParents[node.id]) {
                                    // Moved to a different group
                                    if (groupLists[elementParents[node.id]]) {
                                        groupLists[elementParents[node.id]].editableList('removeItem',listElements[node.id].data('data'))
                                    }
                                    if (groupLists[node.group]) {
                                        groupLists[node.group].editableList('removeItem',node)
                                        groupLists[node.group].editableList('addItem',node);
                                    }
                                }
                                else {
                                    var l = node._def.label;
                                    try {
                                        l = (typeof l === "function" ? l.call(node) : l)||"";
                                    }
                                    catch(err) {
                                        console.log("Definition error: "+d.type+".label",err);
                                        l = d.type;
                                    }
                                    listElements[node.id].children(".nr-db-sb-list-header").find(".nr-db-sb-title").html(l);
                                }
                            }
                            else {
                                if (groupLists[node.group]) {
                                    groupLists[node.group].editableList('addItem',node)
                                }
                            }
                        }
                    }
                };

                RED.events.on("editor:save",editSaveEventHandler);

                var pendingAdd = [];
                var pendingAddTimer = null;

                function handlePendingAdds() {
                    var hasTabs = false;
                    var hasGroups = false;
                    pendingAdd.sort(function(A,B) {
                        hasTabs = hasTabs || A.type === "ui_tab" || B.type === "ui_tab";
                        hasGroups = hasGroups || A.type === "ui_group" || B.type === "ui_group";
                        if (A.type === B.type) {
                            return 0;
                        }
                        if (A.type === "ui_tab") {
                            return -1;
                        }
                        else if (B.type === "ui_tab") {
                            return 1;
                        }
                        else if (A.type === "ui_group") {
                            return -1;
                        }
                        else if (B.type === "ui_group") {
                            return 1;
                        }
                        return 0
                    });
                    for (var i=0; i<pendingAdd.length; i++) {
                        var node = pendingAdd[i];
                        if (node.type === "ui_tab") {
                            tabContainer.editableList('addItem',{node:node,groups:[]});
                        }
                        else {
                            if (hasTabs) {
                                // We've added some tabs, need to give jquery time to add the lists
                                pendingAdd = pendingAdd.slice(i);
                                pendingAddTimer = setTimeout(handlePendingAdds,50);
                                return;
                            }
                            if (node.type === "ui_group") {
                                if (tabLists[node.tab]) {
                                    tabLists[node.tab].editableList('addItem',{node:node,widgets:[]});
                                }
                            }
                            else {
                                if (hasGroups) {
                                    // We've added some tabs, need to give jquery time to add the lists
                                    pendingAdd = pendingAdd.slice(i);
                                    pendingAddTimer = setTimeout(handlePendingAdds,50);
                                    return;
                                }
                                if (groupLists[node.group]) {
                                    groupLists[node.group].editableList('addItem',node)
                                }
                                else {
                                    refreshOrphanedWidgets();
                                }
                            }
                        }
                    }
                    pendingAdd = [];
                }

                nodesAddEventHandler = function(node) {
                    if (/^ui_/.test(node.type) && !listElements[node.id]) {
                        pendingAdd.push(node);
                        clearTimeout(pendingAddTimer);
                        pendingAddTimer = setTimeout(handlePendingAdds,100);
                    }
                };
                RED.events.on("nodes:add", nodesAddEventHandler);

                nodesRemoveEventHandler = function(node) {
                    if (/^ui_/.test(node.type)) {
                        if (node.type === "ui_tab" || node.type === "ui_link") {
                            if (listElements[node.id]) {
                                tabContainer.editableList('removeItem',listElements[node.id].data('data'));
                                delete tabLists[node.id];
                            }
                        }
                        else if (node.type === "ui_group") {
                            if (tabLists[node.tab] && listElements[node.id]) {
                                tabLists[node.tab].editableList('removeItem',listElements[node.id].data('data'));
                            }
                            delete groupLists[node.id];
                        }
                        else {
                            if (groupLists[node.group]) {
                                groupLists[node.group].editableList('removeItem',node)
                            }
                        }
                        refreshOrphanedWidgets();
                        delete listElements[node.id];
                    }
                };

                RED.events.on("nodes:remove", nodesRemoveEventHandler);
            }
        });

    $.widget("nodereddashboard.elementSizerByNum", {
        _create: function() {
            var that = this;
            var has_height = this.options.has_height;
            var pos = this.options.pos;
            var c_width = has_height ? '15%' : '6%';
            var container = $('<div>').css({
                position: 'absolute',
                background: 'white',
                padding: '10px 10px 10px 10px',
                border: '1px solid grey',
                zIndex: '20',
                borderRadius: "4px",
                display:"none",
                width: c_width
            }).appendTo(document.body);
            var box0 = $("<div>").css({
                fontSize: '13px',
                color: '#aaa',
                float: 'left',
                paddingTop: '1px'
            }).appendTo(container);

            var width = $(this.options.width).val();
            var height = has_height ? $(this.options.height).val() : undefined;
            var max_w = '';
            var groupNode = this.options.groupNode;
            if(groupNode) {
                max_w = 'max="'+groupNode.width+'"';
            }
            width = (width > 0) ? width : 1;
            height = (height > 0) ? height : 1;
            var in0 = $('<input type="number" min="1" '+max_w+'>')
                .css("width", has_height ? "45%" : "100%")
                .val(width)
                .appendTo(box0);
            if(has_height) {
                var pad = $('<span>')
                    .html(" x ")
                    .appendTo(box0);
                var in1 = $('<input type="number" min="1">')
                    .css("width", "45%")
                    .val(height)
                    .appendTo(box0);
            }
            var closeTimer;
            var closeFunc = function() {
                var w = in0.val();
                var h = has_height ? in1.val() : undefined;
                var label = that.options.label;
                label.html(w+(has_height ? (' x '+h) : ''));
                $(that.options.width).val(w).change();
                if(has_height) {
                    $(that.options.height).val(h).change();
                }
                that.destroy();
            };
            container.keypress(function(e) {
                if(e.which === 13) { // pressed ENTER
                    container.fadeOut(100, closeFunc);
                }
            });
            container.on('mouseleave', function(e) {
                closeTimer = setTimeout(function() {
                    container.fadeOut(200, closeFunc);
                }, 100);
            });
            container.on('mouseenter', function(e) {
                clearTimeout(closeTimer);
            });
            container.css({
                top: (pos.top -10)+"px",
                left: (pos.left +10)+"px"
            });
            container.fadeIn(200);
        }
    });

    $.widget( "nodereddashboard.elementSizer", {
        _create: function() {
            var that = this;
            var gridWidth = 6;
            var width = parseInt($(this.options.width).val()||0);
            var height = parseInt(this.options.hasOwnProperty('height')?$(this.options.height).val():"1")||0;
            var hasAuto = (!this.options.hasOwnProperty('auto') || this.options.auto);

            this.element.css({
                minWidth: this.element.height()+4
            });
            var auto_text = c_("auto");
            var sizeLabel = (width === 0 && height === 0)?auto_text:width+(this.options.hasOwnProperty('height')?" x "+height:"");
            this.element.html(sizeLabel).on('mousedown',function(evt) {
                evt.stopPropagation();
                evt.preventDefault();

                var width = parseInt($(that.options.width).val()||0);
                var height = parseInt(that.options.hasOwnProperty('height')?$(that.options.height).val():"1")||0;
                var maxWidth = 0;
                var maxHeight;
                var fixedWidth = false;
                var fixedHeight = false;
                var group = $(that.options.group).val();
                if (group) {
                    var groupNode = RED.nodes.node(group);
                    if (groupNode) {
                        gridWidth = Math.max(6,groupNode.width,+width);
                        maxWidth = groupNode.width || gridWidth;
                        fixedWidth = true;
                    }
                    maxHeight = Math.max(6,+height+1);
                }
                else {
                    gridWidth = Math.max(12,+width);
                    maxWidth = gridWidth;
                    maxHeight = 1;
                    fixedHeight = true;
                }

                var pos = $(this).offset();
                var container = $('<div>').css({
                    position: 'absolute',
                    background: 'white',
                    padding: '5px 10px 10px 10px',
                    border: '1px solid grey',
                    zIndex: '20',
                    borderRadius: "4px",
                    display:"none"
                }).appendTo(document.body);
                var closeTimer;

                container.on('mouseleave',function(evt) {
                    closeTimer = setTimeout(function() {
                        container.fadeOut(200, function() { $(this).remove(); });
                    },100)
                });
                container.on('mouseenter',function() {
                    clearTimeout(closeTimer);
                })

                var label = $("<div>").css({
                    fontSize: '13px',
                    color: '#aaa',
                    float: 'left',
                    paddingTop: '1px'
                }).appendTo(container).html((width === 0 && height === 0)?auto_text:(width+(that.options.hasOwnProperty('height')?" x "+height:"")));
                label.hover(function() {
                    $(this).css('text-decoration', 'underline');
                }, function() {
                    $(this).css('text-decoration', 'none');
                });
                label.click(function(e) {
                    var group = $(that.options.group).val();
                    var groupNode = null;
                    if(group) {
                        groupNode = RED.nodes.node(group);
                        if(groupNode === null) {
                            return;
                        }
                    }
                    $(that).elementSizerByNum({
                        width: that.options.width,
                        height: that.options.height,
                        groupNode: groupNode,
                        pos: pos,
                        label: that.element,
                        has_height: that.options.hasOwnProperty('height')
                    });
                    closeTimer = setTimeout(function() {
                        container.fadeOut(200, function() {
                            $(this).remove();
                        });
                    },100)
               });

                var buttonRow = $('<div>',{style:"text-align:right; height:25px;"}).appendTo(container);

                if (hasAuto) {
                    var button = $('<a>',{href:"#",class:"editor-button editor-button-small",style:"margin-bottom:5px"})
                    .html(auto_text)
                    .appendTo(buttonRow)
                    .on('mouseup',function(evt) {
                        that.element.html(auto_text)
                        $(that.options.width).val(0).change();
                        $(that.options.height).val(0).change();
                        evt.preventDefault();
                        container.fadeOut(200, function() { $(this).remove(); });
                    });
                }

                var cellBorder = "1px dashed lightGray";
                var cellBorderExisting = "1px solid gray";
                var cellBorderHighlight = "1px dashed black";
                var rows = [];
                function addRow(i) {
                    var row = $('<div>').css({padding:0,margin:0,height:"25px","box-sizing":"border-box"}).appendTo(container);
                    rows.push(row);
                    cells.push([])
                    for (var j=0; j<gridWidth; j++) {
                        addCell(i,j);
                    }
                }
                function addCell(i,j) {
                    var row = rows[i];
                    var cell = $('<div>').css({
                        display:"inline-block",
                        width: "25px",
                        height: "25px",
                        borderRight: (j===(width-1)&&i<height)?cellBorderExisting:cellBorder,
                        borderBottom: (i===(height-1)&&j<width)?cellBorderExisting:cellBorder,
                        boxSizing: "border-box",
                        cursor:"pointer",
                        background: (j<maxWidth)?"#fff":"#eee"
                    }).appendTo(row);
                    cells[i].push(cell);
                    if (j===0) {
                        cell.css({borderLeft:((i<=height-1)?cellBorderExisting:cellBorder)});
                    }
                    if (i===0) {
                        cell.css({borderTop:((j<=width-1)?cellBorderExisting:cellBorder)});
                    }
                    if (j<maxWidth) {
                        cell.data("w",j);
                        cell.data("h",i);
                        cell.on("mouseup",function() {
                            that.element.html(($(this).data("w")+1)+(that.options.hasOwnProperty('height')?" x "+($(this).data("h")+1):""))
                            $(that.options.width).val($(this).data("w")+1).change();
                            $(that.options.height).val($(this).data("h")+1).change();
                            container.fadeOut(200, function() { $(this).remove(); });
                        });
                        cell.on("mouseover",function() {
                            var w = $(this).data("w");
                            var h = $(this).data("h");
                            label.html((w+1)+(that.options.hasOwnProperty('height')?" x "+(h+1):""));
                            for (var y = 0; y<maxHeight; y++) {
                                for (var x = 0; x<maxWidth; x++) {
                                    cells[y][x].css({
                                        background: (y<=h && x<=w)?'#ddd':'#fff',
                                        borderLeft: (x===0&&y<=h)?cellBorderHighlight:(x===0)?((y<=height-1)?cellBorderExisting:cellBorder):'',
                                        borderTop: (y===0&&x<=w)?cellBorderHighlight:(y===0)?((x<=width-1)?cellBorderExisting:cellBorder):'',
                                        borderRight: (x===w&&y<=h)?cellBorderHighlight:((x===width-1&&y<=height-1)?cellBorderExisting:cellBorder),
                                        borderBottom: (y===h&&x<=w)?cellBorderHighlight:((y===height-1&&x<=width-1)?cellBorderExisting:cellBorder)
                                    })
                                }
                            }
                            if (!fixedHeight && h === maxHeight-1) {
                                addRow(maxHeight++)
                            }
                            if (!fixedWidth && w === maxWidth-1) {
                                maxWidth++;
                                gridWidth++;
                                for (var r=0; r<maxHeight; r++) {
                                    addCell(r,maxWidth-1);
                                }
                            }
                        })
                    }
                }
                var cells = [];
                for (var i=0; i<maxHeight; i++) {
                    addRow(i);
                }
                container.css({
                    top:(pos.top)+"px",
                    left:(pos.left)+"px"
                });
                container.fadeIn(200);
            })
        }
    });
})(jQuery);
</script>

<script type="text/x-red" data-template-name="ui_base">
    <div class='form-row'>
        This <i>ui_base</i> node is the main node that all<br/>other dashboard widget nodes communicate to.<br/>
        <br/>One instance is required to support the dashboard.<br/>
        <br/>If you have no dashboard you can delete this node.<br/>
        It will be re-created automatically if required.<br/>
    </div>
</script>

<script type="text/x-red" data-help-name="ui_base">
</script>