Intial Commit

This commit is contained in:
valki
2020-10-17 18:42:50 +02:00
commit 664c6d8ca3
5892 changed files with 759183 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

View File

@@ -0,0 +1,96 @@
{
"ui_base" : {
"label" : {
"dashboard" : "dashboard",
"title" : "Title",
"options" : "Options",
"date-format" : "Date Format",
"sizes" : "Sizes",
"horizontal" : "Horizontal",
"vertical" : "Vertical",
"widget-size" : "1x1 Widget Size",
"widget-spacing" : "Widget Spacing",
"group-padding" : "Group Padding",
"group-spacing" : "Group Spacing",
"layout" : "Layout",
"angular": "Angular",
"theme" : "Theme",
"site" : "Site"
},
"auto" : "auto",
"title" : "Node-RED Dashboard",
"layout" : {
"tab-and-link" : "Tabs & Links",
"tab" : "tab",
"link" : "link",
"group" : "group",
"edit" : "edit",
"spacer": "spacer",
"layout" : "layout",
"layout-editor" : "Dashboard layout editor",
"width" : "Width"
},
"theme" : {
"style" : "Style",
"custom-profile" : "Custom Profile",
"custom-profile-name" : "Untitled Theme 1",
"base-settings" : "Base Settings",
"page-settings" : "Page Settings",
"page" : {
"title" : "Title Bar Background",
"page" : "Page Background",
"side" : "Side Bar Background"
},
"group-settings" : "Group Settings",
"group" : {
"text" : "Group Text",
"border" : "Group Border",
"background" : "Group Background"
},
"widget-settings" : "Widget Settings",
"widget" : {
"text" : "Widget Text",
"colour" : "Widget Colour",
"background" : "Widget Background"
}
},
"style" : {
"light" : "Light (default)",
"dark" : "Dark",
"custom" : "Custom",
"primary" : "Primary",
"accents" : "Accents",
"background" : "Background",
"warnings" : "Warnings",
"palette": "Light / Dark"
},
"base" : {
"colour" : "Colour",
"font" : "Font"
},
"font" : {
"system" : "System Font (default)"
},
"site" : {
"title" : "Node-RED Dashboard",
"date-format" : "DD/MM/YYYY"
},
"title-bar" : {
"show" : "Show the title bar",
"hide" : "Hide the title bar"
},
"swipe" : {
"no-swipe" : "No swipe between tabs",
"allow-swipe" : "Allow swipe between tabs"
},
"lock" : {
"clicked" : "Click to show side menu",
"locked" : "Always show side menu"
},
"temp" : {
"allow-theme" : "Node-RED theme everywhere",
"no-theme" : "Use Angular theme in ui_template",
"none" : "Angular theme everywhere"
}
}
}

View File

@@ -0,0 +1,14 @@
{
"ui_group" : {
"label" : {
"name" : "Name",
"tab" : "Tab",
"width" : "Width",
"default" : "Default",
"group" : "Group",
"unassigned" : "unassigned"
},
"display-name" : "Display group name",
"collapse-name" : "Allow group to be collapsed"
}
}

View File

@@ -0,0 +1,14 @@
{
"ui_link" : {
"label" : {
"name" : "Name",
"link" : "Link",
"icon" : "Icon",
"open-in" : "Open in",
"new-tab" : "New Tab",
"this-tab" : "This Tab",
"iframe" : "iframe"
},
"tip" : "The <b>Icon</b> field can be either a <a href=\"https://klarsys.github.io/angular-material-icons/\" target=\"_blank\">Material Design icon</a> <i>(e.g. 'check', 'close')</i> or a <a href=\"https://fontawesome.com/v4.7.0/icons/\" target=\"_blank\">Font Awesome icon</a> <i>(e.g. 'fa-fire')</i>, or a <a href=\"https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md\" target=\"_blank\">Weather icon</a> <i>(e.g. 'wi-wu-sunny')</i>.</p><p>You can use the full set of google material icons if you add 'mi-' to the icon name. e.g. 'mi-videogame_asset'.</p>"
}
}

View File

@@ -0,0 +1,21 @@
{
"ui_tab" : {
"label" : {
"home" : "Home",
"tab" : "Tab",
"name" : "Name",
"icon" : "Icon",
"state" : "State",
"navmenu" : "Nav. Menu",
"enabled" : "Enabled",
"disabled" : "Disabled",
"visible" : "Visible",
"hidden" : "Hidden"
},
"info": {
"disabled": " Tab is inactive in Dashboard.",
"hidden": " Tab is not shown in Navigation Menu."
},
"tip" : "The <b>Icon</b> field can be either a <a href=\"https://klarsys.github.io/angular-material-icons/\" target=\"_blank\">Material Design icon</a> <i>(e.g. 'check', 'close')</i> or a <a href=\"https://fontawesome.com/v4.7.0/icons/\" target=\"_blank\">Font Awesome icon</a> <i>(e.g. 'fa-fire')</i>, or a <a href=\"https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md\" target=\"_blank\">Weather icon</a> <i>(e.g. 'wi-wu-sunny')</i>.</p><p>You can use the full set of google material icons if you add 'mi-' to the icon name. e.g. 'mi-videogame_asset'.</p>"
}
}

View File

@@ -0,0 +1,17 @@
{
"ui_template" : {
"label" : {
"category" : "dashboard",
"type" : "Template type",
"local" : "Widget in group",
"global" : "Added to site <head> section",
"group" : "Group",
"size" : "Size",
"name" : "Name",
"pass-through" : "Pass through messages from input.",
"store-state" : "Add output messages to stored state.",
"template" : "Template",
"expand": "Expand"
}
}
}

View File

@@ -0,0 +1,11 @@
{
"ui_ui_control" : {
"label" : {
"category" : "dashboard",
"name" : "Name"
},
"placeholder" : {
"name" : "Name"
}
}
}

View File

@@ -0,0 +1,96 @@
{
"ui_base" : {
"label" : {
"dashboard" : "ダッシュボード",
"title" : "タイトル",
"options" : "オプション",
"date-format" : "日付形式",
"sizes" : "サイズ",
"horizontal" : "横",
"vertical" : "縦",
"widget-size" : "最小Widgetサイズ",
"widget-spacing" : "Widget間隔",
"group-padding" : "グループパディング",
"group-spacing" : "グループ間隔",
"layout" : "配置",
"angular": "Angular",
"theme" : "テーマ",
"site" : "サイト"
},
"auto" : "自動",
"title" : "Node-REDダッシュボード",
"layout" : {
"tab-and-link" : "タブ & リンク",
"tab" : "タブ",
"link" : "リンク",
"group" : "グループ",
"edit" : "編集",
"spacer": "スペーサ",
"layout" : "レイアウト",
"layout-editor" : "ダッシュボードレイアウトエディタ",
"width" : "幅"
},
"theme" : {
"style" : "スタイル",
"custom-profile" : "カスタムプロファイル",
"custom-profile-name" : "名称未設定テーマ 1",
"base-settings" : "基本設定",
"page-settings" : "ページ設定",
"page" : {
"title" : "タイトルバー背景色",
"page" : "ページ背景色",
"side" : "サイドバー背景色"
},
"group-settings" : "グループ設定",
"group" : {
"text" : "グループ文字色",
"border" : "グループボーダー色",
"background" : "グループ背景色"
},
"widget-settings" : "Widget設定",
"widget" : {
"text" : "Widget文字色",
"colour" : "Widget色",
"background" : "Widget背景色"
}
},
"style" : {
"light" : "ライト (デフォルト)",
"dark" : "ダーク",
"custom" : "カスタム",
"primary" : "プライマリ",
"accents" : "アクセント",
"background" : "背景",
"warnings" : "警告",
"palette": "ライト/ダーク"
},
"base" : {
"colour" : "色",
"font" : "フォント"
},
"font" : {
"system" : "システムフォント (デフォルト)"
},
"site" : {
"title" : "Node-RED ダッシュボード",
"date-format" : "YYYY/MM/DD"
},
"title-bar" : {
"show" : "タイトルバー表示",
"hide" : "タイトルバー非表示"
},
"swipe" : {
"no-swipe" : "スワイプによるタブ切り替えをしない",
"allow-swipe" : "スワイプによるタブ切り替えをする"
},
"lock" : {
"clicked" : "サイドメニューをクリックで表示",
"locked" : "サイドメニューを表示したままにする"
},
"temp" : {
"no-theme" : "ui_templateでテーマ設定を許可しない",
"allow-theme" : "ui_templateでテーマ設定を許可する",
"none" : "Angularテーマを全ての箇所で使用"
}
}
}

View File

@@ -0,0 +1,14 @@
{
"ui_group" : {
"label" : {
"name" : "名前",
"tab" : "タブ",
"width" : "幅",
"default" : "デフォルト",
"group" : "グループ",
"unassigned" : "未設定"
},
"display-name" : "グループ名を表示する",
"collapse-name" : "グループの折りたたみを有効にする"
}
}

View File

@@ -0,0 +1,14 @@
{
"ui_link" : {
"label" : {
"name" : "名前",
"link" : "リンク",
"icon" : "アイコン",
"open-in" : "開く方法",
"new-tab" : "新規タブ",
"this-tab" : "このタブ",
"iframe" : "iframe"
},
"tip" : "<b>アイコン</b>フィールドには <a href=\"https://klarsys.github.io/angular-material-icons/\" target=\"_blank\">Material Design icon</a> <i>(例: 'check', 'close')</i>、<a href=\"https://fontawesome.com/v4.7.0/icons/\" target=\"_blank\">Font Awesome icon</a> <i>(例: 'fa-fire')</i>、もしくは <a href=\"https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md\" target=\"_blank\">Weather icon</a> <i>(例: 'wi-wu-sunny')</i>を指定できます。</p>"
}
}

View File

@@ -0,0 +1,21 @@
{
"ui_tab" : {
"label" : {
"home" : "ホーム",
"tab" : "タブ",
"name" : "名前",
"icon" : "アイコン",
"state" : "状態",
"navmenu" : "メニュー",
"enabled" : "有効",
"disabled" : "無効",
"visible" : "表示",
"hidden" : "非表示"
},
"info": {
"disabled": " タブを無効化します",
"hidden": " タブを移動メニューに表示しません"
},
"tip" : "<b>アイコン</b>フィールドには <a href=\"https://klarsys.github.io/angular-material-icons/\" target=\"_blank\">Material Design icon</a> <i>(例: 'check', 'close')</i>、<a href=\"https://fontawesome.com/v4.7.0/icons/\" target=\"_blank\">Font Awesome icon</a> <i>(例: 'fa-fire')</i>、もしくは <a href=\"https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md\" target=\"_blank\">Weather icon</a> <i>(例: 'wi-wu-sunny')</i>を指定できます。</p>"
}
}

View File

@@ -0,0 +1,44 @@
<script type="text/html" data-help-name="ui_template">
<p>Template WidgetにはHTMLコードおよびAngular/Angular-Materialディレクティブを指定できます</p>
<p>このノードで動的なユーザインターフェイス要素を作成し入力によって見た目を変更したりメッセージをNode-REDに送り返したりできます</p>
<p><b>:</b><br>
<pre style="font-size:smaller;">&lt;div layout=&quot;row&quot; layout-align=&quot;space-between&quot;&gt;
&lt;p&gt;数値は&lt;/p&gt;
&lt;p ng-style=&quot;{color: (msg.payload || 0) % 2 === 0 ? 'green' : 'red'}&quot;&gt;
{{(msg.payload || 0) % 2 === 0 ? '偶数' : '奇数'}}
&lt;/p&gt;
&lt;/div&gt;</pre>
このコードは<code>msg.payload</code>で受け取った数値が偶数か奇数かを表示します。同時に、偶数であれば緑に、奇数であれば赤にテキストの色を変更します。<br/>
次は一意なIDをテンプレートに設定デフォルトのテーマカラーを設定入力メッセージの到着を監視する例です</p>
<pre style="font-size:smaller;">
&lt;div id="{{'my_'+$id}}" style="{{'color:'+theme.base_color}}"&gt;何らかのテキスト&lt;/div&gt;
&lt;script&gt;
(function(scope) {
scope.$watch('msg', function(msg) {
if (msg) {
// メッセージ同着時に適当な処理を実行
$("#my_"+scope.$id).html(msg.payload);
}
});
})(scope);
&lt;/script&gt;</pre>
<p>この方法で作成したテンプレートはコピー可能ですコピーはそれぞれ独立して利用できます</p>
<p><b>メッセージ送信:</b><br>
<pre style="font-size:smaller;">
&lt;script&gt;
var value = "こんにちは世界";
// もしくは、コールバック関数で値を書き換え
this.scope.action = function() { return value; }
&lt;/script&gt;
&lt;md-button ng-click=&quot;send({payload:action()})&quot;&gt;
クリックするとこんにちは世界を送信します
&lt;/md-button&gt;</pre>
この例はクリックするとペイロードに<code>'こんにちは世界'</code></p>
<p><b><code>msg.template</code>使:</b><br>
<code>msg.template</code><br>
テンプレートは入力が変化した場合に再ロードされます<br>
HTMLコードフィールドに記述したコードは<code>msg.template</code></p>
<p>以下のアイコンフォントの利用も可能です: <a href="https://klarsys.github.io/angular-material-icons/" target="_blank">Material Design icons</a>,
<a href="https://fontawesome.com/v4.7.0/icons/" target="_blank">Font Awesome icons</a>,
<a href="https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md" target="_blank">Weather icons</a></p>
</script>

View File

@@ -0,0 +1,17 @@
{
"ui_template" : {
"label" : {
"category" : "dashboard",
"type" : "コード種別",
"local" : "グループ内のWidget",
"global" : "<head>ヘッドセクションへ追加",
"group" : "グループ",
"size" : "サイズ",
"name" : "名前",
"pass-through" : "入力メッセージをそのまま渡す",
"store-state" : "出力メッセージを状態として保存",
"template" : "HTMLコード",
"expand": "展開する"
}
}
}

View File

@@ -0,0 +1,15 @@
<script type="text/html" data-help-name="ui_ui_control">
<p>ダッシュボードの動的制御を行います</p>
<p>表示されているタブの切り替えが可能です<code>msg.payload</code>
The default function is to change the currently displayed tab. <code>msg.payload</code><code>{tab:""}</code><b></b><b></b> (0)</p>
<p>空のタブ名("")を送信すると表示されているページを更新しますまた"+1"を送ると次のタブ"-1"を送ると前のタブに切り替えられます</p>
<p>widgetグループを表示を次のようなペイロードで制御できます<br/>
<code>{group:{hide:["タブ名_グループ名"], show:["他_グループ_表示"], focus:true}}</code><br/>
<b>focus</b>は、グループが表示されるよう必要に応じて画面をスクロールするためのパラメータで、省略可能です。グループの指定は<i>タブ名</i><i>グループ名</i>(_)</p>
<p>クライアントのブラウザが接続もしくは切断した場合次のプロパティを持つメッセージを送信します:</p>
<ul>
<li><code>payload</code> - <i>connect</i>もしくは<i>lost</i>,
<li><code>socketid</code> - ID(),
<li><code>socketip</code> - IP
</ul>
</script>

View File

@@ -0,0 +1,11 @@
{
"ui_ui_control" : {
"label" : {
"category" : "dashboard",
"name" : "名前"
},
"placeholder" : {
"name" : "名前"
}
}
}

View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<script type="text/javascript">
(function() {
var myvoice = 0;
var voices;
RED.nodes.registerType('ui_audio',{
category: 'dashboard',
paletteLabel: 'audio out',
color: 'rgb(119, 198, 204)',
defaults: {
name: {value:""},
group: {type: 'ui_group', required: true},
voice: {value:""},
always: {value:""}
},
inputs:1,
outputs:0,
icon: "feed.png",
align: "right",
label: function() { return this.name||"audio out"; },
labelStyle: function() { return this.name?"node_label_italic":""; },
onpaletteadd: function() {
if ('speechSynthesis' in window) { voices = window.speechSynthesis.getVoices(); }
},
oneditprepare: function() {
if ('speechSynthesis' in window) {
voices = window.speechSynthesis.getVoices();
for (i = 0; i < voices.length ; i++) {
//console.log(i,voices[i].name,voices[i].lang,voices[i].voiceURI,voices[i].default);
var option = document.createElement('option');
option.textContent = i + " : " + voices[i].name + ' (' + voices[i].lang + ')';
if (voices[i].default) { option.textContent += ' -- DEFAULT'; }
option.setAttribute('value', voices[i].lang);
document.getElementById("node-input-voice").appendChild(option);
}
$('#node-input-voice').val(this.voice || 0);
}
else {
$('#voice-input-row').hide();
}
$("#node-input-voice").on("change", function() {
myvoice = this.voice = $("#node-input-voice").val();
});
}
});
})();
</script>
<script type="text/html" data-template-name="ui_audio">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row" id="voice-input-row">
<label for="node-input-voice"><i class="fa fa-language"></i> TTS Voice</label>
<select id="node-input-voice" style="width:70%"></select>
</div>
<div class="form-row">
<label for="node-input-always"></label>
<input type="checkbox" checked id="node-input-always" style="display:inline-block; width:auto; vertical-align:top;">
Play audio when window not in focus.
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/html" data-help-name="ui_audio">
<p>Plays audio or text to speech (TTS) in the dashboard.</p>
<p>To work the dashboard web page must be open.</p>
<p>Expects <code>msg.payload</code> to contain a buffer of a <b>wav</b> or <b>mp3</b> file.</p>
<p>If your browser has native support for Text-to-Speech then a <code>msg.payload</code>
can also be a <b>string</b> to be read aloud.</p>
<p>When a <code>msg.reset</code> is available with value <code>true</code>, then playback of the current audio fragment will be stopped.</p>
<p>The <b>node status</b> reflects the current playback status:
<ul>
<li><b>started:</b> the audio fragment playback has been started.</li>
<li><b>reset:</b> the audio fragment playback has been reset (i.e. stopped before completed).</li>
</ul>
As soon as the audio fragment playback is completed, the node status will be cleared automatically.</p>
</script>

View File

@@ -0,0 +1,46 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function uiAudioNode(config) {
RED.nodes.createNode(this,config);
this.voice = config.voice;
this.group = config.group;
this.always = config.always || false;
if (this.group && RED.nodes.getNode(this.group).hasOwnProperty("config")) {
this.tabname = RED.nodes.getNode(RED.nodes.getNode(this.group).config.tab).name;
}
var node = this;
node.status({});
this.on('input', function(msg) {
if (msg.reset == true) {
ui.emit('ui-audio', { reset:true, tabname:node.tabname, always:node.always });
}
else if (Buffer.isBuffer(msg.payload)) {
ui.emit('ui-audio', { audio:msg.payload, tabname:node.tabname, always:node.always });
}
else if (typeof msg.payload === "string") {
ui.emit('ui-audio', { tts:msg.payload, voice:(node.voice || msg.voice || 0), tabname:node.tabname, always:node.always });
}
});
var updateStatus = function(audioStatus) {
if (audioStatus === "complete") {
// When the audio or speech has played completely, clear the node status
node.status({});
}
else if (audioStatus.indexOf("error") === 0) {
node.status({shape:"ring",fill:"red",text:audioStatus});
}
else {
node.status({shape:"dot",fill:"blue",text:audioStatus});
}
};
ui.ev.on('audiostatus', updateStatus);
this.on('close', function() {
ui.ev.removeListener('audiostatus', updateStatus);
})
}
RED.nodes.registerType("ui_audio", uiAudioNode);
}

View File

@@ -0,0 +1,3406 @@
<style>
:root {
--nr-db-dark-text: #444;
--nr-db-light-text: #eee;
--nr-db-disabled-text: #999;
}
.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: var(--nr-db-dark-text);
}
.nr-db-sb-list-header {
cursor: pointer;
position:relative;
color: var(--nr-db-dark-text);
padding:3px;
white-space: nowrap;
}
.nr-db-sb-list-header:hover {
color: var(--nr-db-dark-text);
}
.nr-db-sb-title-hidden {
text-decoration: line-through;
}
.nr-db-sb-title-disabled {
color: var(--nr-db-disabled-text);
}
.nr-db-sb-tab-list-header {
background: var(--nr-db-light-text);
padding:5px;
}
.nr-db-sb-group-list-header:hover,
.nr-db-sb-widget-list-header:hover {
background: var(--nr-db-light-text);
}
.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: var(--nr-db-light-text);
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: var(--nr-db-light-text);
}
.nr-db-sb-group-list>li.ui-sortable-helper {
border-top: 1px solid var(--nr-db-light-text);
}
.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: var(--nr-db-mid-grey);
}
span.nr-db-color-pick-container {
max-width: 50px;
border-radius: 3px;
margin-left: 15px;
}
input.nr-db-field-themeColor[type="color"] {
width: 60px !important;
padding: 0px;
height: 20px;
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;
}
ul.red-ui-dashboard-theme-styles {
list-style: none;
}
ul.red-ui-dashboard-theme-styles li {
margin-bottom: 6px;
}
.nr-db-resetIcon {
margin: 3px 6px 0px 6px;
float: right;
color: var(--nr-db-mid-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>
<!-- Dashboard layout tool -->
<link rel="stylesheet" href="./ui_base/css/gridstack.min.css">
<link rel="stylesheet" href="./ui_base/css/gridstack-extra.min.css">
<style>
.grid-stack {
background-color: #f8f8f8;
border: solid 2px #C0C0C0;
margin: auto;
min-height: 46px;
display: table-cell;
background-image: linear-gradient(#C0C0C0 1px, transparent 0),
linear-gradient(90deg, #C0C0C0 1px, transparent 0);
background-size: 40px 47px;
}
.grid-stack>.grid-stack-item>.grid-stack-item-content {
top: 3px;
left: 5px;
right: 5px;
bottom: 3px;
}
.grid-stack-item-content {
color: #2c3e50;
text-align: center;
background-color: #b0dfe3;
border-radius: 2px;
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;
white-space: nowrap;
font-size: 12px;
opacity: 0.7;
}
.grid-stack-item {
cursor: move;
}
.nr-dashboard-layout-container-fluid {
width: 100%;
padding-right: 0px;
padding-left: 0px;
margin-right: 0px;
margin-left: 0px;
}
.nr-dashboard-layout-row {
width: 100%;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin-right: 0px;
margin-left: 0px;
}
.nr-dashboard-layout-span12 {
width: 98.4%;
padding: 2px;
margin-left: 2px;
}
.nr-dashboard-layout-span6 {
width: 49.2%;
padding: 2px;
margin-left: 2px;
}
.nr-dashboard-layout-span4 {
width: 32.7%;
padding: 2px;
margin-left: 2px;
}
.nr-dashboard-layout-span3 {
width: 24.3%;
padding: 2px;
margin-left: 2px;
}
.nr-dashboard-layout-span2 {
width: 16.0%;
padding: 2px;
margin-left: 2px;
}
.nr-dashboard-layout-resize-disable {
cursor: pointer;
float: right;
position: relative;
z-index: 90;
margin-right: 4px;
}
.nr-dashboard-layout-resize-enable {
cursor: pointer;
float: right;
position: relative;
z-index: 90;
margin-right: 1px;
}
.grid-stack>.ui-state-disabled {
opacity: 1;
background-image: none;
}
.grid-stack>.grid-stack-item>.ui-resizable-handle {
z-index: 90;
margin-right: -7px;
}
</style>
<script type="text/javascript">
(function($) {
var editSaveEventHandler;
var nodesAddEventHandler;
var nodesRemoveEventHandler;
var layoutUpdateEventHandler; // Dashboard layout tool
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');
// Dashboard layout tool
// Load gridstack library
var loadGsLib = function(path, callback) {
$.ajax({ url: path,
success: function (data) {
var jsScript = document.createElement("script");
jsScript.type = "application/javascript";
jsScript.src = path;
document.body.appendChild(jsScript);
if (callback) {
callback();
}
},
error: function (xhr, ajaxOptions, thrownError) {
// TODO
}
});
};
loadGsLib('ui_base/js/lodash.min.js', function() {
loadGsLib('ui_base/js/gridstack.min.js', function() {
loadGsLib('ui_base/js/gridstack.jQueryUI.min.js', null)
});
});
var tabDatas; // Layout editing tab data
var oldSpacer; // Spacer not needed after editing
var widthChange; // Group width change
var widgetMove; // Move widget event
var widgetResize; // Change widget event
var widgetDrag; // Drag wiget eevnt
var MAX_GROUP_WIDTH = 30; // The maximum width is 30
/////////////////////////////////////////////////////////
// Get widget under specified tab from node information
/////////////////////////////////////////////////////////
function getTabDataFromNodes(tabID) {
var nodes = RED.nodes.createCompleteNodeSet(false);
var tab = {};
// Tab information
for (var cnt = 0; cnt < nodes.length; cnt++) {
if (nodes[cnt].type == "ui_tab" && nodes[cnt].id == tabID) {
tab = {
id: nodes[cnt].id,
name: nodes[cnt].name,
type: nodes[cnt].type,
order: nodes[cnt].order,
groups: []
};
break;
}
}
// Group information
for (var cnt = 0; cnt < nodes.length; cnt++) {
if (nodes[cnt].type == "ui_group" && nodes[cnt].tab == tabID) {
var group = {
id: nodes[cnt].id,
name: nodes[cnt].name,
type: nodes[cnt].type,
order: nodes[cnt].order,
width: nodes[cnt].width,
widgets: []
};
tab.groups.push(group);
}
}
// Widget information
var groupsIdx = {};
for (var cnt = 0; cnt < tab.groups.length; cnt++) {
groupsIdx[tab.groups[cnt].id] = tab.groups[cnt];
}
for (var cnt = 0; cnt < nodes.length; cnt++) {
var group = groupsIdx[nodes[cnt].group];
if (group != null && (/^ui_/.test(nodes[cnt].type) && nodes[cnt].type !== 'ui_link' && nodes[cnt].type !== 'ui_toast' && nodes[cnt].type !== 'ui_ui_control' && nodes[cnt].type !== 'ui_audio' && nodes[cnt].type !== 'ui_base' && nodes[cnt].type !== 'ui_group' && nodes[cnt].type !== 'ui_tab')) {
var widget = {
id: nodes[cnt].id,
type: nodes[cnt].type,
order: nodes[cnt].order,
width: nodes[cnt].width,
height: nodes[cnt].height,
auto: nodes[cnt].width == 0 ? true : false
};
group.widgets.push(widget);
if (!isLayoutToolSupported(nodes[cnt].type)){
console.log("LayoutTool warning: Unsupported widget. Widget="+JSON.stringify(widget));
}
}
}
return tab;
}
//////////////////////////////////////////////////
// Update node information in the edited widget
////////////////////////////////////////////////////
function putTabDataToNodes() {
// Delete old flow spacer node
for (var cnt = 0; cnt < oldSpacer.length; cnt++) {
RED.nodes.remove(oldSpacer[cnt]);
RED.nodes.dirty(true);
RED.view.redraw(true);
}
var t_groups = tabDatas.groups;
for (var cnt1 = 0; cnt1 < t_groups.length; cnt1++){
var n_group = RED.nodes.node(t_groups[cnt1].id);
n_group.width = t_groups[cnt1].width;
var t_widgets = t_groups[cnt1].widgets;
for (var cnt2 = 0; cnt2 < t_widgets.length; cnt2++) {
var n_widget = RED.nodes.node(t_widgets[cnt2].id);
if (n_widget != null) {
if (n_widget.group !== n_group.id) {
var oldGroupNode = RED.nodes.node(n_widget.group);
if (oldGroupNode) {
var index = oldGroupNode.users.indexOf(n_widget);
oldGroupNode.users.splice(index,1);
}
n_widget.group = n_group.id;
n_group.users.push(n_widget);
}
n_widget.order = t_widgets[cnt2].order;
if (t_widgets[cnt2].auto === true ) {
n_widget.width = 0;
n_widget.height = 0;
} else {
n_widget.width = t_widgets[cnt2].width;
n_widget.height = t_widgets[cnt2].height;
}
n_widget.changed = true;
n_widget.dirty = true;
RED.editor.validateNode(n_widget);
RED.events.emit("layout:update",n_widget);
RED.nodes.dirty(true);
RED.view.redraw(true);
} else {
// Add a spacer node
if (t_widgets[cnt2].type === 'ui_spacer') {
var spaceNode = {
_def: RED.nodes.getType("ui_spacer"),
type: "ui_spacer",
hasUsers: false,
users: [],
id: RED.nodes.id(),
tab: tabDatas.id,
group: n_group.id,
order: t_widgets[cnt2].order,
name: "spacer",
width: t_widgets[cnt2].width,
height: t_widgets[cnt2].height,
label: function() { return this.name + " " + this.width + "x" + this.height; }
};
RED.nodes.add(spaceNode);
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
};
}
RED.sidebar.info.refresh();
}
////////////////////////////////////////
// Sort by order
////////////////////////////////////////
function compareOrder(a, b) {
var r = 0;
if (a.order < b.order) { r = -1; }
else if (a.order > b.order) { r = 1; }
return r;
}
////////////////////////////////////////
// Sort by XY
////////////////////////////////////////
function compareXY(a, b) {
var r = 0;
if (a.y < b.y) { r = -1; }
else if (a.y > b.y) { r = 1; }
else if (a.x < b.x) { r = -1; }
else if (a.x > b.x) { r = 1; }
return r;
}
///////////////////////////////////////////////////////
// Placeable location search (placed in the upper left)
///////////////////////////////////////////////////////
function search_point(width, height, maxWidth, maxHeight, tbl){
for (var y=0; y < maxHeight; y++) {
for (var x=0; x < maxWidth; x++) {
if (check_matrix(x, y, width, height, maxWidth, tbl)) {
fill_matrix(x, y, width, height, maxWidth, tbl);
return {x:x, y:y};
}
}
}
return false;
}
// Check placement position
function check_matrix(px, py, width, height, maxWidth, tbl){
if (px+width > maxWidth) return false;
for (var y=py; y < py+height; y++) {
for (var x=px; x<px+width; x++) {
if (tbl[maxWidth*y+x]) return false;
}
}
return true;
}
// Mark the placement position
function fill_matrix(px, py, width, height, maxWidth, tbl){
for (var y=py; y < py+height; y++) {
for (var x=px; x < px+width; x++) {
tbl[maxWidth*y+x] = 1;
}
}
}
////////////////////////////////////////////////////
// Apply edit result to tab information for editing
////////////////////////////////////////////////////
function saveGridDatas(){
var groups = tabDatas.groups;
for (var cnt = 0; cnt < groups.length; cnt++) {
// Get layout editing results
var gridID = '#grid'+cnt;
var serializedData = _.map($(gridID+'.grid-stack > .grid-stack-item:visible'), function (el) {
el = $(el);
var node = el.data('_gridstack_node');
return {
id: el[0].dataset.noderedid,
type: el[0].dataset.noderedtype,
group: groups[cnt].id,
width: Number(node.width),
height: Number(node.height),
x: node.x,
y: node.y,
auto: (el[0].dataset.noderedsizeauto == 'true') ? true : false
};
}, this);
var width = Number(groups[cnt].width);
var height = 0;
// Search group height
for (var i=0; i < serializedData.length; i++){
var wd = serializedData[i];
if (height < wd.y + wd.height) {
height = wd.y + wd.height;
}
}
// Place widget on table
var tbl = new Array(width * height);
for (var i = 0; i< tbl.length; i++){
tbl[i]=0;
}
for (var i = 0; i < serializedData.length; i++){
var wd = serializedData[i];
for (var y = wd.y; y < wd.y+wd.height; y++){
for (var x = wd.x; x < wd.x+wd.width; x++){
tbl[width*y+x]=1;
}
}
}
// Add Spacer to Blank
for (var y = 0; y < height; y++){
var spacerAdded = false;
for (var x = 0; x < width; x++){
if (tbl[width*y+x]===0) {
if (!spacerAdded) {
// Add 1x1 spacer
serializedData.push({
x: x,
y: y,
width: 1,
height: 1,
name: 'spacer',
type: 'ui_spacer'
});
spacerAdded = true;
} else {
// Extend the spacer width by 1
serializedData[serializedData.length-1].width += 1;
}
} else {
spacerAdded = false;
}
}
}
// Sort Gridstack objects by x, y information
serializedData.sort(compareXY);
// Delete x and y elements as information for sorting, and give order
var order = 0;
for (i in serializedData) {
order++;
delete serializedData[i].x;
delete serializedData[i].y;
serializedData[i].order = order;
}
// Update widget information in group with edited data
var group = groups[cnt];
delete group.widgets;
group.widgets = serializedData;
}
// Save process call
putTabDataToNodes();
};
////////////////////////////////////////////////////
// Get default height for automatic settings
////////////////////////////////////////////////////
function getDefaultHeight(nodeID, groupWidth) {
var redNode = RED.nodes.node(nodeID);
var height = 1;
if (redNode.type === 'ui_gauge') {
if (redNode.gtype === 'gage') {
height = Math.round(groupWidth/2)+1;
} else if (redNode.gtype === 'wave') {
if (groupWidth < 3) {
height = 1;
} else {
height = Math.round(groupWidth*0.75);
}
} else { // donut or compass
if (groupWidth < 3) {
height = 1;
} else if (groupWidth < 11) {
height = groupWidth - 1;
} else {
height = Math.round(groupWidth*0.95);
}
}
} else if (redNode.type === 'ui_chart') {
height = Math.floor(groupWidth/2)+1;
} else if (redNode.type === 'ui_form') {
var optNum = redNode.options.length; // Sub widget number
if (redNode.label) {
height = optNum + 2; // Label and Button
} else {
height = optNum + 1; // Button only
}
} else if (redNode.type === 'ui_lineargauge') {
if (redNode.unit && redNode.name) {
height = 5;
} else {
height = 4;
}
} else if (redNode.type === 'ui_list') {
height = 5;
} else if (redNode.type === 'ui_vega') {
height = 5;
}
return height;
}
/////////////////////////////
// Grid width change
////////////////////////////
var changeGroupWidth = function(id) {
var widthID = '#change-width'+id;
var gridID = '#grid'+id;
$(widthID).spinner( {
min: 1,
max: MAX_GROUP_WIDTH,
spin: function(event, ui){
// Search current maximum width
serializedData = _.map($(gridID+'.grid-stack > .grid-stack-item:visible'), function (el) {
el = $(el);
var node = el.data('_gridstack_node');
return {
width: Number(node.width),
x: node.x,
auto: (el[0].dataset.noderedsizeauto == 'true') ? true : false
};
}, this);
var maxWidth = 0;
for (var i=0; i < serializedData.length; i++){
var wd = serializedData[i];
if (wd.auto == false) {
if (maxWidth < wd.x + wd.width) {
maxWidth = wd.x + wd.width;
}
}
}
var width = ui.value;
if (width < maxWidth) {
width = maxWidth;
}
var grid = $(gridID+'.grid-stack').data('gridstack');
$(gridID+'.grid-stack').css("width", width * 40);
$(gridID+'.grid-stack').css("background-size", 100/width+"% 47px");
grid.setGridWidth(tabDatas.groups[id].width, true);
grid.setGridWidth(width, true);
tabDatas.groups[id].width = width;
$(gridID+'.grid-stack > .grid-stack-item:visible').each( function(idx, el) {
el = $(el);
var node = el.data('_gridstack_node');
var auto = (el[0].dataset.noderedsizeauto == 'true') ? true : false;
var type = el[0].dataset.noderedtype;
grid.resizable(el, !auto);
if (auto === true) {
grid.resize(el, width, getDefaultHeight(node.id, width));
}
});
if (width !== ui.value) {
event.stopPropagation();
event.preventDefault();
}
}
});
};
//////////////////////////////////
// Move between groups of widgets
//////////////////////////////////
var moveGroupWidget = function(id) {
var toGridID = '#grid'+id;
var toGrid = $(toGridID+'.grid-stack').data('gridstack');
toGrid.container.droppable({
acccept: '.grid-stack-item',
tolerance: 'pointer',
drop: function( event, ui ) {
var fromGridID = '#' + ui.draggable[0].offsetParent.id;
// Move the widget
if (fromGridID !== toGridID) {
var toGridOffset = $(toGridID).offset();
var groupWidth = tabDatas.groups[id].width;
// Reset group width before moving (Grid stack measures)
toGrid.setGridWidth(groupWidth, true);
// Current widget width factor for x position
var curWidth = ($('#grid1').offset().left - $('#grid0').offset().left) * 0.9;
var xfactor = curWidth / (groupWidth * 40);
xfactor = xfactor > 1.0 ? 1.0 : xfactor;
var x = Math.floor((ui.offset.left - toGridOffset.left)/(40 * xfactor));
var y = Math.floor((ui.offset.top - toGridOffset.top)/47);
var node = {
type: ui.draggable[0].dataset.noderedtype,
id: ui.draggable[0].dataset.noderedid,
x: x,
y: y,
width: ui.draggable[0].dataset.gsWidth,
height: ui.draggable[0].dataset.gsHeight,
auto: (ui.draggable[0].dataset.noderedsizeauto == 'true') ? true : false
};
var dispType = ui.draggable[0].dataset.nodereddisptype;
var dispLabel = ui.draggable[0].dataset.nodereddisplabel;
var item = $('<div></div>', {
'data-noderedtype': node.type,
'data-noderedid': node.id,
'data-nodereddisptype': dispType,
'data-nodereddisplabel': dispLabel,
'data-noderedsizeauto': node.auto
});
if (node.auto === true) {
node.x = 0;
node.width = groupWidth;
node.height = getDefaultHeight(node.id, groupWidth);
}
var itemContent = $('<div></div>', {
addClass: 'grid-stack-item-content',
title: dispLabel + ':' + dispType
});
if (node.auto === true) {
itemContent.append('<i class="fa fa-unlock nr-dashboard-layout-resize-enable"></i>');
itemContent.find('.nr-dashboard-layout-resize-enable').on('click',layoutResizeEnable);
} else {
itemContent.append('<i class="fa fa-lock nr-dashboard-layout-resize-disable"</i>');
itemContent.find('.nr-dashboard-layout-resize-disable').on('click',layoutResizeDisable);
}
itemContent.append('<b>'+ dispLabel +'</b><br/>'+ dispType);
itemContent.appendTo(item);
var newWidget = toGrid.addWidget(
item,
node.x, node.y, node.width, node.height, false, null, null,
ui.draggable[0].dataset.gsMinHeight,
ui.draggable[0].dataset.gsMaxHeight,
node.id);
var fromWidget = ui.draggable[0];
if (node.auto === true) {
$(fromWidget).find('.nr-dashboard-layout-resize-enable').off('click');
} else {
$(fromWidget).find('.nr-dashboard-layout-resize-disable').off('click');
}
var fromGrid = $(fromGridID+'.grid-stack').data('gridstack');
fromGrid.removeWidget(fromWidget);
toGrid.resizable(newWidget, !node.auto);
}
}
});
}
//////////////////////////////////////////
// Widget size change (start event)
//////////////////////////////////////////
var resizeGroupWidget = function(id) {
var gridID = '#grid'+id;
var grid = $(gridID+'.grid-stack').data('gridstack');
$(gridID+'.grid-stack').on('resizestart', function(event, ui) {
// Reset group width
grid.setGridWidth(tabDatas.groups[id].width, true);
});
}
//////////////////////////////////////////
// Widget drag (start event)
//////////////////////////////////////////
var dragGroupWidget = function(id) {
var gridID = '#grid'+id;
var grid = $(gridID+'.grid-stack').data('gridstack');
$(gridID+'.grid-stack').on('dragstart', function(event, ui) {
// Reset group width
grid.setGridWidth(tabDatas.groups[id].width, true);
});
}
//////////////////////////////////////////
// Layout resize Disable (Auto:false)
//////////////////////////////////////////
var layoutResizeDisable = function(e) {
var target = $(e.target);
var el = target.parents('.grid-stack-item:visible');
var grid = target.parents('.grid-stack').data('gridstack');
var node = el.data('_gridstack_node');
var id = Number(target.parents('.grid-stack').attr('id').slice(4));
var width = Number(tabDatas.groups[id].width);
var nodeID = el[0].dataset.noderedid;
var height = getDefaultHeight(nodeID, width);
grid.move(el, 0, node.y);
grid.resize(el, width, height);
grid.resizable(el, false);
el.find('.nr-dashboard-layout-resize-disable').off('click');
el.attr({'data-noderedsizeauto':'true'});
target.removeClass().addClass('fa fa-unlock nr-dashboard-layout-resize-enable');
el.find('.nr-dashboard-layout-resize-enable').on('click',layoutResizeEnable);
}
//////////////////////////////////////////
// Layout resize Enable (Auto:true)
//////////////////////////////////////////
var layoutResizeEnable = function(e){
var target = $(e.target);
var el = target.parents('.grid-stack-item:visible');
var grid = target.parents('.grid-stack').data('gridstack');
grid.resizable(el, true);
el.find('.nr-dashboard-layout-resize-enable').off('click');
el.attr({'data-noderedsizeauto':'false'});
target.removeClass().addClass('fa fa-lock nr-dashboard-layout-resize-disable');
el.find('.nr-dashboard-layout-resize-disable').on('click',layoutResizeDisable);
}
//////////////////////////////////////////
// Check dashboard layout tool supported
//////////////////////////////////////////
function isLayoutToolSupported(nodeType){
if (nodeType !== "ui_spacer" &&
nodeType !== "ui_button" &&
nodeType !== "ui_dropdown" &&
nodeType !== "ui_switch" &&
nodeType !== "ui_slider" &&
nodeType !== "ui_numeric" &&
nodeType !== "ui_text_input" &&
nodeType !== "ui_date_picker" &&
nodeType !== "ui_colour_picker" &&
nodeType !== "ui_form" &&
nodeType !== "ui_text" &&
nodeType !== "ui_gauge" &&
nodeType !== "ui_chart" &&
nodeType !== "ui_template" &&
nodeType !== "ui_list" &&
nodeType !== "ui_table" &&
nodeType !== "ui_lineargauge" &&
nodeType !== "ui_my-little-ui-node" &&
nodeType !== "ui_vega") {
return false;
} else {
return true;
}
}
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);
RED.events.off("layout:update",layoutUpdateEventHandler); // Dashboard layout tool
},
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, "nr-dashboard") });
}
else {
RED.keyboard.add("*","ctrl-shift-d","dashboard:show-dashboard");
RED.actions.add("dashboard:show-dashboard",function() { window.open(document.location.protocol+"//"+lnk, "nr-dashboard") });
}
$('<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%")
.on("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")
.on("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")
.on("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")
.on("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")
.on("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%")
.on("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">').text(c_("label.horizontal")).appendTo(divSetSizes);
$('<span style="width:20%; display:inline-block; font-size:smaller">').text(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">').text(c_("label.widget-size")).appendTo(divSetSizes);
$('<input type="number" name="sx" min="24" id="nr-db-field-sx">').val(48).css("width","20%")
.on("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%")
.on("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">').text(c_("label.widget-spacing")).appendTo(divSetSizes);
$('<input type="number" name="cx" min="0" id="nr-db-field-cx">').val(6).css("width","20%")
.on("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%")
.on("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">').text(c_("label.group-padding")).appendTo(divSetSizes);
$('<input type="number" name="px" min="0" id="nr-db-field-px">').val(0).css("width","20%")
.on("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%")
.on("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">').text(c_("label.group-spacing")).appendTo(divSetSizes);
$('<input type="number" name="gx" min="0" id="nr-db-field-gx">').val(6).css("width","20%")
.on("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%")
.on("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)
.on("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)
.on("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)
.on("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)
.on("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")
.on("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">').text(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%")
.on("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">').text(c_("theme.custom-profile")).appendTo(customThemeLibraryContainer);
$('<input type="text" id="ui-sidebar-name" style="vertical-align:top;" placeholder="profile name (not blank)">')
.val(c_("theme.custom-profile-name"))
.on("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">').text(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"/>')
.on("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>')
.on("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">').text(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">').text(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">').text(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"/>')
.on("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 = {};
// 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.history.push({
t:'add',
nodes:[item.node.id],
dirty:RED.nodes.dirty()
});
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);
var tabicon = "fa-object-group";
//var tabicon = item.node.disabled ? "fa-window-close-o" : item.node.hidden ? "fa-eye-slash" : "fa-object-group";
$('<i>',{class:"nr-db-sb-icon nr-db-sb-tab-icon fa "+tabicon}).appendTo(titleRow);
var tabhide = item.node.hidden ? " nr-db-sb-title-hidden" : "";
var tabable = item.node.disabled ? " nr-db-sb-title-disabled" : "";
$('<span>',{class:"nr-db-sb-title"+tabhide+tabable}).text(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">').text(item.node.name||"untitled").appendTo(nameContainer);
$('<div class="nr-db-sb-link-url">').text(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();
});
// Dashboard layout tool
if (item.type === 'ui_tab') {
var layoutButton = $('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-pencil"></i> '+c_("layout.layout")+'</a>').appendTo(buttonGroup);
layoutButton.on('click',function(evt) {
var editTabName = item.node.name ? item.node.name : item.node.id;
var trayOptions = {
title: c_("layout.layout-editor") + " : " + editTabName,
width: Infinity,
buttons: [
{
id: "node-dialog-cancel",
text: RED._("common.label.cancel"),
click: function() {
// clean editor
RED.tray.close();
}
},
{
id: "node-dialog-ok",
text: RED._("common.label.done"),
class: "primary",
click: function() {
// Save data after editing
saveGridDatas();
RED.tray.close();
}
}
],
resize: function(dimensions) {
//
},
open: function(tray) {
// Get widget of specified tab from node information
tabDatas = getTabDataFromNodes(item.node.id);
// The width that can be handled by Layout is up to MAX_GROUP_WIDTH
// Groups exceeding the maximum width are not supported.
var tmpGroups = tabDatas.groups;
tmpGroups.sort(compareOrder);
var groups = [];
for (var cnt = 0; cnt < tmpGroups.length; cnt++) {
if (tmpGroups[cnt].width <= MAX_GROUP_WIDTH) {
groups.push(tmpGroups[cnt]);
}
}
tabDatas.groups = groups;
var editor = $('<div></div>',{addClass: 'nr-dashboard-layout-container-fluid'});
var row = $('<div></div>',{addClass: 'nr-dashboard-layout-row'});
var span_num = Math.floor(12 / groups.length); // bootstrap grid 12 splits
span_num = span_num < 2 ? 2 : span_num; // max 6 groups per row
for (var cnt = 0; cnt < groups.length; cnt++) {
if (cnt !=0 && (cnt % 6) == 0) {
editor.append(row);
editor.append('<div><br></div>');
row = $('<div></div>',{addClass: 'nr-dashboard-layout-row'});
}
var span = $('<div></div>',{addClass: 'nr-dashboard-layout-span' + span_num});
var groupName = groups[cnt].name ? groups[cnt].name : groups[cnt].id;
var title = $('<div></div>', {
style: "margin-top:2px; margin-bottom:2px;"
});
var title_group = $('<div></div>', {
title: groupName,
style: "margin-left:4px; margin-right:8px; overflow:hidden;"
}).appendTo(title);
$("<b/>").text(groupName).appendTo(title_group);
var title_width = $('<div></div>', {
style: "text-align:right; margin-right:8px;"
}).appendTo(title);
$("<span/>", {
style: "margin_right: 8px;"
}).text(c_("layout.width")+': ').appendTo(title_width);
var changeWidth = $('<input>', {
id: 'change-width' + cnt,
value: groups[cnt].width,
style: 'width:30px;',
readonly: true
});
title_width.append(changeWidth);
title.css('white-space','nowrap');
title.css('overflow','hidden');
var gridstack = $('<div></div>', {
id: 'grid'+cnt,
addClass: 'grid-stack'
});
span.append(title);
span.append(gridstack);
row.append(span);
}
if (groups.length != 0) {
editor.append(row);
}
// Show layout editor in tray
var trayBody = tray.find('.red-ui-tray-body, .editor-tray-body');
trayBody.css('overflow','auto');
trayBody.append(editor);
/////////////////////////////////////////
// Gridstack.js option
////////////////////////////////////////
var options = {
cellHeight: 46,
verticalMargin: 1,
float: true,
alwaysShowResizeHandle: true,
disableOneColumnMode : true
};
/////////////////////////////////////////
// Editor screen generation
/////////////////////////////////////////
oldSpacer = [];
widthChange = [];
widgetMove = [];
widgetResize = [];
widgetDrag = [];
for (var cnt=0; cnt < groups.length; cnt++) {
var gridID='#grid' + cnt;
// gridstack generation
$(gridID).gridstack(options);
// Clear the contents of Grind
var grid = $(gridID+'.grid-stack').data('gridstack');
grid.removeAll();
// Set the width of the display area of gridstack
var groupWidth = Number(groups[cnt].width);
$(gridID+'.grid-stack').css("width", groupWidth * 40);
$(gridID+'.grid-stack').css("background-size", 100/groupWidth+"% 47px");
grid.setGridWidth(groupWidth, true);
// Determination of placement position of widget of Grid
var widgets = groups[cnt].widgets;
widgets.sort(compareOrder);
var tbl = {};
for (var cnt2 = 0; cnt2 < widgets.length; cnt2++) {
// Set default value when there is auto width
if (widgets[cnt2].auto == true) {
widgets[cnt2].width = groupWidth;
// Adjust to the group width
} else if (widgets[cnt2].width > groupWidth) {
widgets[cnt2].width = groupWidth;
}
// Auto support
if (widgets[cnt2].auto === true || widgets[cnt2].type === 'ui_form') {
widgets[cnt2].height = getDefaultHeight(widgets[cnt2].id, groupWidth);
}
// Calculate coordinates to be placed
var point = search_point(Number(widgets[cnt2].width), Number(widgets[cnt2].height), groupWidth, 256, tbl);
if (point) {
widgets[cnt2].x = point.x;
widgets[cnt2].y = point.y;
}
}
var items = GridStackUI.Utils.sort(widgets);
_.each(items, function (node) {
var minHeight = null;
var maxHeight = null;
// ui_form is fixed to height 2
if (node.type === 'ui_form') {
minHeight = node.height;
maxHeight = node.height;
}
if (node.type !== 'ui_spacer') {
var dispNode = RED.nodes.node(node.id);
var dispType = dispNode._def.paletteLabel;
var dispLabel = dispNode._def.label;
try {
dispLabel = (typeof dispLabel === "function" ? dispLabel.call(dispNode) : dispLabel)||"";
}
catch(err) {
console.log("Definition error: " + node.type + ".label",err);
dispLabel = dispType;
}
var item = $('<div></div>', {
'data-noderedtype': node.type,
'data-noderedid': node.id,
'data-nodereddisptype': dispType,
'data-nodereddisplabel': dispLabel,
'data-noderedsizeauto': node.auto
});
var itemContent = $('<div></div>', {
addClass: 'grid-stack-item-content',
title: dispLabel + ':' + dispType
});
if (node.auto === true) {
itemContent.append('<i class="fa fa-unlock nr-dashboard-layout-resize-enable"></i>');
} else {
itemContent.append('<i class="fa fa-lock nr-dashboard-layout-resize-disable"></i>');
}
itemContent.append('<b>'+ dispLabel +'</b><br/>'+ dispType);
item.append(itemContent);
grid.addWidget(
item,
node.x, node.y, node.width, node.height, false, null, null,
minHeight, maxHeight, node.id);
} else {
// Record the spacer node ID to be deleted
oldSpacer.push(node.id);
}
});
$(gridID+'.grid-stack > .grid-stack-item:visible').each( function(idx, el) {
el = $(el);
var node = el.data('_gridstack_node');
var auto = (el[0].dataset.noderedsizeauto == 'true') ? true : false;
grid.resizable(el, !auto);
});
// Group width change
widthChange.push(new changeGroupWidth(cnt));
// Move widgets between groups (drag and drop)
widgetMove.push(new moveGroupWidget(cnt));
// Resize widget in group (start event)
widgetResize.push(new resizeGroupWidget(cnt));
// Dragging widgets in a group (start event)
widgetDrag.push(new dragGroupWidget(cnt));
}
$('.grid-stack>.grid-stack-item>.grid-stack-item-content>.nr-dashboard-layout-resize-disable').on('click',layoutResizeDisable);
$('.grid-stack>.grid-stack-item>.grid-stack-item-content>.nr-dashboard-layout-resize-enable').on('click',layoutResizeEnable);
},
close: function() {
//
},
show: function() {}
}
RED.tray.show(trayOptions);
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.history.push({
t:'add',
nodes:[group.node.id],
dirty:RED.nodes.dirty()
});
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">').text(groupNode.name||groupNode.id||"").appendTo(titleRow);
listElements[groupNode.id] = container;
var buttonGroup = $('<div>',{class:"nr-db-sb-list-header-button-group"}).appendTo(titleRow);
var spacerButton = $('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-plus"></i> '+c_("layout.spacer")+'</a>').appendTo(buttonGroup);
spacerButton.on('click',function(evt) {
var spaceNode = {
_def: RED.nodes.getType("ui_spacer"),
type: "ui_spacer",
hasUsers: false,
users: [],
id: RED.nodes.id(),
tab: item.node.name,
group: group.node.id,
order: i+1,
name: "spacer",
width: 1,
height:1,
label: function() { return "spacer " + this.width + "x" + this.height; }
};
RED.nodes.add(spaceNode);
RED.editor.validateNode(spaceNode);
RED.history.push({
t:'add',
nodes:[spaceNode.id],
dirty:RED.nodes.dirty()
});
RED.nodes.dirty(true);
RED.view.redraw();
evt.stopPropagation();
evt.preventDefault();
});
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">').text(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) {
var historyEvents = [];
items.each(function(i,el) {
var node = el.data('data');
var hev = {
t:'edit',
node:node,
changes:{
order:node.order,
group:node.group
},
dirty:node.dirty,
changed:node.changed
};
historyEvents.push(hev);
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.history.push({
t:'multi',
events: historyEvents
});
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) {
var historyEvents = [];
items.each(function(i,el) {
var groupData = el.data('data');
var node = groupData.node;
var hev = {
t:'edit',
node:node,
changes:{
order:node.order,
tab:node.tab
},
dirty:node.dirty,
changed:node.changed
};
historyEvents.push(hev);
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.history.push({
t:'multi',
events: historyEvents
});
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) {
var historyEvents = [];
items.each(function(i,el) {
var itemData = el.data('data');
var node = itemData.node;
var hev = {
t:'edit',
node:node,
changes:{
order:node.order
},
dirty:node.dirty,
changed:node.changed
}
historyEvents.push(hev);
var changed = false;
if (node.order !== i+1) {
node.order = i+1;
changed = true;
}
if (changed) {
node.dirty = true;
node.changed = true;
}
})
RED.history.push({
t:'multi',
events: historyEvents
});
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;
});
var hev = [];
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);
hev.push(tab.id);
}
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
};
hev.push(groupNode.id);
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.history.push({
t:'add',
nodes: hev,
dirty:RED.nodes.dirty()
});
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;
}
case 'ui_spacer': {
if (groups.hasOwnProperty(node.group)) {
groupElements[node.group] = groupElements[node.group]||[];
groupElements[node.group].push(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-angLook").val(globalDashboardNode.theme.angularTheme.palette || "light");
$("#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").text(node.name||node.id);
if (node.type === "ui_group") {
refresh();
}
else {
if (node.hidden === true) { listElements[node.id].children(".nr-db-sb-list-header").find(".nr-db-sb-title").addClass('nr-db-sb-title-hidden'); }
else { listElements[node.id].children(".nr-db-sb-list-header").find(".nr-db-sb-title").removeClass('nr-db-sb-title-hidden'); }
if (node.disabled === true) { listElements[node.id].children(".nr-db-sb-list-header").find(".nr-db-sb-title").addClass('nr-db-sb-title-disabled'); }
else { listElements[node.id].children(".nr-db-sb-list-header").find(".nr-db-sb-title").removeClass('nr-db-sb-title-disabled'); }
}
}
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").text(node.name||"untitled");
container.find(".nr-db-sb-link-url").text(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").text(l);
}
}
else {
if (groupLists[node.group]) {
if (node.order === 0) { node.order = groupLists[node.group].editableList('length'); }
groupLists[node.group].editableList('addItem',node);
}
}
}
}
};
RED.events.on("editor:save",editSaveEventHandler);
// Dashboard layout tool
layoutUpdateEventHandler = function(node) {
if (/^ui_/.test(node.type) && node.type !== 'ui_link' && node.type !== 'ui_toast' && node.type !== 'ui_ui_control' && node.type !== 'ui_audio' && node.type !== 'ui_base' && node.type !== 'ui_group' && node.type !== 'ui_tab') {
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);
groupLists[node.group].editableList('sort',function(a,b){return a.order-b.order;});
}
}
else {
groupLists[node.group].editableList('sort',function(a,b){return a.order-b.order;});
}
}
}
};
RED.events.on("layout:update",layoutUpdateEventHandler);
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>')
.text(" 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.text(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.text(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).text((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"})
.text(auto_text)
.appendTo(buttonRow)
.on('mouseup',function(evt) {
that.element.text(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.text(($(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.text((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/html" 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/html" data-help-name="ui_base">
</script>

View File

@@ -0,0 +1,117 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
var path= require('path');
var node;
var set = RED.settings.ui || "{}";
function BaseNode(config) {
RED.nodes.createNode(this, config);
node = this;
var baseFontName = "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif";
var defaultLightTheme = {
baseColor: '#0094CE',
baseFont: baseFontName
}
var defaultDarkTheme = {
baseColor: '#097479',
baseFont: baseFontName
}
var defaultCustomTheme = {
name: 'Untitled Theme 1',
baseColor: defaultLightTheme.baseColor,
baseFont: baseFontName
}
var defaultAngularTheme = {
primary:'indigo',
accents:'teal',
warn: "red",
background:'grey'
};
// Setup theme name
// First try old format (for upgrading with old flow file)
// Then try new format
// Else fallback to theme-light
var themeName;
if (typeof(config.theme) === 'string') { themeName = config.theme; }
else { themeName = config.theme.name || "theme-light"; }
// Setup other styles
var defaultThemeState = {}
if (themeName === 'theme-light') {
defaultThemeState["base-font"] = {value: baseFontName};
defaultThemeState["base-color"] = {value: "#0094CE"};
defaultThemeState["page-backgroundColor"] = {value: "#fafafa"};
defaultThemeState["page-titlebar-backgroundColor"] = {value: "#0094CE"};
defaultThemeState["page-sidebar-backgroundColor"] = {value: "#ffffff"};
defaultThemeState["group-backgroundColor"] = {value: "#ffffff"};
defaultThemeState["group-textColor"] = {value: "#000000"};
defaultThemeState["group-borderColor"] = {value: "#ffffff"};
defaultThemeState["widget-textColor"] = {value: "#111111"};
defaultThemeState["widget-backgroundColor"] = {value: "#0094CE"};
}
else {
defaultThemeState["base-font"] = {value: baseFontName};
defaultThemeState["base-color"] = {value: "#097479"};
defaultThemeState["page-backgroundColor"] = {value: "#111111"};
defaultThemeState["page-titlebar-backgroundColor"] = {value: "#097479"};
defaultThemeState["page-sidebar-backgroundColor"] = {value: "#000000"};
defaultThemeState["group-backgroundColor"] = {value: "#333333"};
defaultThemeState["group-textColor"] = {value: "#10cfd8"};
defaultThemeState["group-borderColor"] = {value: "#555555"};
defaultThemeState["widget-textColor"] = {value: "#eeeeee"};
defaultThemeState["widget-backgroundColor"] = {value: "#097479"};
}
var defaultThemeObject = {
name: themeName,
lightTheme: config.theme.lightTheme || defaultLightTheme,
darkTheme: config.theme.darkTheme || defaultDarkTheme,
customTheme: config.theme.customTheme || defaultCustomTheme,
angularTheme: config.theme.angularTheme || defaultAngularTheme,
themeState: config.theme.themeState || defaultThemeState
}
this.config = {
theme: defaultThemeObject,
site: config.site
}
ui.addBaseConfig(this.config);
}
RED.nodes.registerType("ui_base", BaseNode);
RED.library.register("themes");
RED.httpAdmin.get('/uisettings', function(req, res) {
res.json(set);
});
RED.httpAdmin.get('/ui_base/js/*', function(req, res) {
var filename = path.join(__dirname , '../dist/js', req.params[0]);
res.sendFile(filename, function (err) {
if (err) {
if (node) {
node.warn(filename + " not found. Maybe running in dev mode.");
}
else {
console.log("ui_base - error:",err);
}
}
});
});
RED.httpAdmin.get('/ui_base/css/*', function(req, res) {
var filename = path.join(__dirname , '../dist/css', req.params[0]);
res.sendFile(filename, function (err) {
if (err) {
if (node) {
node.warn(filename + " not found. Maybe running in dev mode.");
}
else {
console.log("ui_base - error:",err);
}
}
});
});
};

View File

@@ -0,0 +1,123 @@
<script type="text/javascript">
RED.nodes.registerType('ui_button',{
category: 'dashboard',
color: 'rgb(176, 223, 227)',
defaults: {
name: {value: ''},
group: {type: 'ui_group', required: true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0},
passthru: {value: false},
label: {value: 'button'},
tooltip: {value: ''},
color: {value: ''},
bgcolor: {value: ''},
icon: {value: ''},
payload: {value: '',validate: (RED.validators.hasOwnProperty('typedInput')?RED.validators.typedInput('payloadType'):function(v) { return true})},
payloadType: { value: 'str'},
topic: {value: ''}
},
inputs:1,
outputs:1,
outputLabels: function() { if (this.payloadType === "str") {
return this.payload;
} else {return this.payloadType; } },
icon: "ui_button.png",
paletteLabel: 'button',
label: function() { return this.name || (~this.label.indexOf("{{") ? null : this.label) || 'button'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
$('#node-input-payload').typedInput({
default: 'str',
typeField: $("#node-input-payloadType"),
types: ['str','num','bool','json','bin','date','flow','global']
})
}
});
</script>
<script type="text/html" data-template-name="ui_button">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-icon"><i class="fa fa-picture-o"></i> Icon</label>
<input type="text" id="node-input-icon" placeholder="optional icon ">
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-label" placeholder="optional label">
</div>
<div class="form-row">
<label for="node-input-tooltip"><i class="fa fa-info-circle"></i> Tooltip</label>
<input type="text" id="node-input-tooltip" placeholder="optional tooltip">
</div>
<div class="form-row">
<label for="node-input-color"><i class="fa fa-tint"></i> Colour</label>
<input type="text" id="node-input-color" placeholder="optional text/icon color">
</div>
<div class="form-row">
<label for="node-input-bgcolor"><i class="fa fa-tint"></i> Background</label>
<input type="text" id="node-input-bgcolor" placeholder="optional background color">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-payload"><i class="fa fa-envelope-o"></i> When clicked, send:</label>
</div>
<div class="form-row">
<label for="node-input-payload" style="padding-left: 25px; margin-right: -25px">Payload</label>
<input type="text" id="node-input-payload" style="width:70%">
<input type="hidden" id="node-input-payloadType">
</div>
<div class="form-row">
<label for="node-input-topic" style="padding-left: 25px; margin-right: -25px">Topic</label>
<input type="text" id="node-input-topic">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-passthru"><i class="fa fa-arrow-right"></i> If <code>msg</code> arrives on input, emulate a button click: </label>
<input type="checkbox" id="node-input-passthru" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/html" data-help-name="ui_button">
<p>Adds a button to the user interface.</p>
<p>Clicking the button generates a message with <code>msg.payload</code> set to the <b>Payload</b> field.
If no payload is specified, the node id is used.</p>
<p>The <b>Size</b> defaults to 3 by 1.</p>
<p>The <b>Icon</b> can be defined, as either a <a href="https://klarsys.github.io/angular-material-icons/" target="_blank">Material Design icon</a>
<i>(e.g. 'check', 'close')</i> or a <a href="https://fontawesome.com/v4.7.0/icons/" target="_blank">Font Awesome icon</a>
<i>(e.g. 'fa-fire')</i>, or a <a href="https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md">Weather icon</a>.
You can use the full set of google material icons if you add 'mi-' to the icon name. e.g. 'mi-videogame_asset'.</p>
<p>The colours of the text and background may be set. They can also be set by a message property by setting
the field to the name of the property, for example <code>{{msg.background}}</code>.</p>
<p>The label can also be set by a message property by setting
the field to the name of the property, for example <code>{{msg.topic}}</code>.</p>
<p>If set to pass through mode a message arriving on the input will act like pressing the button.
The output payload will be as defined in the node configuration.</p>
<p>The <b>Topic</b> field can be used to set the <code>msg.topic</code> property that is output.</p>
<p>Setting <code>msg.enabled</code> to <code>false</code> will disable the button.</p>
</script>

View File

@@ -0,0 +1,69 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function ButtonNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
var payloadType = config.payloadType;
var payload = config.payload;
if (payloadType === 'flow' || payloadType === 'global') {
try {
var parts = RED.util.normalisePropertyExpression(payload);
if (parts.length === 0) {
throw new Error();
}
}
catch(err) {
node.warn("Invalid payload property expression - defaulting to node id")
payload = node.id;
payloadType = 'str';
}
}
else {
payload = payload || node.id;
}
var done = ui.add({
node: node,
tab: tab,
group: group,
emitOnlyNewValues: false,
forwardInputMessages: config.passthru || false,
storeFrontEndInputAsState: false,
control: {
type: 'button',
label: config.label,
tooltip: config.tooltip,
color: config.color,
bgcolor: config.bgcolor,
icon: config.icon,
order: config.order,
value: payload,
format: config.bgcolor,
width: config.width || group.config.width || 3,
height: config.height || 1
},
beforeSend: function (msg) {
msg.topic = config.topic || msg.topic;
},
convertBack: function (value) {
if (payloadType === "date") {
value = Date.now();
}
else {
value = RED.util.evaluateNodeProperty(payload,payloadType,node);
}
return value;
}
});
node.on("close", done);
}
RED.nodes.registerType("ui_button", ButtonNode);
};

View File

@@ -0,0 +1,295 @@
<style>
input.series-color {
width: 100px;
text-align: center;
}
input.series-color::-webkit-color-swatch {
border: none;
}
</style>
<script type="text/javascript">
RED.nodes.registerType('ui_chart',{
category: 'dashboard',
color: 'rgb(119, 198, 204)',
defaults: {
name: {value: ''},
group: {type: 'ui_group', required: true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}},
height: {value: 0},
label: {value: 'chart'},
chartType: {value: 'line'},
legend: {value: 'false'},
xformat: {value: 'HH:mm:ss'},
interpolate: {value: 'linear', required:true},
nodata: {value: ''},
dot: {value: false},
ymin: {value: '', validate:function(value) { return value === '' || RED.validators.number(); }},
ymax: {value: '', validate:function(value) { return value === '' || RED.validators.number(); }},
removeOlder: {value: 1, validate:RED.validators.number(), required:true},
removeOlderPoints: {value: '', validate:function(value) { return value === '' || RED.validators.number(); }},
removeOlderUnit: {value: '3600', required:true},
cutout: {value: 0},
useOneColor: {value: false},
colors: {value: ['#1F77B4', '#AEC7E8', '#FF7F0E', '#2CA02C', '#98DF8A', '#D62728', '#FF9896', '#9467BD', '#C5B0D5']},
useOldStyle: {value: false},
outputs: {value: 1}
},
inputs:1,
outputs:1,
inputLabels: function() { return this.chartType; },
outputLabels: ["chart state"],
align: "right",
icon: "ui_chart.png",
paletteLabel: 'chart',
label: function() { return this.name || (~this.label.indexOf("{{") ? null : this.label) || 'chart'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
var oldouts = this.outputs;
if (RED.nodes.filterLinks({source:{id:this.id},sourcePort:1}).length > 0) { this.outputs = 2; }
else { this.outputs = 1; }
if (this.outputs !== oldouts) { this.changed = true; }
if (!$("#node-input-chartType").val()) {
$("#node-input-chartType").val("line");
}
if (this.useOldStyle === undefined) {
$("#node-input-useOldStyle").prop('checked', true);
}
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
$("#node-input-chartType").on("change", function() {
$("#legend-show").hide();
if ($(this).val() === "horizontalBar") {
$("#y-label-show").hide();
$("#x-label-show").show();
}
else {
$("#y-label-show").show();
$("#x-label-show").hide();
}
if ($(this).val() === "line") {
$("#x-axis-show").show();
$("#x-axis-label-show").show();
$("#interpolate-show").show();
$("#legend-show").show();
$("#y-axis-show").show();
$("#hole-size-show").hide();
$("#show-dot-field").show();
$("#show-useOneColor").hide();
}
else {
$("#x-axis-show").hide();
$("#x-axis-label-show").hide();
$("#interpolate-show").hide();
$("#show-dot-field").hide();
if (($(this).val() === "bar")||($(this).val() === "horizontalBar")) {
$("#show-useOneColor").show();
$("#legend-show").show();
}
else {
$("#show-useOneColor").hide();
}
if ($(this).val() === "pie") {
$("#y-axis-show").hide();
$("#legend-show").show();
$("#hole-size-show").show();
}
else {
$("#y-axis-show").show();
$("#hole-size-show").hide();
}
}
});
var setColour = function(id, value) {
$(id).val(value);
$(id).css("background-color", value);
var rgb = tinycolor(value).toRgb();
var level = ((rgb.r*299) + (rgb.g*587) + (rgb.b*114))/1000;
var textColor = (level >= 128) ? '#111111' : '#eeeeee';
$(id).css("color", textColor);
}
$(".series-color").on("change", function() {
setColour("#"+$(this).attr("id"), $(this).val());
});
var oval = $("#node-input-xformat").val();
if (!oval) { $("#node-input-xformat").val("HH:mm:ss"); }
var odef = 'custom';
if (oval === "HH:mm:ss") { odef = oval; }
if (oval === "HH:mm") { odef = oval; }
if (oval === "Y-M-D") { odef = oval; }
if (oval === "D/M") { odef = oval; }
if (oval === "dd HH:mm") { odef = oval; }
if (oval === "auto") { odef = oval; }
var ohms = {value:"HH:mm:ss", label:"HH:mm:ss", hasValue:false};
var ohm = {value:"HH:mm", label:"HH:mm", hasValue:false};
var oymd = {value:"Y-M-D", label:"Year-Month-Date", hasValue:false};
var odm = {value:"D/M", label:"Date/Month", hasValue:false};
var oahm = {value:"dd HH:mm", label:"Day HH:mm", hasValue:false};
var ocus = {value:"custom", label:"custom", icon:"red/images/typedInput/az.png"};
var oaut = {value:"auto", label:"automatic", hasValue:false};
$("#node-input-xformat").typedInput({
default: odef,
types:[ ohms, ohm, oahm, odm, oymd, ocus, oaut ]
});
var defaultColors = ['#1F77B4', '#AEC7E8', '#FF7F0E', '#2CA02C', '#98DF8A', '#D62728', '#FF9896', '#9467BD', '#C5B0D5'];
if (this.colors) {
for (var i=0; i<this.colors.length; i++) {
var value = this.colors[i] || defaultColors[i];
setColour("#node-input-color"+(i+1), value);
}
}
else {
for (var c=0; c<defaultColors.length; c++) {
setColour("#node-input-color"+(c+1), defaultColors[c]);
}
}
},
oneditsave: function() {
if ($("#node-input-xformat").typedInput('type') !== 'custom') {
$("#node-input-xformat").val($("#node-input-xformat").typedInput('type'));
}
this.colors = [$("#node-input-color1").val(),$("#node-input-color2").val(),$("#node-input-color3").val(),
$("#node-input-color4").val(),$("#node-input-color5").val(),$("#node-input-color6").val(),
$("#node-input-color7").val(),$("#node-input-color8").val(),$("#node-input-color9").val()];
}
});
</script>
<script type="text/html" data-template-name="ui_chart">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-label" placeholder="optional chart title">
</div>
<div class="form-row">
<label for="node-input-removeOlder"><i class="fa fa-line-chart"></i> Type</label>
<select id="node-input-chartType" style="width:159px; font-family:'FontAwesome','Helvetica Neue', Helvetica, Arial, sans-serif">
<option value="line"> &#xf201; Line chart</option>
<option value="bar"> &#xf080; Bar chart</option>
<option value="horizontalBar"> &#xf080; Bar chart (H)</option>
<option value="pie"> &#xf200; Pie chart</option>
<option value="polar-area"> &#xf200; Polar area chart</option>
<option value="radar"> &#xf200; Radar chart</option>
</select>
<div id="show-dot-field" style="display:inline-block;">
<input type="checkbox" id="node-input-dot" style="display:inline-block; width:auto; vertical-align:baseline; margin-left:40px; margin-right:5px;">enlarge points
</div>
</div>
<div class="form-row" id="x-axis-show">
<label for="node-input-removeOlder">X-axis</label>
<label for="node-input-removeOlder" style="width:auto">last</label>
<input type="text" id="node-input-removeOlder" style="width:50px;">
<select id="node-input-removeOlderUnit" style="width:80px;">
<option value="1">seconds</option>
<option value="60">minutes</option>
<option value="3600">hours</option>
<option value="86400">days</option>
<option value="604800">weeks</option>
</select>
<label for="node-input-removeOlderPoints" style="width:auto; margin-left:10px; margin-right:10px;">OR</label>
<input type="text" id="node-input-removeOlderPoints" style="width:60px;" placeholder="1000">
<span style="margin-left:5px;">points</span>
</div>
<div class="form-row" id="x-axis-label-show">
<label for="node-input-xformat">X-axis Label</label>
<input type="text" id="node-input-xformat" style="width:268px;">
</div>
<div class="form-row" id="y-axis-show">
<label id="y-label-show" for="node-input-ymin">Y-axis</label>
<label id="x-label-show" for="node-input-ymin">X-axis</label>
<label for="node-input-ymin" style="width:auto">min</label>
<input type="text" id="node-input-ymin" style="width:92px">
<label for="not-input-ymax" style="width:auto; margin-left:20px;">max</label>
<input type="text" id="node-input-ymax" style="width:92px">
</div>
<div class="form-row" id="legend-show">
<label for="node-input-legend">Legend</label>
<select id="node-input-legend" style="width:120px;">
<option value="false">None</option>
<option value="true">Show</option>
</select>
<span id="interpolate-show">&nbsp;&nbsp;&nbsp;&nbsp;Interpolate
<select id="node-input-interpolate" style="width:120px;">
<option value="linear">linear</option>
<option value="step">step</option>
<option value="bezier">bezier</option>
</select>
</span>
<span id="hole-size-show">&nbsp;&nbsp;&nbsp;&nbsp;Cutout
<input type="text" id="node-input-cutout" style="width:35px"> %
</span>
</div>
<div id="show-useOneColor" style="display:none; height:24px;">
<input type="checkbox" id="node-input-useOneColor" style="display:inline-block; width:auto; vertical-align:baseline; margin-left:105px; margin-right:5px;">Use first colour for all bars
</div>
<div class="form-row" id="ui-chart-colours">
<label for="node-input-color1">Series Colours</label>
<input type="color" id="node-input-color1" class="series-color" style="width:100px;"/>
<input type="color" id="node-input-color2" class="series-color" style="width:100px;"/>
<input type="color" id="node-input-color3" class="series-color" style="width:100px;"/>
<div style="margin-top:5px; margin-left:104px;">
<input type="color" id="node-input-color4" class="series-color" style="width:100px;"/>
<input type="color" id="node-input-color5" class="series-color" style="width:100px;"/>
<input type="color" id="node-input-color6" class="series-color" style="width:100px;"/>
</div>
<div style="margin-top:5px; margin-left:104px;">
<input type="color" id="node-input-color7" class="series-color" style="width:100px;"/>
<input type="color" id="node-input-color8" class="series-color" style="width:100px;"/>
<input type="color" id="node-input-color9" class="series-color" style="width:100px;"/>
</div>
</div>
<div class="form-row">
<label for="node-input-nodata">Blank label</label>
<input type="text" id="node-input-nodata" placeholder="display this text before valid data arrives">
</div>
<div class="form-row">
<input type="checkbox" id="node-input-useOldStyle" style="display:inline-block; width:auto; vertical-align:baseline; margin-left:105px; margin-right:5px;">Use deprecated (pre 2.5.0) data format.
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/html" data-help-name="ui_chart">
<p>Plots the input values on a chart. This can either be a time based line chart, a bar chart (vertical or horizontal),
or a pie chart.</p>
<p>Each input <code>msg.payload</code> value will be converted to a number. If the
conversion fails, the message is ignored.</p>
<p>Minimum and Maximum <b>Y</b> axis values are optional. The graph will auto-scale to any values received.</p>
<p>Multiple series can be shown on the same chart by using a different <code>msg.topic</code>
value on each input message. Multiple bars of the same series can be shown by using the <code>msg.label</code> property.</p>
<p>The <b>X</b> axis defines a time window or a maximum number of points to display. Older data will be automatically removed from the graph.
The axis labels can be formatted using a <a href="https://momentjs.com/docs/#/displaying/format/" target="_blank">
Moment.js time formatted</a> string.</p>
<p>Inputting a <code>msg.payload</code> containing a blank array <code>[]</code> will clear the chart.</p>
<p>See <b><a href="https://github.com/node-red/node-red-dashboard/blob/master/Charts.md" target="_new">this information</a></b>
for how to pre-format data to be passed in as a complete chart.</p>
<p>The <b>Blank label</b> field can be used to display some text before any valid data is received.</p>
<p>The label can also be set by a message property by setting
the field to the name of the property, for example <code>{{msg.topic}}</code>.</p>
<p>The node output contains an array of the chart state that can be persisted if needed. This can be passed
into the chart node to re-display the persisted data.</p>
</script>

View File

@@ -0,0 +1,280 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
var ChartIdList = {};
function ChartNode(config) {
RED.nodes.createNode(this, config);
this.chartType = config.chartType || "line";
this.newStyle = (!config.hasOwnProperty("useOldStyle") || (config.useOldStyle === true)) ? false : true;
var node = this;
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
if (config.width === "0") { delete config.width; }
if (config.height === "0") { delete config.height; }
// number of pixels wide the chart will be... 43 = sizes.sx - sizes.px
//var pixelsWide = ((config.width || group.config.width || 6) - 1) * 43 - 15;
if (!tab || !group) { return; }
var dnow = Date.now();
var options = {
emitOnlyNewValues: true,
node: node,
tab: tab,
group: group,
control: {
type: 'chart',
look: node.chartType,
order: config.order,
label: config.label,
legend: config.legend || false,
interpolate: config.interpolate,
nodata: config.nodata,
width: parseInt(config.width || group.config.width || 6),
height: parseInt(config.height || group.config.width/2+1 || 4),
ymin: config.ymin,
ymax: config.ymax,
dot: config.dot || false,
xformat : config.xformat || "HH:mm:ss",
cutout: parseInt(config.cutout || 0),
colors: config.colors,
useOneColor: config.useOneColor || false,
animation: false,
spanGaps: false,
options: {},
},
convertBack: function(data) {
if (node.newStyle) {
if (data && data[0] && data[0].hasOwnProperty("values")) {
return [data[0].values];
}
}
else {
if (data && data[0]) {
if (data[0] && data[0].hasOwnProperty("values") && data[0].values.hasOwnProperty("series") ) {
var o = [];
for (var i=0; i<data[0].values.series.length; i++) {
if (data[0].values.data[i] !== undefined) {
if (node.chartType !== "line") {
o.push({ key:data[0].values.series[i], values:data[0].values.data[i][0] });
}
else {
var d = data[0].values.data[i].map(function(i) { return [i.x, i.y]; });
o.push({ key:data[0].values.series[i], values:d });
}
}
}
data = o;
}
}
return data;
}
},
convert: function(value, oldValue, msg) {
var converted = {};
if (ChartIdList.hasOwnProperty(node.id) && ChartIdList[node.id] !== node.chartType) {
value = [];
}
if (this.control.look !== node.chartType) {
if ((this.control.look === "line") || (node.chartType === "line")) { value = []; }
node.chartType = this.control.look;
}
ChartIdList[node.id] = node.chartType;
if (Array.isArray(value)) {
if (value.length === 0) { // reset chart
converted.update = false;
converted.updatedValues = [];
return converted;
}
// New style
if (!value[0].hasOwnProperty("key")) {
if (value[0].hasOwnProperty("series") && value[0].hasOwnProperty("data")) {
var flag = true;
for (var dd = 0; dd < value[0].data.length; dd++ ) {
if (!isNaN(value[0].data[dd][0])) { flag = false; }
}
if (node.chartType === "line") {
if (flag) { delete value[0].labels; }
}
else if (node.chartType === "bar" || node.chartType === "horizontalBar") {
if (flag) {
var tmp = [];
for (var d=0; d<value[0].data.length; d++) {
tmp.push([value[0].data[d]]);
}
value[0].data = tmp;
var tmp2 = value[0].series;
value[0].series = value[0].labels;
value[0].labels = tmp2;
}
}
value = [{ key:node.id, values:(value[0] || {series:[], data:[], labels:[]}) }];
}
else {
node.warn("Bad data inject");
value = oldValue;
}
}
// Old style
else {
if (node.chartType !== "line") {
var nb = { series:[], data:[], labels:[] };
for (var v in value) {
if (value.hasOwnProperty(v)) {
nb.data.push([ value[v].values ]);
nb.series.push(value[v].key);
}
}
value = [{key:node.id, values:nb}];
}
else {
if (value[0] && value[0].hasOwnProperty("values")) {
if (Array.isArray(value[0].values)) { // Handle "old" style data array
var na = {series:[], data:[]};
for (var n=0; n<value.length; n++) {
na.series.push(value[n].key);
na.data.push(value[n].values.map(function(i) {
return {x:i[0], y:i[1]};
}));
}
value = [{ key:node.id, values:na }];
}
}
}
}
//console.log("RETURN",JSON.stringify(value));
converted.update = false;
converted.updatedValues = value;
}
else {
if (value === false) { value = null; } // let false also create gaps in chart
if (value !== null) { // let null object through for gaps
value = parseFloat(value); // only handle numbers
if (isNaN(value)) { return; } // return if not a number
}
converted.newPoint = true;
var label = msg.label || " ";
var series = msg.series || msg.topic || "";
//if (node.chartType === "bar" || node.chartType === "horizontalBar" || node.chartType === "pie") {
if (node.chartType !== "line") {
if (!node.newStyle || !msg.series) {
label = msg.topic || msg.label || " ";
series = msg.series || "";
}
}
if ((!oldValue) || (oldValue.length === 0)) {
oldValue = [{ key:node.id, values:{ series:[], data:[], labels:[] } }];
}
//if (node.chartType === "line" || node.chartType === "pie" || node.chartType === "bar" || node.chartType === "horizontalBar" || node.chartType === "radar") { // Line, Bar and Radar
var refill = false;
if (node.chartType === "line") { label = ""; }
var s = oldValue[0].values.series.indexOf(series);
if (!oldValue[0].values.hasOwnProperty("labels")) { oldValue[0].values.labels = []; }
var l = oldValue[0].values.labels.indexOf(label);
if (s === -1) {
oldValue[0].values.series.push(series);
s = oldValue[0].values.series.length - 1;
oldValue[0].values.data[s] = [];
if (l > 0) { refill = true; }
}
if (l === -1) {
oldValue[0].values.labels.push(label);
l = oldValue[0].values.labels.length - 1;
if (l > 0) { refill = true; }
}
if (node.chartType === "line") {
var time;
if (msg.timestamp !== undefined) { time = new Date(msg.timestamp).getTime(); }
else { time = new Date().getTime(); }
var limitOffsetSec = parseInt(config.removeOlder) * parseInt(config.removeOlderUnit);
var limitTime = time - limitOffsetSec * 1000;
if (time < limitTime) { return oldValue; } // ignore if too old for window
var point = { "x":time, "y":value };
oldValue[0].values.data[s].push(point);
converted.newPoint = [{ key:node.id, update:true, values:{ series:series, data:point, labels:label } }];
var rc = 0;
for (var u = 0; u < oldValue[0].values.data[s].length; u++) {
if (oldValue[0].values.data[s][u].x >= limitTime) {
break; // stop as soon as we are in time window.
}
else {
oldValue[0].values.data[s].shift();
rc += 1;
}
}
if (config.removeOlderPoints) {
while (oldValue[0].values.data[s].length > config.removeOlderPoints) {
oldValue[0].values.data[s].shift();
rc += 1;
}
}
if (rc > 0) { converted.newPoint[0].remove = rc; }
var swap; // insert correctly if a timestamp was earlier.
for (var t = oldValue[0].values.data[s].length-2; t>=0; t--) {
if (oldValue[0].values.data[s][t].x <= time) {
break; // stop if we are in the right place
}
else {
swap = oldValue[0].values.data[s][t];
oldValue[0].values.data[s][t] = oldValue[0].values.data[s][t+1];
oldValue[0].values.data[s][t+1] = swap;
}
}
if (swap) { converted.newPoint = true; } // if inserted then update whole chart
if (Date.now() > (dnow + 60000)) {
dnow = Date.now();
for (var x = 0; x < oldValue[0].values.data.length; x++) {
for (var y = 0; y < oldValue[0].values.data[x].length; y++) {
if (oldValue[0].values.data[x][y].x >= limitTime) {
break; // stop as soon as we are in time window.
}
else {
oldValue[0].values.data[x].shift();
converted.newPoint = true;
y = y - 1;
}
}
}
}
}
else {
oldValue[0].values.data[s][l] = value;
if (refill) {
for (var i = 0; i < oldValue[0].values.series.length; i++) {
for (var k = 0; k < oldValue[0].values.labels.length; k++) {
oldValue[0].values.data[i][k] = oldValue[0].values.data[i][k] || null;
}
}
}
}
converted.update = true;
converted.updatedValues = oldValue;
}
return converted;
}
};
var chgtab = function() {
node.receive({payload:"R"});
};
ui.ev.on('changetab', chgtab);
var done = ui.add(options);
var st = setTimeout(function() {
node.emit("input",{payload:"start"}); // trigger a redraw at start to flush out old data.
if (node.wires.length === 2) { // if it's an old version of the node honour it
node.send([null, {payload:"restore", for:node.id}]);
}
}, 100);
node.on("close", function() {
if (st) { clearTimeout(st); }
ui.ev.removeListener('changetab', chgtab);
done();
})
}
RED.nodes.registerType("ui_chart", ChartNode);
};

View File

@@ -0,0 +1,151 @@
<script type="text/javascript">
RED.nodes.registerType('ui_colour_picker',{
category: 'dashboard',
color: 'rgb(176, 223, 227)',
defaults: {
name: {value: ''},
label: {value: ''},
group: {type: 'ui_group', required: true},
format: {value: 'hex'},
outformat: {value: 'string'},
showSwatch: {value: true},
showPicker: {value: false},
showValue: {value: false},
showHue: {value: false},
showAlpha: {value: false},
showLightness: {value: true},
square: {value: "false"},
dynOutput: {value: "false"},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0},
passthru: {value: true},
topic: {value: ''}
},
inputs:1,
outputs:1,
outputLabels: function() { return this.format; },
icon: "ui_colour_picker.png",
paletteLabel: 'colour picker',
label: function() { return this.name || (~this.label.indexOf("{{") ? null : this.label) || 'colour picker'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
if (this.square === undefined) {
this.square = "false";
$("#node-input-square").val("false");
}
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
$("#node-input-format").on("change", function() {
if ($(this).val() === "hex") {
$("#node-alpha-control").hide();
}
else {
$("#node-alpha-control").show();
}
});
},
oneditsave: function() {
if (!$("#node-input-showPicker").is(':checked') && !$("#node-input-showValue").is(':checked')) {
$("#node-input-showSwatch").prop('checked', true);
this.showSwatch = true;
}
}
});
</script>
<script type="text/html" data-template-name="ui_colour_picker">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-label">
</div>
<div class="form-row">
<label for="node-input-format"><i class="fa fa-keyboard-o"></i> Format</label>
<select id="node-input-format" style="width:156px;">
<option value="hex">hex</option>
<option value="hex8">hex8</option>
<option value="hsl">hsl</option>
<option value="hsv">hsv</option>
<option value="rgb">rgb</option>
</select>
<select id="node-input-square" style="width:130px; margin-left:30px">
<option value="false">round</option>
<option value="true">square</option>
</select>
</div>
<div class="form-row">
<label>&nbsp;</label> Show hue slider : <input type="checkbox" id="node-input-showHue" style="display:inline-block; width:auto; vertical-align:baseline;">
<br/>
<label>&nbsp;</label> Show lightness slider : <input type="checkbox" id="node-input-showLightness" style="display:inline-block; width:auto; vertical-align:baseline;">
<br/>
<span id="node-alpha-control"><label>&nbsp;</label> Show transparency slider : <input type="checkbox" id="node-input-showAlpha" style="display:inline-block; width:auto; vertical-align:baseline;"></span>
</div>
<div class="form-row">
If width is 4 or greater:<br/>
<label>&nbsp;</label>
Always show swatch : <input type="checkbox" checked id="node-input-showSwatch" style="display:inline-block; width:auto; vertical-align:top;">
<br/>
<label>&nbsp;</label>
Always show picker : <input type="checkbox" checked id="node-input-showPicker" style="display:inline-block; width:auto; vertical-align:top;">
<br/>
<label>&nbsp;</label>
Always show value field : <input type="checkbox" checked id="node-input-showValue" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-passthru"><i class="fa fa-arrow-right"></i> If <code>msg</code> arrives on input, pass through to output: </label>
<input type="checkbox" checked id="node-input-passthru" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label for="node-input-dynOutput"><i class="fa fa-envelope-o"></i> Send</label>
<select id="node-input-dynOutput" style="width:60%">
<option value="false">one value when released/closed</option>
<option value="true">multiple values during editing</option>
</select>
</div>
<div class="form-row">
<label for="node-input-outformat" style="padding-left: 25px; margin-right: -25px">Payload</label>
<select id="node-input-outformat" style="width:60%">
<option value="string">current value as a string</option>
<option value="object">current value as an object</option>
</select>
</div>
<div class="form-row">
<label for="node-input-topic" style="padding-left: 25px; margin-right: -25px">Topic</label>
<input type="text" id="node-input-topic" placeholder="optional topic">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/html" data-help-name="ui_colour_picker">
<p>Adds a colour picker to the dashboard.</p>
<p>If the group width is 4 or greater then the picker can be set to be visible at all times.</p>
<p><b>Format</b> can be rgb, hex, hex8, hsv, or hsl. Transparency is supported for all except hex.</p>
<p>If a <b>Topic</b> is specified, it will be added as <code>msg.topic</code>.</p>
<p>Setting <code>msg.enabled</code> to <code>false</code> will disable the input.</p>
<p>If set to pass through mode a message arriving on the input will be evaluated for any colour format available
as Format. If the conversion fails #000000 will be used.</p>
</script>

View File

@@ -0,0 +1,57 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
var tc = require('../dist/js/tinycolor-min');
function ColourPickerNode(config) {
RED.nodes.createNode(this, config);
this.format = config.format;
this.outformat = config.outformat;
var node = this;
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
var done = ui.add({
node: node,
tab: tab,
group: group,
forwardInputMessages: config.passthru,
control: {
type: 'colour-picker',
label: config.label,
format: config.format,
showPicker: config.showPicker,
showSwatch: config.showSwatch,
showValue: config.showValue,
showHue: config.showHue,
showAlpha: config.showAlpha,
showLightness: config.showLightness,
square: (config.square == 'true') || false,
dynOutput: config.dynOutput,
allowEmpty: true,
order: config.order,
value: '',
width: config.width || group.config.width || 6,
height: config.height || 1
},
beforeSend: function (msg) {
if (node.outformat === 'object') {
var pay = tc(msg.payload);
if (node.format === 'rgb') { msg.payload = pay.toRgb(); }
if (node.format === 'hsl') { msg.payload = pay.toHsl(); }
if (node.format === 'hsv') { msg.payload = pay.toHsv(); }
}
msg.topic = config.topic || msg.topic;
},
convert: function(p,o,m) {
if (m.payload === undefined) { return; }
var colour = tc(m.payload);
return colour.toString(config.format);
}
});
node.on("close", done);
}
RED.nodes.registerType("ui_colour_picker", ColourPickerNode);
};

View File

@@ -0,0 +1,81 @@
<script type="text/javascript">
RED.nodes.registerType('ui_date_picker',{
category: 'dashboard',
color: 'rgb(176, 223, 227)',
defaults: {
name: {value: ''},
label: {value: 'date'},
group: {type: 'ui_group', required: true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0},
passthru: {value: true},
topic: {value: ''}
},
inputs:1,
outputs:1,
outputLabels: ["epoch mS"],
icon: "ui_date_picker.png",
paletteLabel: 'date picker',
label: function() { return this.name || (~this.label.indexOf("{{") ? null : this.label) || 'date picker'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
}
});
</script>
<script type="text/html" data-template-name="ui_date_picker">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-label">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-passthru"><i class="fa fa-arrow-right"></i> If <code>msg</code> arrives on input, pass through to output: </label>
<input type="checkbox" checked id="node-input-passthru" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-payload"><i class="fa fa-envelope-o"></i> When changed, send:</label>
</div>
<div class="form-row">
<label style="padding-left:25px; margin-right:-25px">Payload</label>
<label style="width:auto">Current value</label>
</div>
<div class="form-row">
<label for="node-input-topic" style="padding-left:25px; margin-right:-25px">Topic</label>
<input type="text" id="node-input-topic">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/html" data-help-name="ui_date_picker">
<p>Adds a date picker widget to the user interface.</p>
<p>The date display can be formatted in the Dashboard - Site tab using <a href="https://momentjs.com/docs/#/displaying/">
moment.js</a> formatting. For example <code>MM/DD/YYYY</code>, <code>Do MMM YYYY</code> or <code>YYYY-MM-DD</code>.</p>
<p>Setting <code>msg.enabled</code> to <code>false</code> will disable the input.</p>
</script>

View File

@@ -0,0 +1,47 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function DatePickerNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
var done = ui.add({
node: node,
tab: tab,
group: group,
forwardInputMessages: config.passthru,
control: {
type: 'date-picker',
label: config.label,
order: config.order,
ddd : new Date(),
width: config.width || group.config.width || 6,
height: config.height || 1
},
convert: function (p,o,m) {
var d = new Date(m.payload);
this.control.ddd = d;
return m.payload;
},
beforeEmit: function (msg, value) {
if (value === undefined) { return; }
value = new Date(value);
return { msg:msg, value:value };
},
convertBack: function (value) {
var d = new Date(value).valueOf();
return d;
},
beforeSend: function (msg) {
msg.topic = config.topic || msg.topic;
}
});
node.on("close", done);
}
RED.nodes.registerType("ui_date_picker", DatePickerNode);
};

View File

@@ -0,0 +1,163 @@
<script type="text/javascript">
RED.nodes.registerType('ui_dropdown',{
category: 'dashboard',
color: 'rgb(176, 223, 227)',
defaults: {
name: {value: ''},
label: {value: ''},
tooltip: {value: ''},
place: {value: 'Select option'},
group: {type: 'ui_group', required:true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0},
passthru: {value: true},
options: {value:[{value: '', label : ''}]},
payload: {value: ''},
topic: {value: ''}
},
inputs:1,
outputs:1,
icon: "ui_dropdown.png",
paletteLabel: 'dropdown',
label: function() { return this.name || (~this.label.indexOf("{{") ? null : this.label) || 'dropdown'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
function generateOption(i, option) {
var container = $('<li/>',{style:"background: #fff; margin:0; padding:8px 0px 0px; border-bottom: 1px solid #ccc;"});
var row = $('<div/>').appendTo(container);
var row2 = $('<div/>',{style:"padding-top:5px; padding-left:175px;"}).appendTo(container);
var row3 = $('<div/>',{style:"padding-top:5px; padding-left:120px;"}).appendTo(container);
$('<i style="color:#eee; cursor:move; margin-left:3px;" class="node-input-option-handle fa fa-bars"></i>').appendTo(row);
var valueField = $('<input/>',{class:"node-input-option-value",type:"text",style:"margin-left:7px; width:calc(50% - 32px);", placeholder: 'Value',value:option.value}).appendTo(row).typedInput({default:option.type||'str',types:['str','num','bool']});
var labelField = $('<input/>',{class:"node-input-option-label",type:"text",style:"margin-left:7px; width:calc(50% - 32px);", placeholder: 'Label', value:option.label}).appendTo(row);
var finalspan = $('<span/>',{style:"float:right; margin-right:8px;"}).appendTo(row);
var deleteButton = $('<a/>',{href:"#",class:"editor-button editor-button-small", style:"margin-top:7px; margin-left:5px;"}).appendTo(finalspan);
$('<i/>',{class:"fa fa-remove"}).appendTo(deleteButton);
deleteButton.click(function() {
container.css({"background":"#fee"});
container.fadeOut(300, function() {
$(this).remove();
});
});
$("#node-input-option-container").append(container);
}
$("#node-input-add-option").click(function() {
generateOption($("#node-input-option-container").children().length+1, {});
$("#node-input-option-container-div").scrollTop($("#node-input-option-container-div").get(0).scrollHeight);
});
for (var i=0; i<this.options.length; i++) {
var option = this.options[i];
generateOption(i+1,option);
}
$( "#node-input-option-container" ).sortable({
axis: "y",
handle:".node-input-option-handle",
cursor: "move"
});
},
oneditsave: function() {
var options = $("#node-input-option-container").children();
var node = this;
node.options = [];
options.each(function(i) {
var option = $(this);
var o = {
label: option.find(".node-input-option-label").val(),
value: option.find(".node-input-option-value").typedInput('value'),
type: option.find(".node-input-option-value").typedInput('type')
};
if (option.find(".node-input-option-value").typedInput('type') === "num") {
o.value = Number(o.value);
}
if (option.find(".node-input-option-value").typedInput('type') === "bool") {
o.value = (o.value == "true");
}
node.options.push(o);
});
},
oneditresize: function() {
}
});
</script>
<script type="text/html" data-template-name="ui_dropdown">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-tag"></i> Label</label>
<input type="text" id="node-input-label" placeholder="optional label">
</div>
<div class="form-row">
<label for="node-input-tooltip"><i class="fa fa-info-circle"></i> Tooltip</label>
<input type="text" id="node-input-tooltip" placeholder="optional tooltip">
</div>
<div class="form-row">
<label for="node-input-place"><i class="fa fa-tag"></i> Placeholder</label>
<input type="text" id="node-input-place" placeholder="optional placeholder">
</div>
<div class="form-row node-input-option-container-row" style="margin-bottom: 0px;width: 100%">
<label for="node-input-width" style="vertical-align:top"><i class="fa fa-list-alt"></i> Options</label>
<div id="node-input-option-container-div" style="box-sizing: border-box; border-radius: 5px; height: 257px; padding: 5px; border: 1px solid #ccc; overflow-y:scroll;display: inline-block; width: calc(70% + 15px);">
<ol id="node-input-option-container" style=" list-style-type:none; margin: 0;"></ol>
</div>
</div>
<div class="form-row">
<a href="#" class="editor-button editor-button-small" id="node-input-add-option" style="margin-top: 4px; margin-left: 103px;"><i class="fa fa-plus"></i> <span>option</span></a>
</div>
<div class="form-row">
<label style="width:auto" for="node-input-passthru"><i class="fa fa-arrow-right"></i> If <code>msg</code> arrives on input, pass through to output: </label>
<input type="checkbox" checked id="node-input-passthru" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
<input type="text" id="node-input-topic" placeholder="optional msg.topic">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/html" data-help-name="ui_dropdown">
<p>Adds a dropdown select box to the user interface.</p>
<p>Multiple value / label pairs can be added as required. If the label is not specified the
value will be used for both.</p>
<p>The configured value of the selected item will be returned as <code>msg.payload</code>.</p>
<p>Setting <code>msg.payload</code> to the value will set the pre-selected choice in the dropdown.</p>
<p>Optionally the <b>Topic</b> field can be used to set the <code>msg.topic</code> property.</p>
<p>The Options may be configured by inputting <code>msg.options</code> containing an array.
If just text then the value will be the same as the label, otherwise you can specify both by
using an object of <code>"label":"value"</code> pairs :</p>
<code>[ "Choice 1", "Choice 2", {"Choice 3":"3"} ]</code>
</script>

View File

@@ -0,0 +1,150 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function DropdownNode(config) {
RED.nodes.createNode(this, config);
this.pt = config.passthru;
this.state = [" "," "];
var node = this;
node.status({});
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
var control = {
type: 'dropdown',
label: config.label,
tooltip: config.tooltip,
place: config.place || "Select option",
order: config.order,
value: config.payload || node.id,
width: config.width || group.config.width || 6,
height: config.height || 1
};
for (var o=0; o<config.options.length; o++) {
config.options[o].label = config.options[o].label || config.options[o].value;
}
control.options = config.options;
var emitOptions;
var done = ui.add({
node: node,
tab: tab,
group: group,
forwardInputMessages: config.passthru,
control: control,
convert: function (payload, oldValue, msg) {
// convert msg
// as of now, only allow a full replacement of options
// beforeEmit is only called when a node linked to us sends a msg
// we are expecting to receive an "update options" msg
// which we expect to be an array of new options
// for convenience, we pass an indication to the node connected to this dropdown
// that this is an "update options" message coming from the input sender
// 'beforeEmit' is called before 'beforeSend', so we may pass in that info
// otherwise that convenience info would not be sent (would not cause any problems)...
emitOptions = {isOptionsValid:false, value:undefined, newOptions:undefined};
do {
if (!msg.options || !Array.isArray(msg.options)) { break; }
emitOptions.newOptions = [];
if (msg.options.length === 0) {
emitOptions.isOptionsValid = true;
break;
}
// could check whether or not all members have same type
for (var i = 0; i < msg.options.length; i++) {
var opt = msg.options[i];
if (opt === undefined || opt === null) { continue; }
switch (typeof opt) {
case 'number': {
opt = "" + opt;
emitOptions.newOptions.push({label:opt, value:opt});
break;
}
case 'string': {
emitOptions.newOptions.push({label:opt, value:opt});
break;
}
case 'object': {
// assuming object of {label:value}
for (var m in opt) {
if (opt.hasOwnProperty(m)) {
emitOptions.newOptions.push({label:m, value:opt[m]});
}
}
break;
}
default:
// do nothing, just continue with next option
}
}
// send null object on change of menu list
if (emitOptions.newOptions.length > 0) { emitOptions.value = null; }
// or send the preselected value
if (msg.payload) { emitOptions.value = msg.payload; }
emitOptions.isOptionsValid = true;
} while (false);
// finally adjust msg to reflect the input
msg._fromInput = true;
if (emitOptions.isOptionsValid) {
control.options = emitOptions.newOptions;
control.value = emitOptions.value;
}
else {
if (msg.options) {
node.error("ERR: Invalid Options", msg);
}
}
if (msg.hasOwnProperty("payload")) {
emitOptions.value = msg.payload;
control.value = emitOptions.value;
emitOptions._fromInput = true;
return emitOptions;
}
// we do not overide payload here due to 'opt.emitOnlyNewValues' in ui.js
// when undefined is returned, msg will not be forwarded
return emitOptions.isOptionsValid ? emitOptions : undefined; // always pass entire object (newValue == oldValue)
},
beforeEmit: function (msg, newValue) {
return emitOptions;
},
beforeSend: function (msg) {
var val = "";
if (msg._fromInput) {
delete msg.options;
msg.payload = emitOptions.value;
}
for (var i=0; i<control.options.length; i++) {
if (control.options[i].value === msg.payload) { val = control.options[i].label; }
}
msg.topic = config.topic || msg.topic;
if (node.pt) {
node.status({shape:"dot",fill:"grey",text:val});
}
else {
node.state[1] = val;
node.status({shape:"dot",fill:"grey",text:node.state[1] + " | " + node.state[1]});
}
}
});
if (!node.pt) {
node.on("input", function(msg) {
node.state[0] = msg.payload;
node.status({shape:"dot",fill:"grey",text:node.state[0] + " | " + node.state[1]});
});
}
node.on("close", done);
}
RED.nodes.registerType("ui_dropdown", DropdownNode);
};

View File

@@ -0,0 +1,314 @@
<script type="text/javascript">
RED.nodes.registerType('ui_form',{
category: 'dashboard',
color: 'rgb(176, 223, 227)',
defaults: {
name: {value: ''},
label: {value: ''},
group: {type: 'ui_group', required: true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0},
options: {value:[{value:'', label :'', type:'', required:true}], validate:function(value) {
if (value.length ) {
for (var i = 0; i < value.length; i++) {
if (!value[i].value) {
return false;
}
}
}
else {
return false;
}
return true;
}, required:true},
formValue:{value:{}},
payload: {value: ''},
submit: {value: "submit"},
cancel: {value: "cancel"},
topic: {value: ''}
},
inputs:1,
outputs:1,
icon: "ui_form.png",
paletteLabel: 'form',
label: function() { return this.name || this.label || 'form'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
if ($("#node-input-submit").val() === null) { $("#node-input-submit").val("submit"); }
if ($("#node-input-cancel").val() === null) { $("#node-input-cancel").val("cancel"); }
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
this.resizeRule = function(option,newWidth) {
//option.find(".node-input-option-type").width(newWidth);
// option.find(".node-input-option-label").width(newWidth);
// option.find(".node-input-option-value").width(newWidth);
}
function generateOption(i, option) {
var container = $('<li/>',{style:"background: #fff; margin:0; padding:8px 0px 0px; border-bottom: 1px solid #ccc;"});
var row = $('<div/>').appendTo(container);
var row2 = $('<div/>',{style:"padding-top:5px; padding-left:175px;"}).appendTo(container);
var row3 = $('<div/>',{style:"padding-top:5px; padding-left:120px;"}).appendTo(container);
$('<i style="color:#eee; cursor:move; margin-left:3px;" class="node-input-option-handle fa fa-bars"></i>').appendTo(row);
var labelField = $('<input/>',{class:"node-input-option-label",type:"text",style:"margin-left:7px; width:20%;", placeholder: 'e.g. Name', value:option.label}).appendTo(row);//.typedInput({default:'str',types:['str', 'num']});
var valueClass ="node-input-option-value"
if (!option.value) { valueClass ="node-input-option-value input-error"; }
var valueField = $('<input/>',{class:valueClass,type:"text",style:"margin-left: 7px; width: 20%;", placeholder: 'e.g. name',value:option.value}).appendTo(row);//.typedInput({default:'str',types:['str','num','bool']});
valueField.keyup(function() {
if ($(this).val() && $(this).hasClass('input-error')) {
$(this).removeClass('input-error')
}
else {
if (!$(this).val()) {
$(this).addClass('input-error')
}
}
});
// var typeField = $('<input/>',{class:"node-input-option-type",type:"text",style:"margin-left: 7px; width: 135px;", placeholder: 'Type', value:option.type}).appendTo(row).typedInput({default:'str',types:['str', 'num']});
var typeField = $('<select/>',{class:"node-input-option-type",type:"text",style:"margin-left:7px; width:16%"}).appendTo(row);//.typedInput({default:'str',types:['str', 'num']});
var arr = [
{val : "text", text: 'Text'},
{val : "multiline", text: 'Multiline'},
{val : "number", text: 'Number'},
{val : "email", text: 'E-mail'},
{val : "password", text: 'Password'},
{val : "checkbox", text: 'Checkbox'},
{val : "switch", text: 'Switch'},
{val : "date", text: 'Date'}
];
//var sel = $('<select>').appendTo('body');
$(arr).each(function() {
var isSelected= false;
if (option.type == this.val) {
isSelected = true;
}
typeField.append($("<option>").attr('value',this.val).text(this.text).prop('selected',isSelected));
});
//var labelForRequried = $('<span/>',{style:"margin: 10px;"}).text('Required').appendTo(row);
var requiredContainer= $('<div/>',{style:"display:inline-block; height:34px; width:13%; vertical-align: middle"}).appendTo(row);
var requiredInnerContainer= $('<div/>',{style:"left:35%; position:relative; width:30px"}).appendTo(requiredContainer);
var reqRow=$("<label />",{class:"switch",style:"top:10px; width:30px;"}).appendTo(requiredInnerContainer);
//var required = $('<input/>',{class:"node-input-option-required",style:"margin: 5px;width:19%",type:"checkbox", checked:option.required}).appendTo(row);//labelForRequried);//.typedInput({default:'str',types:['str', 'num']});
var required = $('<input/>',{class:"node-input-option-required", type:"checkbox", checked:option.required, style:"vertical-align:top;"}).appendTo(reqRow);//labelForRequried);//.typedInput({default:'str',types:['str', 'num']});
var reqDiv=$("<div />",{class:"slider round"}).appendTo(reqRow);
var vis = option.rows ? 'visible' : 'hidden';
var rowsField = $('<input/>',{class:"node-input-option-rows", type:"number", style:"width:10%;", placeholder:'Rows', value:option.rows }).css('visibility',vis).appendTo(row);
var finalspan = $('<div/>',{style:"display:inline-block; width:5%;"}).appendTo(row);
var deleteButton = $('<a/>',{href:"#",class:"editor-button", style:"font-size:1.3em; left:45%; position:relative;"}).appendTo(finalspan);
$('<i/>',{class:"fa fa-trash-o"}).appendTo(deleteButton);
typeField.change(function(e){
if (e.target.value != 'multiline') {
rowsField.val(undefined)
option.rows = null;
rowsField.css('visibility','hidden')
} else {
rowsField.css('visibility','visible')
if (!rowsField[0].value) rowsField[0].value = 3;
}
})
deleteButton.click(function() {
container.find(".node-input-option-value").removeAttr('required')
container.css({"background":"#fee"});
container.fadeOut(300, function() {
$(this).remove();
});
});
$("#node-input-option-container").append(container);
}
$("#node-input-add-option").click(function() {
generateOption($("#node-input-option-container").children().length+1, {});
$("#node-input-option-container-div").scrollTop($("#node-input-option-container-div").get(0).scrollHeight);
});
for (var i=0; i<this.options.length; i++) {
var option = this.options[i];
generateOption(i+1,option);
}
$( "#node-input-option-container" ).sortable({
axis: "y",
handle:".node-input-option-handle",
cursor: "move"
});
},
oneditsave: function() {
var options = $("#node-input-option-container").children();
var node = this;
node.options = [];
node.formValue = {};
options.each(function(i) {
var option = $(this);
var o = {
label: option.find(".node-input-option-label").val(),//typedInput('value'),
value: option.find(".node-input-option-value").val(),//typedInput('value'),
type: option.find(".node-input-option-type").val(),//typedInput('value')
required: option.find(".node-input-option-required").is(':checked'),
rows: parseInt(option.find(".node-input-option-rows").val())
};
// o.value= o.value||o.label||(o.type+"_"+i);
node.formValue[o.value]= o.type == "checkbox" || o.type == "switch" ? false : "";
node.options.push(o);
});
},
oneditresize: function() {
var options = $("#node-input-option-container").children();
var newWidth = ($("#node-input-option-container").width() - 175)/2;
var node = this;
options.each(function(i) {
node.resizeRule($(this),newWidth);
});
}
});
</script>
<script type="text/html" data-template-name="ui_form">
<style>
.switch {
position: relative;
display: inline-block;
width: 30px;
height: 18px;
}
.switch input {display:none;}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 15px;
width: 15px;
left: 2px;
bottom: 2px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #910000;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(11px);
-ms-transform: translateX(11px);
transform: translateX(11px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
</style>
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-tag"></i> Label</label>
<input type="text" id="node-input-label" placeholder="optional label">
</div>
<div class="form-row node-input-option-container-row" style="margin-bottom:0px; width:100%; min-width:520px">
<label style="vertical-align:top;"><i class="fa fa-list-alt"></i> Form elements</label>
<div style="display:inline-block; width:78%; border:1px solid #ccc; border-radius:5px; box-sizing:border-box;">
<div style="width:100%; display: inline-block; background-color:#f3f3f3; padding-top:10px; padding-buttom:10px; border-top:0px solid; border-radius:5px 5px 0 0; border-bottom:1px solid #ccc;">
<div style="width:94%; display:inline-block; margin-left:27px">
<div style="width:20%; text-align:center; float:left;">Label</div>
<div style="width:20%; text-align:center; float:left; margin-left:9px">Name</div>
<div style="margin-left:7px; width:16%; text-align:center; float:left; margin-left:9px">Type</div>
<div style="width:16%; text-align:center; float:left;">Required</div>
<div style="width:10%; text-align:center; float:left;">Rows</div>
<div style="width:12%; text-align:center; float:left;">Remove</div>
</div>
</div>
<div id="node-input-option-container-div" style=" height: 257px; padding: 5px; overflow-y:scroll;">
<ol id="node-input-option-container" style=" list-style-type:none; margin: 0;"></ol>
</div>
</div>
</div>
<div class="form-row">
<a href="#" class="editor-button editor-button-small" id="node-input-add-option" style="margin-top: 4px; margin-left: 103px;"><i class="fa fa-plus"></i> <span>element</span></a>
</div>
<div class="form-row">
<label for="node-input-submit"><i class="fa fa-square"></i> Buttons</label>
<i class="fa fa-thumbs-o-up"></i> <input type="text" id="node-input-submit" placeholder="submit button text" style="width:35%;">
<span style="margin-left:16px"><i class="fa fa-thumbs-o-down"></i></span>
<input type="text" id="node-input-cancel" placeholder="cancel button text" style="width:35%;">
</div>
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
<input type="text" id="node-input-topic" placeholder="optional msg.topic">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/html" data-help-name="ui_form">
<p>Adds a form to user interface.</p>
<p>Helps to collect multiple value from the user on submit button click as an object in <code> msg.payload</code> </p>
<p>Multiple input elements can be added using add elements button</p>
<p>Each element contains following components:</p>
<ul>
<li> <b>Label</b> : Value that will be the label of the element in the user interface</li>
<li> <b>Name</b> : Represents the key (variable name) in the <code>msg.payload</code> in which the value of the corresponding element present</li>
<li> <b>Type</b> : Drop drown option to select the type of input element</li>
<li> <b>Required</b> : On switching on the user has to supply the value before submitting</li>
<li> <b>Rows</b> : number of UI rows for multiline text input</li>
<li> <b>Delete</b> : To remove the current element form the form</li>
</ul>
<p>Optionally the <b>Topic</b> field can be used to set the <code>msg.topic</code> property.</p>
<p>The Cancel button can be hidden by setting it's value to be blank "".</p>
</script>

View File

@@ -0,0 +1,38 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function FormNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
var done = ui.add({
node: node,
tab: tab,
group: group,
forwardInputMessages: false,
control: {
type: 'form',
label: config.label,
order: config.order,
value: config.payload || node.id,
width: config.width || group.config.width || 6,
height: config.height || config.options.length ,
options: config.options,
formValue: config.formValue,
submit: config.submit,
cancel: config.cancel,
sy: ui.getSizes().sy
},
beforeSend: function (msg) {
msg.topic = config.topic || undefined;
}
});
node.on("close", done);
}
RED.nodes.registerType("ui_form", FormNode);
};

View File

@@ -0,0 +1,183 @@
<style>
input.gauge-color {
width: 100px;
text-align: center;
}
input.gauge-color::-webkit-color-swatch {
border: none;
}
</style>
<script type="text/javascript">
RED.nodes.registerType('ui_gauge',{
category: 'dashboard',
color: 'rgb(119, 198, 204)',
defaults: {
name: {value: ''},
group: {type: 'ui_group', required: true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v || 0;
var currentGroup = $('#node-input-group').val() || this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0},
gtype: {value: 'gage'},
title: {value: 'gauge'},
label: {value: 'units'},
format: {value: '{{value}}'},
min: {value: 0, required: true, validate: RED.validators.number()},
max: {value: 10, required: true, validate: RED.validators.number()},
colors: {value: ["#00B500","#E6E600","#CA3838"]},
seg1: {value: ""},
seg2: {value: ""}
},
inputs:1,
outputs:0,
inputLabels: function() { return this.min+" - "+this.max; },
align: "right",
icon: "ui_gauge.png",
paletteLabel: 'gauge',
label: function() { return this.name || (~this.title.indexOf("{{") ? null : this.title) || ((this.gtype === "gage") ? "gauge" : this.gtype) || 'gauge'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
var setColour = function(id, value) {
$(id).val(value);
$(id).css("background-color", value);
var rgb = tinycolor(value).toRgb();
var level = ((rgb.r*299) + (rgb.g*587) + (rgb.b*114))/1000;
var textColor = (level >= 128) ? '#111111' : '#eeeeee';
$(id).css("color", textColor);
}
$(".gauge-color").on("change", function() {
setColour("#"+$(this).attr("id"), $(this).val());
});
var defaultColors = ['#00B500', '#E6E600', '#CA3838'];
if (this.colors) {
for (var i=0; i<this.colors.length; i++) {
var value = this.colors[i] || defaultColors[i];
setColour("#node-input-color"+(i+1), value);
}
}
else {
for (var j=0; j<defaultColors.length; j++) {
setColour("#node-input-color"+(j+1), defaultColors[j]);
}
}
if (this.gtype === undefined) {
this.gtype = "gage";
$("#node-input-gtype").val("gage");
}
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
$("#node-input-gtype").on("change", function() {
if ($(this).val() === "wave") {
$("#ui-gauge-format").hide();
}
else {
$("#ui-gauge-format").show();
}
if (($(this).val() === "compass") || ($(this).val() === "wave")) {
$("#ui-gauge-colours").hide();
$("#ui-gauge-segments").hide();
}
else {
$("#ui-gauge-colours").show();
$("#ui-gauge-segments").show();
}
});
$("#node-input-min").on("change", function() {
$("#seg-min").text($(this).val());
});
$("#node-input-max").on("change", function() {
$("#seg-max").text($(this).val());
});
},
oneditsave: function() {
this.colors = [$("#node-input-color1").val(),$("#node-input-color2").val(),$("#node-input-color3").val()];
}
});
</script>
<script type="text/html" data-template-name="ui_gauge">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-gtype"><i class="fa fa-list"></i> Type</label>
<select id="node-input-gtype" style="width:200px !important">
<option value="gage">Gauge</option>
<option value="donut">Donut</option>
<option value="compass">Compass</option>
<option value="wave">Level</option>
</select>
</div>
<div id="ui-gauge-labels">
<div class="form-row">
<label for="node-input-title"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-title">
</div>
<div class="form-row" id="ui-gauge-format">
<label for="node-input-format"><i class="fa fa-i-cursor"></i> Value format</label>
<input type="text" id="node-input-format" placeholder="{{value}}">
</div>
<div class="form-row" id="ui-gauge-units">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Units</label>
<input type="text" id="node-input-label" placeholder="optional sub-label">
</div>
</div>
<div class="form-row">
<label for="node-input-min">Range</label>
<span for="node-input-min">min</span>
<input type="text" id="node-input-min" style="width:80px">
<span for="node-input-max" style="margin-left:20px;">max</span>
<input type="text" id="node-input-max" style="width:80px">
</div>
<div class="form-row" id="ui-gauge-colours">
<label for="node-input-color1">Colour gradient</label>
<input type="color" id="node-input-color1" class="gauge-color" style="width:100px;"/>
<input type="color" id="node-input-color2" class="gauge-color" style="width:100px;"/>
<input type="color" id="node-input-color3" class="gauge-color" style="width:100px;"/>
</div>
<div class="form-row" id="ui-gauge-segments">
<label>Sectors</label>
<span id="seg-min" style="display:inline-block; width:40px;">0</span>...
<input type="text" id="node-input-seg1" style="text-align:center; width:87px;" placeholder="optional"> ...
<input type="text" id="node-input-seg2" style="text-align:center; width:87px;" placeholder="optional"> ...
<span id="seg-max" style="display:inline-block; width:40px; text-align:right">10</span>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/html" data-help-name="ui_gauge">
<p>Adds a gauge type widget to the user interface.</p>
<p>The <code>msg.payload</code> is searched for a numeric <i>value</i> and is formatted in accordance with
the defined <b>Value Format</b>, which can then be formatted using
<a href="https://scotch.io/tutorials/all-about-the-built-in-angularjs-filters" target="_blank">Angular filters</a>.</p>
<p>For example : <code>{{value | number:1}}%</code> will round the value to one decimal place and append a % sign.</p>
<p>The colours of each of 3 sectors can be specified and the gauge will blend between them.
The colours should be specified in hex (#rrggbb) format.</p>
<p>If you specify numbers for the sectors then the colours changes per sector.
If not specified the colours are blended across the total range.</p>
<p>The gauge has several modes. Regular gauge, donut, compass and wave.</p>
<p>The label can also be set by a message property by setting
the field to the name of the property, for example <code>{{msg.topic}}</code>.</p>
</script>

View File

@@ -0,0 +1,85 @@
module.exports = function (RED) {
var ui = require('../ui')(RED);
function GaugeNode(config) {
RED.nodes.createNode(this, config);
this.colors = config.colors || ["#00B500","#E6E600","#CA3838"];
var node = this;
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
if (config.width === "0") { delete config.width; }
if (config.height === "0") { delete config.height; }
if (config.height === "1") { config.hideMinMax = true; }
node.autoheight = parseInt(group.config.width*0.5+1.5) || 4;
if (config.gtype && config.gtype === "wave") { node.autoheight = parseInt(group.config.width*0.75+0.5); }
if (config.gtype && config.gtype === "donut") { node.autoheight = parseInt(group.config.width -1); }
if (config.gtype && config.gtype === "compass") { node.autoheight = parseInt(group.config.width -1); }
var sizes = ui.getSizes();
var theme = ui.getTheme();
if (theme === undefined) {
theme = {"group-textColor":{value:"#000"}};
theme["widget-textColor"] = {value:"#000"};
theme["widget-backgroundColor"] = {value:'#1784be'};
}
var gageoptions = {};
gageoptions.lineWidth = {'theme-dark':0.75};
gageoptions.pointerOptions = {'theme-dark':{color:'#8e8e93'}, 'theme-custom':theme["group-textColor"].value};
gageoptions.backgroundColor = {'theme-dark':'#515151', 'theme-custom':theme["widget-textColor"].value };
gageoptions.compassColor = {'theme-dark':'#0b8489', 'theme-light':'#1784be', 'theme-custom':theme["widget-backgroundColor"].value};
var waveoptions = {};
waveoptions.circleColor = {'theme-dark':'#097479', 'theme-light':'#0094ce', 'theme-custom':theme["widget-backgroundColor"].value};
waveoptions.waveColor = {'theme-dark':'#097479', 'theme-light':'#0094ce', 'theme-custom':theme["widget-backgroundColor"].value};
waveoptions.textColor = {'theme-dark':'#0b8489', 'theme-light':'#1784be', 'theme-custom':theme["widget-textColor"].value};
waveoptions.waveTextColor = {'theme-dark':'#0fbbc3', 'theme-light':'#a4dbf8', 'theme-custom':theme["widget-textColor"].value};
var done = ui.add({
node: node,
tab: tab,
group: group,
emitOnlyNewValues: false,
control: {
type: 'gauge',
name: config.name,
label: config.title,
units: config.label,
order: config.order,
value: config.min,
format: config.format,
gtype: config.gtype || 'gage',
min: (parseFloat(config.min) < parseFloat(config.max)) ? parseFloat(config.min) : parseFloat(config.max),
seg1: (parseFloat(config.seg1) < parseFloat(config.seg2)) ? parseFloat(config.seg1) : parseFloat(config.seg2),
seg2: (parseFloat(config.seg1) < parseFloat(config.seg2)) ? parseFloat(config.seg2) : parseFloat(config.seg1),
max: (parseFloat(config.min) < parseFloat(config.max)) ? parseFloat(config.max) : parseFloat(config.min),
reverse: (parseFloat(config.max) < parseFloat(config.min)) ? true : false,
sizes: sizes,
hideMinMax: config.hideMinMax,
width: config.width || group.config.width || 6,
height: config.height || node.autoheight,
colors: node.colors,
gageoptions: gageoptions,
waveoptions: waveoptions,
options: null
},
convert: function(p,o,m) {
var form = config.format.replace(/{{/g,"").replace(/}}/g,"").replace(/\s/g,"") || "_zzz_zzz_zzz_";
var value = RED.util.getMessageProperty(m,form);
if (value !== undefined) {
if (!isNaN(parseFloat(value))) { value = parseFloat(value); }
return value;
}
if (!isNaN(parseFloat(p))) { p = parseFloat(p); }
return p;
//return ui.toFloat.bind(this, config);
}
});
node.on("close", done);
}
RED.nodes.registerType("ui_gauge", GaugeNode);
};

View File

@@ -0,0 +1,86 @@
<script type="text/javascript">
// convert to i18 text
function c_ui_group(x) {
return RED._("node-red-dashboard/ui_group:ui_group."+x);
}
RED.nodes.registerType('ui_group',{
category: 'config',
defaults: {
name: {value: c_ui_group("label.default")},
tab: {type:"ui_tab", required: true },
order: {value: 0},
disp: {value: true},
width: {value: 6},
collapse: {value: false},
disabled: {value: false},
hidden: {value: false}
},
sort: function(A,B) {
if (A.tab !== B.tab) {
var tabA = RED.nodes.node(A.tab);
var tabB = RED.nodes.node(B.tab);
if (!tabA && tabB) {
return -1;
}
else if (tabA && !tabB) {
return 1;
}
else {
return tabA.order - tabB.order;
}
}
return A.order - B.order;
},
paletteLabel: 'dashboard group',
label: function() {
var tabNode = RED.nodes.node(this.tab);
if (tabNode) {
return "["+(tabNode.name||c_ui_group("label.tab"))+"] " + (this.name || c_ui_group("label.group"));
}
return "["+c_ui_group("label.unassigned")+"] " + (this.name || c_ui_group("label.group"));
},
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
$("#node-input-size").elementSizer({
width: "#node-config-input-width",
auto: false
});
$("#node-config-input-disp").on("change", function() {
if ($("#node-config-input-disp").is(":checked")) {
$("#group-collapse-flag").show();
}
else {
$("#group-collapse-flag").hide();
$("#node-config-input-collapse").prop("checked",false);
}
});
}
});
</script>
<script type="text/html" data-template-name="ui_group">
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="ui_group.label.name"></span></label>
<input type="text" id="node-config-input-name">
</div>
<div class="form-row">
<label for="node-config-input-tab"><i class="fa fa-table"></i> <span data-i18n="ui_group.label.tab"></span></label>
<input type="text" id="node-config-input-tab">
</div>
<div class="form-row">
<label for="node-config-input-width"><i class="fa fa-arrows-h"></i> <span data-i18n="ui_group.label.width"></span></label>
<input type="hidden" id="node-config-input-width">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<input style="margin:8px 0 10px 102px; width:20px;" type="checkbox" checked id="node-config-input-disp"> <label style="width:auto" for="node-config-input-disp"><span data-i18n="ui_group.display-name"></span></label>
</div>
<div class="form-row" id="group-collapse-flag">
<input style="margin:8px 0 10px 102px; width:20px;" type="checkbox" id="node-config-input-collapse"> <label style="width:auto" for="node-config-input-collapse"><span data-i18n="ui_group.collapse-name"></span></label>
</div>
</script>
<script type="text/html" data-help-name="ui_group">
<p>Group</p>
</script>

View File

@@ -0,0 +1,19 @@
module.exports = function(RED) {
function GroupNode(config) {
RED.nodes.createNode(this, config);
this.config = {
name: config.name,
disp: config.disp,
width: config.width,
order: config.order,
tab: config.tab,
collapse: config.collapse || false
};
if (!this.config.hasOwnProperty("disp")) { this.config.disp = true; }
if (this.config.disp !== false) { this.config.disp = true; }
if (!this.config.hasOwnProperty("collapse")) { this.config.collapse = false; }
}
RED.nodes.registerType("ui_group", GroupNode);
};

View File

@@ -0,0 +1,68 @@
<script type="text/javascript">
RED.nodes.registerType('ui_link',{
category: 'config',
color: 'rgb( 63, 173, 181)',
defaults: {
name: {value: 'Google'},
link: {value: 'https://www.google.com'},
icon: {value: 'open_in_browser'},
target: {value: 'newtab', validate :function() { return true; }},
order: {value: 0}
},
inputs:0,
outputs:0,
hasUsers: false,
align: "right",
icon: "ui_link.png",
paletteLabel: 'link',
label: function() { return this.name || 'link'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
document.getElementById('node-config-input-target-opentab').checked = (this.target === 'newtab');
document.getElementById('node-config-input-target-openiframe').checked = (this.target === 'iframe');
document.getElementById('node-config-input-target-openthis').checked = (this.target === 'thistab');
},
oneditsave : function () {
var t = 'iframe';
if (document.getElementById('node-config-input-target-opentab').checked) { t = 'newtab'; }
if (document.getElementById('node-config-input-target-openthis').checked) { t = 'thistab'; }
this.target = t;
},
onadd: function() {
//console.log("PING");
}
});
</script>
<script type="text/html" data-template-name="ui_link">
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="ui_link.label.name"></span></label>
<input type="text" id="node-config-input-name">
</div>
<div class="form-row">
<label for="node-config-input-link"><i class="fa fa-link"></i> <span data-i18n="ui_link.label.link"></span></label>
<input type="text" id="node-config-input-link">
</div>
<div class="form-row">
<label for="node-config-input-icon"><i class="fa fa-image"></i> <span data-i18n="ui_link.label.icon"></span></label>
<input type="text" id="node-config-input-icon">
</div>
<div class="form-row">
<label><i class="fa fa-link"></i> <span data-i18n="ui_link.label.open-in"></span></label>
<input type="radio" id="node-config-input-target-opentab" name="open-link-method" style="width:20px; margin-top:0px; margin-bottom:5px" checked>
<label for="node-config-input-target-opentab" data-i18n="ui_link.label.new-tab"></label><br/>
<input type="radio" id="node-config-input-target-openthis" name="open-link-method" style="width:20px; margin-left:104px; margin-top:0px; margin-bottom:5px">
<label for="node-config-input-target-openthis" data-i18n="ui_link.label.this-tab"></label><br/>
<input type="radio" id="node-config-input-target-openiframe" name="open-link-method" style="width:20px; margin-left:104px; margin-top:0px; margin-bottom:5px">
<label for="node-config-input-target-openiframe" data-i18n="ui_link.label.iframe"></label>
</div>
<div class="form-tips" data-i18n="[html]ui_link.tip"></div>
</script>
<script type="text/html" data-help-name="ui_link">
<p>The <b>Icon</b> can be defined, as either a <a href="https://klarsys.github.io/angular-material-icons/" target="_blank">Material Design icon</a>
<i>(e.g. 'check', 'close')</i> or a <a href="https://fontawesome.com/v4.7.0/icons/" target="_blank">Font Awesome icon</a>
<i>(e.g. 'fa-fire')</i>, or a <a href="https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md">Weather icon</a>.
You can use the full set of google material icons if you add 'mi-' to the icon name. e.g. 'mi-videogame_asset'.</p>
<p>The <b>Open in</b> field controls whether the link opens in a <i>New Tab</i>, or if the link is opened within an <i>iframe</i> on the same page. Some sites, including Google, do not allow the rendering of their page inside an iframe. If you select the <i>iframe</i> option and the site does not show, this is simply because that site forbids the use of it inside an iframe.</p>
</script>

View File

@@ -0,0 +1,12 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function LinkNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var done = ui.addLink(config.name, config.link, config.icon, config.order, config.target);
node.on("close", done);
}
RED.nodes.registerType("ui_link", LinkNode);
};

View File

@@ -0,0 +1,118 @@
<script type="text/javascript">
RED.nodes.registerType('ui_numeric',{
category: 'dashboard',
color: 'rgb(176, 223, 227)',
defaults: {
name: {value: ''},
label: {value: 'numeric'},
tooltip: {value: ''},
group: {type: 'ui_group', required: true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0},
wrap: {value: false},
passthru: {value: true},
topic: {value: ''},
format: {value: '{{value}}'},
min: {value: 0, required: true, validate: RED.validators.number()},
max: {value: 10, required: true, validate: RED.validators.number()},
step: {value: 1}
},
inputs:1,
outputs:1,
outputLabels: function() { return this.min+" - "+this.max; },
icon: "ui_numeric.png",
paletteLabel: 'numeric',
label: function() { return this.name || (~this.label.indexOf("{{") ? null : this.label) || 'numeric'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
}
});
</script>
<script type="text/html" data-template-name="ui_numeric">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-label">
</div>
<div class="form-row">
<label for="node-input-tooltip"><i class="fa fa-info-circle"></i> Tooltip</label>
<input type="text" id="node-input-tooltip" placeholder="optional tooltip">
</div>
<div class="form-row">
<label for="node-input-format"><i class="fa fa-i-cursor"></i> Value Format</label>
<input type="text" id="node-input-format" placeholder="{{value}}">
</div>
<div class="form-row">
<label for="node-input-min"><i class="fa fa-arrows-h"></i> Range</label>
<span for="node-input-min">min</span>
<input type="text" id="node-input-min" style="width:60px">
<span for="not-input-max" style="margin-left:22px;">max</span>
<input type="text" id="node-input-max" style="width:60px">
<span for="not-input-step" style="margin-left:22px;">step</span>
<input type="text" id="node-input-step" style="width:60px">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-wrap"><i class="fa fa-refresh"></i> Wrap value from max to min and min to max.</label>
<input type="checkbox" id="node-input-wrap" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-passthru"><i class="fa fa-arrow-right"></i> If <code>msg</code> arrives on input, pass through to output: </label>
<input type="checkbox" checked id="node-input-passthru" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-payload"><i class="fa fa-envelope-o"></i> When changed, send:</label>
</div>
<div class="form-row">
<label style="padding-left:25px; margin-right:-25px">Payload</label>
<label style="width:auto">Current value</label>
</div>
<div class="form-row">
<label for="node-input-topic" style="padding-left:25px; margin-right:-25px">Topic</label>
<input type="text" id="node-input-topic">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/html" data-help-name="ui_numeric">
<p>Adds a numeric input widget to the user interface.</p>
<p>The user can set the value between
the limits (<b>min</b> and <b>max</b>). Each value change will generate a <code>msg.payload</code>.</p>
<p>If <b>Topic</b> is specified, it will be added as <code>msg.topic</code>.<p>
<p>Any input messages will be converted to a number, the <b>min</b> value will be used if conversion fails,
and it will update the user interface. If the value changes, it will also be passed to the output.</p>
<p>The <b>Value Format</b> field can be used to change the displayed format. For example, a <b>Value Format</b>
of <code>{{value}} %</code>
with a value of <b>23</b> will show <b>23 %</b> on the user interface. The <b>Value Format</b> field can contain
HTML or Angular filters to format the output (eg: <code>&amp;deg;</code> will show the degree symbol).</p>
<p>Setting the Value Format field to <code>{{msg.payload}}</code> will make the input field editable so you can type in a number.</p>
<p>The label can also be set by a message property by setting
the field to the name of the property, for example <code>{{msg.topic}}</code>.</p>
<p>Setting <code>msg.enabled</code> to <code>false</code> will disable the widget output.</p>
</script>

View File

@@ -0,0 +1,60 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function NumericNode(config) {
RED.nodes.createNode(this, config);
this.pt = config.passthru;
this.state = [" "," "];
var node = this;
node.status({});
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
var done = ui.add({
node: node,
tab: tab,
group: group,
forwardInputMessages: config.passthru,
control: {
type: 'numeric',
label: config.label,
tooltip: config.tooltip,
order: config.order,
format: config.format,
pre: config.format.split('{{')[0] || "",
post: config.format.split('}}')[1] || "",
value: Number(config.min),
min: Number(config.min),
max: Number(config.max),
step: Number(config.step || 1),
wrap: config.wrap || false,
width: config.width || group.config.width || 6,
height: config.height || 1,
ed: (config.format.includes("value") ? false : true)
},
beforeSend: function (msg) {
msg.payload = parseFloat(msg.payload);
msg.topic = config.topic || msg.topic;
if (node.pt) {
node.status({shape:"dot",fill:"grey",text:msg.payload});
}
else {
node.state[1] = msg.payload;
node.status({shape:"dot",fill:"grey",text:node.state[1] + " | " + node.state[1]});
}
},
convert: ui.toFloat.bind(this, config)
});
if (!node.pt) {
node.on("input", function(msg) {
node.state[0] = msg.payload;
node.status({shape:"dot",fill:"grey",text:node.state[0] + " | " + node.state[1]});
});
}
node.on("close", done);
}
RED.nodes.registerType("ui_numeric", NumericNode);
};

View File

@@ -0,0 +1,116 @@
<script type="text/javascript">
RED.nodes.registerType('ui_slider',{
category: 'dashboard',
color: 'rgb(176, 223, 227)',
defaults: {
name: {value: ''},
label: {value: 'slider'},
tooltip: {value: ''},
group: {type: 'ui_group', required: true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0},
passthru: {value: true},
outs: {value: 'all'},
topic: {value: ''},
min: {value: 0, required:true, validate:RED.validators.number()},
max: {value: 10, required:true, validate:RED.validators.number()},
step: {value: 1}
},
inputs:1,
outputs:1,
outputLabels: function() { return this.min+" - "+this.max; },
icon: "ui_slider.png",
paletteLabel: 'slider',
label: function() { return this.name || (~this.label.indexOf("{{") ? null : this.label) || 'slider'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
if (!$("#node-input-outs").val()) { $("#node-input-outs").val("all") }
}
});
</script>
<script type="text/html" data-template-name="ui_slider">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-label">
</div>
<div class="form-row">
<label for="node-input-tooltip"><i class="fa fa-info-circle"></i> Tooltip</label>
<input type="text" id="node-input-tooltip" placeholder="optional tooltip">
</div>
<div class="form-row">
<label for="node-input-min"><i class="fa fa-arrows-h"></i> Range</label>
<span for="node-input-min">min</span>
<input type="text" id="node-input-min" style="width:60px">
<span for="not-input-max" style="margin-left:22px;">max</span>
<input type="text" id="node-input-max" style="width:60px">
<span for="not-input-step" style="margin-left:22px;">step</span>
<input type="text" id="node-input-step" style="width:60px">
</div>
<div class="form-row">
<label for="node-input-outs"><i class="fa fa-sign-out"></i> Output</label>
<select id="node-input-outs" style="width:204px">
<option value="all">continuously while sliding</option>
<option value="end">only on release</option>
</select>
</div>
<div class="form-row">
<label style="width:auto" for="node-input-passthru"><i class="fa fa-arrow-right"></i> If <code>msg</code> arrives on input, set slider to new payload value: </label>
<input type="checkbox" checked id="node-input-passthru" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-payload"><i class="fa fa-envelope-o"></i> When changed, send:</label>
</div>
<div class="form-row">
<label style="padding-left:25px; margin-right:-25px">Payload</label>
<label style="width:auto">Current value</label>
</div>
<div class="form-row">
<label for="node-input-topic" style="padding-left:25px; margin-right:-25px">Topic</label>
<input type="text" id="node-input-topic">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/html" data-help-name="ui_slider">
<p>Adds a slider widget to the user interface.</p>
<p>The user can change its value between the limits (<b>min</b> and <b>max</b>). Each value change
will generate a message with the value set as <b>payload</b>.</p>
<p>A vertical slider can be created by setting the size so that the height is greater than the width.</p>
<p>The slider can be reversed by setting the min value larger than the max value. e.g. min 100, max 0.</p>
<p>If a <b>Topic</b> is specified, it will be added as <code>msg.topic</code>.</p>
<p>Input messages will be converted to a number. The <b>min</b> value will be used if conversion fails,
and it will update the user interface. If the value changes, it will also be passed to the output.</p>
<p>The label can also be set by a message property by setting
the field to the name of the property, for example <code>{{msg.topic}}</code>.</p>
<p>Setting <code>msg.enabled</code> to <code>false</code> will disable the slider output.</p>
<p>Note: An input msg to the slider node will not change the status information displayed unless the slider
node output is connected to another node.</p>
</script>

View File

@@ -0,0 +1,56 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function SliderNode(config) {
RED.nodes.createNode(this, config);
this.pt = config.passthru;
this.state = [" "," "];
var node = this;
node.status({});
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
var done = ui.add({
node: node,
tab: tab,
group: group,
forwardInputMessages: config.passthru,
control: {
type: 'slider',
label: config.label,
tooltip: config.tooltip,
order: config.order,
value: config.min,
min: Math.min(config.min, config.max),
max: Math.max(config.max, config.min),
invert: (parseFloat(config.min) > parseFloat(config.max)) ? true : undefined,
step: Math.abs(config.step) || 1,
outs: config.outs || "all",
width: config.width || group.config.width || 6,
height: config.height || 1
},
beforeSend: function (msg) {
msg.topic = config.topic || msg.topic;
if (node.pt) {
node.status({shape:"dot",fill:"grey",text:msg.payload});
}
else {
node.state[1] = msg.payload;
node.status({shape:"dot",fill:"grey",text:node.state[1] + " | " + node.state[1]});
}
},
convert: ui.toFloat.bind(this, config)
});
if (!node.pt) {
node.on("input", function(msg) {
node.state[0] = msg.payload;
node.status({shape:"dot",fill:"grey",text:node.state[0] + " | " + node.state[1]});
});
}
node.on("close", done);
}
RED.nodes.registerType("ui_slider", SliderNode);
};

View File

@@ -0,0 +1,51 @@
<script type="text/javascript">
RED.nodes.registerType('ui_spacer', {
category: 'config',
color: '#45ADB4',
defaults: {
name: {value: "spacer"},
group: {type: 'ui_group', required:true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0}
},
inputs:0,
outputs:0,
hasUsers: false,
icon: "ui_spacer.png",
paletteLabel: 'spacer',
label: function() { return this.name + " " + this.width + "x" + this.height; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
}
});
</script>
<script type="text/html" data-template-name="ui_spacer">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
</script>
<script type="text/html" data-help-name="ui_spacer">
</script>

View File

@@ -0,0 +1,27 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function SpacerNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
var done = ui.add({
node: node,
tab: tab,
group: group,
control: {
type: 'spacer',
order: config.order,
width: config.width || group.config.width || 6,
height: config.height || 1
}
});
node.on("close", done);
}
RED.nodes.registerType("ui_spacer", SpacerNode);
};

View File

@@ -0,0 +1,185 @@
<script type="text/javascript">
RED.nodes.registerType('ui_switch',{
category: 'dashboard',
color: 'rgb(176, 223, 227)',
defaults: {
name: {value: ''},
label: {value: 'switch'},
tooltip: {value: ''},
group: {type: 'ui_group', required: true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0},
passthru: {value: true},
decouple: {value: "false"},
topic: {value: ''},
style: {value: ''},
onvalue: {value: true, required:true, validate: (RED.validators.hasOwnProperty('typedInput')?RED.validators.typedInput('onvalueType'):function(v) { return true})},
onvalueType: {value: 'bool'},
onicon: {value: '' },
oncolor: {value: ''},
offvalue: {value: false, required:true, validate: (RED.validators.hasOwnProperty('typedInput')?RED.validators.typedInput('offvalueType'):function(v) { return true})},
offvalueType: {value: 'bool'},
officon: {value: ''},
offcolor: {value: ''},
},
inputs:1,
outputs:1,
icon: "ui_switch.png",
paletteLabel: 'switch',
label: function() { return this.name || (~this.label.indexOf("{{") ? null : this.label) || 'switch'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
$('#node-input-custom-icons').on("change", function() {
if ($('#node-input-custom-icons').val() === "default") {
$(".form-row-custom-icons").hide();
}
else {
$(".form-row-custom-icons").show();
}
});
if (this.onicon !== "" || this.oncolor !== "" || this.officon !=="" || this.offcolor !== "") {
$('#node-input-custom-icons').val('custom');
}
else {
$(".form-row-custom-icons").hide();
$('#node-input-custom-icons').change();
}
$('#node-input-onvalue').typedInput({
default: 'str',
typeField: $("#node-input-onvalueType"),
types: ['str','num','bool','json','bin','date','flow','global']
});
$('#node-input-offvalue').typedInput({
default: 'str',
typeField: $("#node-input-offvalueType"),
types: ['str','num','bool','json','bin','date','flow','global']
});
$('#node-input-passthru').on("change", function() {
if (this.checked) {
$('.form-row-decouple').hide();
$('#node-input-decouple').val("false");
}
else {
$('.form-row-decouple').show();
}
});
},
oneditsave: function() {
if ($('#node-input-custom-icons').val() === 'default') {
$('#node-input-onicon').val('');
$('#node-input-officon').val('');
$('#node-input-oncolor').val('');
$('#node-input-offcolor').val('');
}
}
});
</script>
<script type="text/html" data-template-name="ui_switch">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-label">
</div>
<div class="form-row">
<label for="node-input-tooltip"><i class="fa fa-info-circle"></i> Tooltip</label>
<input type="text" id="node-input-tooltip" placeholder="optional tooltip">
</div>
<div class="form-row">
<label for="node-input-custom-icons"><i class="fa fa-picture-o"></i> Icon</label>
<select id="node-input-custom-icons">
<option value="default">Default</option>
<option value="custom">Custom</option>
</select>
</div>
<div class="form-row form-row-custom-icons">
<label for="node-input-onicon" style="text-align:right;"><i class="fa fa-toggle-on"></i> On Icon</label>
<input type="text" id="node-input-onicon" style="width:120px">
<label for="node-input-oncolor" style="width:50px; text-align:right;">Colour</label>
<input type="text" id="node-input-oncolor" style="width:120px">
</div>
<div class="form-row form-row-custom-icons">
<label for="node-input-officon" style="text-align:right;"><i class="fa fa-toggle-off"></i> Off Icon</label>
<input type="text" id="node-input-officon" style="width:120px">
<label for="node-input-offcolor" style="width:50px; text-align:right;">Colour</label>
<input type="text" id="node-input-offcolor" style="width:120px">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-passthru"><i class="fa fa-arrow-right"></i> Pass though <code>msg</code> if payload matches new state: </label>
<input type="checkbox" checked id="node-input-passthru" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row form-row-decouple">
<label for="node-input-decouple"><i class="fa fa-toggle-on"></i> Indicator</label>
<select id="node-input-decouple" style="display: inline-block; vertical-align: middle; width:70%;">
<option value="false">Switch icon shows state of the output</option>
<option value="true">Switch icon shows state of the input</option>
</select>
</div>
<div class="form-row">
<label style="width:auto" for="node-input-onvalue"><i class="fa fa-envelope-o"></i> When clicked, send:</label>
</div>
<div class="form-row">
<label for="node-input-onvalue" style="padding-left:25px; margin-right:-25px">On Payload</label>
<input type="text" id="node-input-onvalue" style="width:70%">
<input type="hidden" id="node-input-onvalueType">
</div>
<div class="form-row">
<label for="node-input-offvalue" style="padding-left:25px; margin-right:-25px">Off Payload</label>
<input type="text" id="node-input-offvalue" style="width:70%">
<input type="hidden" id="node-input-offvalueType">
</div>
<div class="form-row">
<label for="node-input-topic" style="padding-left:25px; margin-right:-25px">Topic</label>
<input type="text" id="node-input-topic">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/html" data-help-name="ui_switch">
<p>Adds a switch to the user interface.</p>
<p>Each change in the state of the switch will generate
a <code>msg.payload</code> with the specified <b>On</b> and <b>Off</b> values.</p>
<p>The <b>On/Off Color</b> and <b>On/Off Icon</b> are optional fields. If they are all present, the default
toggle switch will be replaced with the relevant icons and their respective colors.</p>
<p>The <b>On/Off Icon</b> field can be either a <a href="https://klarsys.github.io/angular-material-icons/" target="_blank">Material Design icon</a>
<i>(e.g. 'check', 'close')</i> or a <a href="https://fontawesome.com/v4.7.0/icons/" target="_blank">Font Awesome icon</a>
<i>(e.g. 'fa-fire')</i>, or a <a href="https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md">Weather icon</a>.
You can use the full set of google material icons if you add 'mi-' to the icon name. e.g. 'mi-videogame_asset'.</p>
<p>In pass through mode the switch state can be updated by an incoming <code>msg.payload</code> with the specified values,
that must also match the specified type (number, string, etc). When not in passthrough mode then the icon can either
track the state of the output - or the input msg.payload, in order to provide a closed loop feedback.</p>
<p>The label can also be set by a message property by setting
the field to the name of the property, for example <code>{{msg.topic}}</code>.</p>
<p>If a <b>Topic</b> is specified, it will be added to the output as <code>msg.topic</code>.</p>
<p>Setting <code>msg.enabled</code> to <code>false</code> will disable the switch widget.</p>
</script>

View File

@@ -0,0 +1,138 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function validateSwitchValue(node,property,type,payload) {
if (payloadType === 'flow' || payloadType === 'global') {
try {
var parts = RED.util.normalisePropertyExpression(payload);
if (parts.length === '') {
throw new Error();
}
} catch(err) {
node.warn("Invalid payload property expression - defaulting to node id")
payload = node.id;
payloadType = 'str';
}
}
else {
payload = payload || node.id;
}
}
function SwitchNode(config) {
RED.nodes.createNode(this, config);
this.pt = config.passthru;
this.state = ["off"," "];
this.decouple = (config.decouple === "true") ? false : true;
var node = this;
node.status({});
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
var parts;
var onvalue = config.onvalue;
var onvalueType = config.onvalueType;
if (onvalueType === 'flow' || onvalueType === 'global') {
try {
parts = RED.util.normalisePropertyExpression(onvalue);
if (parts.length === 0) {
throw new Error();
}
} catch(err) {
node.warn("Invalid onvalue property expression - defaulting to true")
onvalue = true;
onvalueType = 'bool';
}
}
var offvalue = config.offvalue;
var offvalueType = config.offvalueType;
if (offvalueType === 'flow' || offvalueType === 'global') {
try {
parts = RED.util.normalisePropertyExpression(offvalue);
if (parts.length === 0) {
throw new Error();
}
} catch(err) {
node.warn("Invalid offvalue property expression - defaulting to false")
offvalue = false;
offvalueType = 'bool';
}
}
var done = ui.add({
node: node,
tab: tab,
group: group,
emitOnlyNewValues: false,
forwardInputMessages: config.passthru,
storeFrontEndInputAsState: (config.decouple === "true") ? false : true, //config.passthru,
state: false,
control: {
type: 'switch' + (config.style ? '-' + config.style : ''),
label: config.label,
tooltip: config.tooltip,
order: config.order,
value: false,
onicon: config.onicon,
officon: config.officon,
oncolor: config.oncolor,
offcolor: config.offcolor,
width: config.width || group.config.width || 6,
height: config.height || 1
},
convert: function (payload, oldval, msg) {
var myOnValue,myOffValue;
if (onvalueType === "date") { myOnValue = Date.now(); }
else { myOnValue = RED.util.evaluateNodeProperty(onvalue,onvalueType,node); }
if (offvalueType === "date") { myOffValue = Date.now(); }
else { myOffValue = RED.util.evaluateNodeProperty(offvalue,offvalueType,node); }
if (!this.forwardInputMessages && this.storeFrontEndInputAsState) {
if (myOnValue === oldval) { return true; }
if (oldval === true) { return true; }
else { return false; }
}
if (RED.util.compareObjects(myOnValue,msg.payload)) { node.state[0] = "on"; return true; }
else if (RED.util.compareObjects(myOffValue,msg.payload)) { node.state[0] = "off"; return false; }
else { return oldval; }
},
convertBack: function (value) {
node.state[1] = value?"on":"off";
if (node.pt) {
node.status({fill:(value?"green":"red"),shape:(value?"dot":"ring"),text:value?"on":"off"});
}
else {
var col = (node.decouple) ? ((node.state[1]=="on")?"green":"red") : ((node.state[0]=="on")?"green":"red");
var shp = (node.decouple) ? ((node.state[1]=="on")?"dot":"ring") : ((node.state[0]=="on")?"dot":"ring");
var txt = (node.decouple) ? (node.state[0] +" | "+node.state[1].toUpperCase()) : (node.state[0].toUpperCase() +" | "+node.state[1])
node.status({fill:col, shape:shp, text:txt});
}
var payload = value ? onvalue : offvalue;
var payloadType = value ? onvalueType : offvalueType;
if (payloadType === "date") { value = Date.now(); }
else { value = RED.util.evaluateNodeProperty(payload,payloadType,node); }
return value;
},
beforeSend: function (msg) {
msg.topic = config.topic || msg.topic;
}
});
if (!node.pt) {
node.on("input", function() {
var col = (node.state[0]=="on") ? "green" : "red";
var shp = (node.state[0]=="on") ? "dot" : "ring";
var txt = (node.decouple) ? (node.state[0] +" | "+node.state[1].toUpperCase()) : (node.state[0].toUpperCase() +" | "+node.state[1])
node.status({fill:col, shape:shp, text:txt});
});
}
node.on("close", done);
}
RED.nodes.registerType("ui_switch", SwitchNode);
};

View File

@@ -0,0 +1,105 @@
<script type="text/javascript">
// convert to i18 text
function c_ui_tab(x) {
return RED._("node-red-dashboard/ui_tab:ui_tab."+x);
}
RED.nodes.registerType('ui_tab',{
category: 'config',
defaults: {
name: {value: c_ui_tab("label.home")},
icon: {value: 'dashboard'},
order: {value: 0},
disabled: {value: false},
hidden: {value: false}
},
paletteLabel: 'dashboard tab',
label: function() { return this.name || c_ui_tab("label.tab"); },
sort: function(A,B) {
return A.order - B.order;
},
oneditprepare: function() {
$("#node-config-input-disabled-btn").on("click", function(e) {
var i = $(this).find("i");
var active = i.hasClass("fa-toggle-on");
var newCls = "fa fa-toggle-" + (active ? "off" : "on");
i.attr("class", newCls);
$("#node-config-input-disabled").prop('checked',active);
var newTxt = c_ui_tab(active ? "label.disabled" : "label.enabled");
$("#node-config-input-disabled-label").text(newTxt);
var info = $("#node-config-input-disabled-info");
var done = active ? info.show() : info.hide();
});
if (this.disabled) {
$("#node-config-input-disabled-btn").click();
}
else {
$("#node-config-input-disabled-label").text(c_ui_tab("label.enabled"));
}
$("#node-config-input-hidden-btn").on("click", function(e) {
var i = $(this).find("i");
var active = i.hasClass("fa-toggle-on");
var newCls = "fa fa-toggle-" + (active ? "off" : "on");
i.attr("class", newCls);
$("#node-config-input-hidden").prop('checked',active);
var newTxt = c_ui_tab(active ? "label.hidden" : "label.visible");
$("#node-config-input-hidden-label").text(newTxt);
var info = $("#node-config-input-hidden-info");
var done = active ? info.show() : info.hide();
});
if (this.hidden) {
$("#node-config-input-hidden-btn").click();
}
else {
$("#node-config-input-hidden-label").text(c_ui_tab("label.visible"));
}
},
oneditsave: function() {
this.disabled = $("#node-config-input-disabled").prop("checked");
this.hidden = $("#node-config-input-hidden").prop("checked");
}
});
</script>
<script type="text/html" data-template-name="ui_tab">
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="ui_tab.label.name"></span></label>
<input type="text" id="node-config-input-name">
</div>
<div class="form-row">
<label for="node-config-input-icon"><i class="fa fa-file-image-o"></i> <span data-i18n="ui_tab.label.icon"></span></label>
<input type="text" id="node-config-input-icon">
</div>
<div class="form-row">
<label for="node-config-input-disabled-btn"><i class="fa fa-ban"></i> <span data-i18n="ui_tab.label.state"></span></label>
<button id="node-config-input-disabled-btn" class="editor-button" style="width:100px; margin-right:6px;"><i class="fa fa-toggle-on"></i> <span id="node-config-input-disabled-label"></span></button>
<input type="checkbox" id="node-config-input-disabled" style="display:none;"/>
<span id="node-config-input-disabled-info" data-i18n="[html]ui_tab.info.disabled" style="display:none;"></span>
</div>
<div class="form-row">
<label for="node-config-input-hidden-btn"><i class="fa fa-eye-slash"></i> <span data-i18n="ui_tab.label.navmenu"></span></label>
<button id="node-config-input-hidden-btn" class="editor-button" style="width:100px; margin-right:6px;"><i class="fa fa-toggle-on"></i> <span id="node-config-input-hidden-label"></span></button>
<input type="checkbox" id="node-config-input-hidden" style="display:none;"/>
<span id="node-config-input-hidden-info" data-i18n="[html]ui_tab.info.hidden" style="display:none;"></span>
</div>
<div class="form-tips" data-i18n="[html]ui_tab.tip"></div>
</script>
<script type="text/html" data-help-name="ui_tab">
<p>Tab configuration for Dashboard</p>
<p><b>Disabled</b> pages are not included in the Dashboard app, and are therefore not functional.
The tab name still appears in the Navigation Menu (unless it is also hidden).
</p>
<p><b>Hidden</b> pages are not listed in the Left-hand Navigation Menu.
However, they are still active in the Dashboard, and can be shown by using a `ui_control` msg.
</p>
<p>The <b>Icon</b> field can be either a <a href="https://klarsys.github.io/angular-material-icons/" target="_blank">Material Design icon</a>
<i>(e.g. 'check', 'close')</i> or a <a href="https://fontawesome.com/v4.7.0/icons/" target="_blank">Font Awesome icon</a>
<i>(e.g. 'fa-fire')</i>, or a <a href="https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md">Weather icon</a>.
You can use the full set of google material icons if you add 'mi-' to the icon name. e.g. 'mi-videogame_asset'.</p>
</script>

View File

@@ -0,0 +1,15 @@
module.exports = function(RED) {
function TabNode(config) {
RED.nodes.createNode(this, config);
this.config = {
name: config.name,
order: config.order || 0,
icon: config.icon || '',
disabled: config.disabled || false,
hidden: config.hidden || false
};
}
RED.nodes.registerType("ui_tab", TabNode);
};

View File

@@ -0,0 +1,246 @@
<script type="text/javascript">
// convert to i18 text
function c_(x) {
return RED._("node-red-dashboard/ui_template:ui_template."+x);
}
RED.nodes.registerType('ui_template',{
category: c_("label.category"),
color: 'rgb( 63, 173, 181)',
defaults: {
group: {type: 'ui_group', required:false},
name: {value: ''},
order: {value: 0},
width: {value: 0, validate: function(v) {
var valid = true
if (this.templateScope !== 'global') {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
}
return valid;
}},
height: {value: 0},
format: {value: '<div ng-bind-html="msg.payload"></div>'},
storeOutMessages: {value: true},
fwdInMessages: {value: true},
templateScope: {value: 'local'}
},
inputs:1,
outputs:1,
icon: "ui_template.png",
paletteLabel: 'template',
label: function() { return this.name || 'template'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
if (RED.editor.hasOwnProperty("editText") && typeof RED.editor.editText === "function") {
$("#node-template-expand-editor").show();
}
else {
$("#node-template-expand-editor").hide();
}
var that = this;
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
if (typeof this.storeOutMessages === 'undefined') {
this.storeOutMessages = true;
$('#node-input-storeOutMessages').prop('checked', true);
}
if (typeof this.fwdInMessages === 'undefined') {
this.fwdInMessages = true;
$('#node-input-fwdInMessages').prop('checked', true);
}
if (typeof this.templateScope === 'undefined') {
this.templateScope = 'local';
$('#node-input-templateScope').val(this.templateScope);
}
$('#node-input-templateScope').on('change', function() {
if ($('#node-input-templateScope').val() === 'global') {
$('#template-row-group, #template-row-size, #template-pass-store').hide();
that._def.defaults.group.required = false;
}
else {
$('#template-row-group, #template-row-size, #template-pass-store').show();
that._def.defaults.group.required = true;
}
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0; i<rows.size(); i++) {
height = height - $(rows[i]).outerHeight(true);
}
if ($('#node-input-templateScope').val() === "global") { height += 240; }
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
if (this.editor) { this.editor.resize(); }
})
this.editor = RED.editor.createEditor({
id: 'node-input-format-editor',
mode: 'ace/mode/html',
value: $("#node-input-format").val()
});
RED.library.create({
url:"uitemplates", // where to get the data from
type:"ui_template", // the type of object the library is for
editor:this.editor, // the field name the main text body goes to
mode:"ace/mode/html",
fields:['name']
});
this.editor.focus();
RED.popover.tooltip($("#node-template-expand-editor"),c_("label.expand"));
$("#node-template-expand-editor").on("click", function(e) {
e.preventDefault();
var value = that.editor.getValue();
RED.editor.editText({
mode: 'html',
value: value,
width: "Infinity",
cursor: that.editor.getCursorPosition(),
complete: function(v,cursor) {
that.editor.setValue(v, -1);
that.editor.gotoLine(cursor.row+1,cursor.column,false);
setTimeout(function() { that.editor.focus(); },300);
}
})
})
},
oneditsave: function() {
var annot = this.editor.getSession().getAnnotations();
this.noerr = 0;
$("#node-input-noerr").val(0);
for (var k=0; k < annot.length; k++) {
if (annot[k].type === "error") {
$("#node-input-noerr").val(annot.length);
this.noerr = annot.length;
}
}
$("#node-input-format").val(this.editor.getValue());
this.editor.destroy();
delete this.editor;
},
oneditcancel: function() {
this.editor.destroy();
delete this.editor;
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0; i<rows.size(); i++) {
height = height - $(rows[i]).outerHeight(true);
}
if ($('#node-input-templateScope').val() === "global") { height += 232; }
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
this.editor.resize();
}
});
</script>
<script type="text/html" data-template-name="ui_template">
<div class="form-row">
<label for="node-input-format"><span data-i18n="ui_template.label.type"></span></label>
<select style="width:76%" id="node-input-templateScope">
<option value="local" data-i18n="ui_template.label.local"></option>
<option value="global" data-i18n="ui_template.label.global"></option>
</select>
</div>
<div class="form-row" id="template-row-group">
<label for="node-input-group"><i class="fa fa-table"></i> <span data-i18n="ui_template.label.group"></span></label>
<input type="text" id="node-input-group">
</div>
<div class="form-row" id="template-row-size">
<label><i class="fa fa-object-group"></i> <span data-i18n="ui_template.label.size"></span></label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="ui_template.label.name"></span></label>
<div style="display:inline-block; width:calc(100% - 105px)">
<input type="text" id="node-input-name">
</div>
</div>
<div class="form-row" style="margin-bottom:0px;">
<label for="node-input-format"><i class="fa fa-copy"></i> <span data-i18n="ui_template.label.template"></span></label>
<input type="hidden" id="node-input-format">
<button id="node-template-expand-editor" class="red-ui-button red-ui-button-small" style="float:right"><i class="fa fa-expand"></i></button>
</div>
<div class="form-row node-text-editor-row">
<div style="height:250px; min-height:100px" class="node-text-editor" id="node-input-format-editor" ></div>
</div>
<div id="template-pass-store">
<div class="form-row" style="margin-bottom:0px;">
<input type="checkbox" id="node-input-fwdInMessages" style="display:inline-block; margin-left:8px; width:auto; vertical-align:top;">
<label for="node-input-fwdInMessages" style="width:70%;"> <span data-i18n="ui_template.label.pass-through"></span></label>
</div>
<div class="form-row" style="margin-bottom:0px;">
<input type="checkbox" id="node-input-storeOutMessages" style="display:inline-block; margin-left:8px; width:auto; vertical-align:top;">
<label for="node-input-storeOutMessages" style="width:70%;"> <span data-i18n="ui_template.label.store-state"></span></label>
</div>
</div>
</script>
<script type="text/html" data-help-name="ui_template">
<p>The template widget can contain any valid html and Angular/Angular-Material directives.</p>
<p>This node can be used to create a dynamic user interface element that changes its appearence
based on the input message and can send back messages to Node-RED.</p>
<p><b>For example:</b><br>
<pre style="font-size:smaller;">&lt;div layout=&quot;row&quot; layout-align=&quot;space-between&quot;&gt;
&lt;p&gt;The number is&lt;/p&gt;
&lt;font color=&quot;{{((msg.payload || 0) % 2 === 0) ? 'green' : 'red'}}&quot;&gt;
{{(msg.payload || 0) % 2 === 0 ? 'even' : 'odd'}}
&lt;/font&gt;
&lt;/div&gt;</pre>
Will display if the number received as <code>msg.payload</code> is even or odd. It will also
change the color of the text to green if the number is even or red if odd.<br/>
The next example shows how to set a unique id for your template, pick up the default theme colour,
and watch for any incoming message.</p>
<pre style="font-size:smaller;">
&lt;div id="{{'my_'+$id}}" style="{{'color:'+theme.base_color}}"&gt;Some text&lt;/div&gt;
&lt;script&gt;
(function(scope) {
scope.$watch('msg', function(msg) {
if (msg) {
// Do something when msg arrives
$("#my_"+scope.$id).html(msg.payload);
}
});
})(scope);
&lt;/script&gt;</pre>
<p>Templates made in this way can be copied and remain independent of each other.</p>
<p><b>Sending a message:</b><br>
<pre style="font-size:smaller;">
&lt;script&gt;
var value = "hello world";
// or overwrite value in your callback function ...
this.scope.action = function() { return value; }
&lt;/script&gt;
&lt;md-button ng-click=&quot;send({payload:action()})&quot;&gt;
Click me to send a hello world
&lt;/md-button&gt;</pre>
Will display a button that when clicked will send a message with the payload <code>'Hello world'</code>.</p>
<p><b>Using <code>msg.template</code>:</b><br>
You can also define the template content via <code>msg.template</code>, so you can use external files for example.<br>
Template will be reloaded on input if it has changed.<br>
Code written in the Template field will be ignored when <code>msg.template</code> is present.</p>
<p>The following icon fonts are available: <a href="https://klarsys.github.io/angular-material-icons/" target="_blank">Material Design icon</a>
<i>(e.g. 'check', 'close')</i> or a <a href="https://fontawesome.com/v4.7.0/icons/" target="_blank">Font Awesome icon</a>
<i>(e.g. 'fa-fire')</i>, or a <a href="https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md">Weather icon</a>.
You can use the full set of google material icons if you add 'mi-' to the icon name. e.g. 'mi-videogame_asset'.</p>
</script>

View File

@@ -0,0 +1,88 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function TemplateNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var group = RED.nodes.getNode(config.group);
if (!group && config.templateScope !== 'global') { return; }
var tab = null;
if (config.templateScope !== 'global') {
tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
if (!config.width) {
config.width = group.config.width;
}
}
var hei = Number(config.height|| 0);
var previousTemplate = null
var theme = ui.getTheme();
var colortheme = {};
for (var i in theme) {
if (theme.hasOwnProperty(i)) {
colortheme[i.replace(/-/g, "_")] = theme[i].value;
}
}
var done = ui.add({
forwardInputMessages: config.fwdInMessages,
storeFrontEndInputAsState: config.storeOutMessages,
emitOnlyNewValues: false,
node: node,
tab: tab,
group: group,
control: {
type: 'template',
order: config.order,
width: config.width || 6,
height: hei,
format: config.format,
templateScope: config.templateScope,
theme: colortheme
},
beforeEmit: function(msg) {
var properties = Object.getOwnPropertyNames(msg).filter(function (p) { return p[0] != '_'; });
var clonedMsg = {
templateScope: config.templateScope
};
for (var i=0; i<properties.length; i++) {
var property = properties[i];
clonedMsg[property] = msg[property];
}
// transform to string if msg.template is buffer
if (clonedMsg.template !== undefined && Buffer.isBuffer(clonedMsg.template)) {
clonedMsg.template = clonedMsg.template.toString();
}
if (clonedMsg.template === undefined && previousTemplate !== null) {
clonedMsg.template = previousTemplate;
}
//This updates the whole page if the template input changes and
//height set to auto - performance killer, but here just in case
// if ((config.height == "0") && (value !== node.oldvalue)) {
// node.oldvalue = value;
// setImmediate(function() { ui.updateUi(); });
// }
if (clonedMsg.template) {
previousTemplate = clonedMsg.template
}
return { msg:clonedMsg };
},
beforeSend: function (msg, original) {
if (original) {
var om = original.msg;
om.socketid = original.socketid;
return om;
}
}
});
node.on("close", done);
}
RED.nodes.registerType("ui_template", TemplateNode);
RED.library.register("uitemplates");
};

View File

@@ -0,0 +1,182 @@
<script type="text/javascript">
RED.nodes.registerType('ui_text',{
category: 'dashboard',
color: 'rgb(119, 198, 204)',
defaults: {
group: {type: 'ui_group', required:true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0},
name: {value: ''},
label: {value: 'text'},
format: {value: '{{msg.payload}}'},
layout: {value:'row-spread'}
},
inputs:1,
outputs:0,
align: "right",
icon: "ui_text.png",
paletteLabel: 'text',
label: function() { return this.name || (~this.label.indexOf("{{") ? null : this.label) || 'text'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
$(".nr-db-text-layout-"+(this.layout||'row-spread')).addClass('selected');
[ ".nr-db-text-layout-row-left",".nr-db-text-layout-row-center",".nr-db-text-layout-row-right",
".nr-db-text-layout-row-spread",".nr-db-text-layout-col-center"].forEach(function(id) {
$(id).click(function(e) {
$(".nr-db-text-layout").removeClass('selected');
$(this).addClass('selected');
$('#node-input-layout').val(id.substring(".nr-db-text-layout-".length));
e.preventDefault();
})
})
}
});
</script>
<script type="text/html" data-template-name="ui_text">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-label">
</div>
<div class="form-row">
<label for="node-input-format"><i class="fa fa-i-cursor"></i> Value format</label>
<input type="text" id="node-input-format" placeholder="{{msg.payload}}">
</div>
<div class="form-row">
<label style="vertical-align: top"><i class="fa fa-th-large"></i> Layout</label>
<div style="display:inline-block">
<input type="hidden" id="node-input-layout"><input type="hidden" id="node-input-layoutAlign">
<div>
<a href="#" class="nr-db-text-layout nr-db-text-layout-row nr-db-text-layout-row-left">
<span class="nr-db-text-layout-label">label</span>
<span class="nr-db-text-layout-value">value</span>
<div class="nr-db-text-layout-checkbox"></div>
</a>
<a href="#" class="nr-db-text-layout nr-db-text-layout-row nr-db-text-layout-row-center">
<span class="nr-db-text-layout-label">label</span>
<span class="nr-db-text-layout-value">value</span>
<div class="nr-db-text-layout-checkbox"></div>
</a>
<a href="#" class="nr-db-text-layout nr-db-text-layout-row nr-db-text-layout-row-right">
<span class="nr-db-text-layout-label">label</span>
<span class="nr-db-text-layout-value">value</span>
<div class="nr-db-text-layout-checkbox"></div>
</a>
</div>
<div>
<a href="#" class="nr-db-text-layout nr-db-text-layout-row nr-db-text-layout-row-spread">
<span class="nr-db-text-layout-label">label</span>
<span class="nr-db-text-layout-value">value</span>
<div class="nr-db-text-layout-checkbox"></div>
</a>
<a href="#" class="nr-db-text-layout nr-db-text-layout-col nr-db-text-layout-col-center">
<span class="nr-db-text-layout-label">label</span>
<span class="nr-db-text-layout-value">value</span>
<div class="nr-db-text-layout-checkbox"></div>
</a>
</div>
</div>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<style>
.nr-db-text-layout {
position:relative;
display: inline-block;
width: 90px;
height: 60px;
border-radius:3px;
border:1px solid #bbb;
cursor:pointer;
color: #666;
margin-right: 10px;
margin-bottom: 10px;
}
.nr-db-text-layout.selected, .nr-db-text-layout:hover {
border-color: #333;
color: #333;
}
.nr-db-text-layout span {
position: absolute;
}
.nr-db-text-layout-value {
font-weight: bold;
}
.nr-db-text-layout-row span { top: 20px; }
.nr-db-text-layout-row-left .nr-db-text-layout-label { left: 2px; }
.nr-db-text-layout-row-left .nr-db-text-layout-value { left: 34px; }
.nr-db-text-layout-row-spread .nr-db-text-layout-label { left: 2px; }
.nr-db-text-layout-row-spread .nr-db-text-layout-value { right: 2px; }
.nr-db-text-layout-row-center .nr-db-text-layout-label { left: 11px; }
.nr-db-text-layout-row-center .nr-db-text-layout-value { right: 11px; }
.nr-db-text-layout-row-right .nr-db-text-layout-label { right: 40px; }
.nr-db-text-layout-row-right .nr-db-text-layout-value { right: 2px; }
.nr-db-text-layout-col span { width: 90px; text-align: center; left: 0px;}
.nr-db-text-layout-col-center .nr-db-text-layout-label { top: 12px; }
.nr-db-text-layout-col-center .nr-db-text-layout-value { top: 26px; }
.nr-db-text-layout-checkbox {
display: none;
width: 10px;
height: 10px;
border-radius: 10px;
border: 1px solid #bbb;
position: absolute;
right: -5px;
top: -5px;
background: #fff;
}
.nr-db-text-layout.selected .nr-db-text-layout-checkbox {
display:inline-block;
box-shadow: inset 0px 0px 0px 2px #fff;
background: #333;
border-color: #333;
}
</style>
<script type="text/html" data-help-name="ui_text">
<p>Will display a non-editable text field on the user interface.</p>
<p>Each received <code>msg.payload</code> will update the text based on the provided <b>Value Format</b>.</p>
<p>The <b>Value Format</b> field can be used to change the displayed format and can contain valid HTML and
<a href="https://scotch.io/tutorials/all-about-the-built-in-angularjs-filters" target="_blank">Angular filters</a>.</p>
<p>For example: <code>{{value | uppercase}} &amp;deg;</code> will uppercase the payload text and add the degree symbol.</p>
<p>The label can also be set by a message property by setting
the field to the name of the property, for example <code>{{msg.topic}}</code>.</p>
<p>The following icon fonts are also available: <a href="https://klarsys.github.io/angular-material-icons/" target="_blank">Material Design icon</a>
<i>(e.g. 'check', 'close')</i> or a <a href="https://fontawesome.com/v4.7.0/icons/" target="_blank">Font Awesome icon</a>
<i>(e.g. 'fa-fire')</i>, or a <a href="https://github.com/Paul-Reed/weather-icons-lite/blob/master/css_mappings.md">Weather icon</a>.
You can use the full set of google material icons if you add 'mi-' to the icon name. e.g. 'mi-videogame_asset'.</p>
<p>The widget also has a class of <code>nr-dashboard-widget-{the_widget_label_with_underscores}</code> which can be used for additional
styling if required. You may need to use the <i>!important</i> flag to override the theme.</p>
</script>

View File

@@ -0,0 +1,51 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function TextNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
var layout = config.layout||"row-spread";
var angLayout = "row";
var angLayoutAlign = "space-between center";
if (layout === "row-spread") { angLayout = 'row'; angLayoutAlign = 'space-between center'}
else if (layout === "row-left") { angLayout = 'row'; angLayoutAlign = 'start center'}
else if (layout === "row-center") { angLayout = 'row'; angLayoutAlign = 'center center'}
else if (layout === "row-right") { angLayout = 'row'; angLayoutAlign = 'end center'}
else if (layout === "col-center") { angLayout = 'column'; angLayoutAlign = 'center center'}
var done = ui.add({
emitOnlyNewValues: false,
node: node,
tab: tab,
group: group,
control: {
type: 'text',
label: config.label,
order: config.order,
format: config.format,
width: config.width || group.config.width || 6,
height: config.height || 1,
layout: angLayout,
layoutAlign: angLayoutAlign
},
convert: function(value) {
if (value !== undefined) {
if (Buffer.isBuffer(value)) {
value = value.toString('binary');
}
else {
value = value.toString();
}
}
return value;
}
});
node.on("close", done);
}
RED.nodes.registerType("ui_text", TextNode);
};

View File

@@ -0,0 +1,114 @@
<script type="text/javascript">
RED.nodes.registerType('ui_text_input',{
category: 'dashboard',
color: 'rgb(176, 223, 227)',
defaults: {
name: {value: ''},
label: {value: ''},
tooltip: {value: ''},
group: {type: 'ui_group', required: true},
order: {value: 0},
width: {value: 0, validate: function(v) {
var width = v||0;
var currentGroup = $('#node-input-group').val()||this.group;
var groupNode = RED.nodes.node(currentGroup);
var valid = !groupNode || +width <= +groupNode.width;
$("#node-input-size").toggleClass("input-error",!valid);
return valid;
}
},
height: {value: 0},
passthru: {value: true},
mode: {value: 'text', required: true},
delay: {value: 300, validate: RED.validators.number()},
topic: {value: ''}
},
inputs:1,
outputs:1,
outputLabels: function() { return this.mode; },
icon: "ui_text.png",
paletteLabel: 'text input',
label: function() { return this.name || (~this.label.indexOf("{{") ? null : this.label) || this.mode+' input'; },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
$("#node-input-size").elementSizer({
width: "#node-input-width",
height: "#node-input-height",
group: "#node-input-group"
});
}
});
</script>
<script type="text/html" data-template-name="ui_text_input">
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-label">
</div>
<div class="form-row">
<label for="node-input-tooltip"><i class="fa fa-info-circle"></i> Tooltip</label>
<input type="text" id="node-input-tooltip" placeholder="optional tooltip">
</div>
<div class="form-row">
<label for="node-input-mode"><i class="fa fa-keyboard-o"></i> Mode</label>
<select style="width:128px" id="node-input-mode">
<option value="text">text input</option>
<option value="email">email address</option>
<option value="password">password</option>
<option value="number">number</option>
<option value="tel">telephone input</option>
<option value="color">color picker</option>
<option value="time">time picker</option>
<option value="week">week picker</option>
<option value="month">month picker</option>
</select>
<label for="node-input-delay" style="text-align:right; width:100px"><i class="fa fa-clock-o"></i> Delay (ms)</label>
<input type="text" style="width:58px" id="node-input-delay">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-passthru"><i class="fa fa-arrow-right"></i> If <code>msg</code> arrives on input, pass through to output: </label>
<input type="checkbox" checked id="node-input-passthru" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-payload"><i class="fa fa-envelope-o"></i> When changed, send:</label>
</div>
<div class="form-row">
<label style="padding-left: 25px; margin-right: -25px">Payload</label>
<label style="width:auto">Current value</label>
</div>
<div class="form-row">
<label for="node-input-topic" style="padding-left: 25px; margin-right: -25px">Topic</label>
<input type="text" id="node-input-topic">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
<div class="form-tips">Setting <b>Delay</b> to 0 waits for Enter or Tab key, to send input.</span></div>
</script>
<script type="text/html" data-help-name="ui_text_input">
<p>Adds a text input field to the user interface. Mode can be regular text, email or color picker.</p>
<p>Any input is sent as <code>msg.payload</code>. If set to pass through mode an arriving <code>msg.payload</code>
will be used if it is different from the existing text in the input field. This allows you to preset
the text of the input field.</p>
<p>The <b>Delay</b> <i>(default : 300ms)</i> sets the amount of time in milliseconds before the output is sent.
Setting to <code>0</code> waits for "Enter" or "Tab" key to send. Enter will send payload but remain in focus.
Tab will send payload and move to next field. Clicking elsewhere will also send the payload.</p>
<p>The email mode will color in red if it is not a valid address and will return undefined.</p>
<p>The time input type returns a number of milliseconds from midnight.</p>
<p>Not all browsers support the <i>week</i> and <i>month</i> input types, and may return <i>undefined</i>.
Please test your target browser(s) before use.</p>
<p>If a <b>Topic</b> is specified, it will be added as <code>msg.topic</code>.</p>
<p>Setting <code>msg.enabled</code> to <code>false</code> will disable the input.</p>
</script>

View File

@@ -0,0 +1,43 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function TextInputNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var group = RED.nodes.getNode(config.group);
if (!group) { return; }
var tab = RED.nodes.getNode(group.config.tab);
if (!tab) { return; }
var done = ui.add({
node: node,
tab: tab,
group: group,
forwardInputMessages: config.passthru,
control: {
type: (config.delay <= 0 ? 'text-input-CR' : 'text-input'),
label: config.label,
tooltip: config.tooltip,
mode: config.mode,
delay: config.delay,
order: config.order,
value: '',
width: config.width || group.config.width || 6,
height: config.height || 1
},
beforeSend: function (msg) {
if (config.mode === "time") {
if (typeof msg.payload === "string") {
msg.payload = Date.parse(msg.payload);
}
}
// if (config.mode === "week") { msg.payload = Date.parse(msg.payload); }
// if (config.mode === "month") { msg.payload = Date.parse(msg.payload); }
msg.topic = config.topic || msg.topic;
}
});
node.on("close", done);
}
RED.nodes.registerType("ui_text_input", TextInputNode);
};

View File

@@ -0,0 +1,108 @@
<script type="text/javascript">
RED.nodes.registerType('ui_toast', {
category: 'dashboard',
color: 'rgb(119, 198, 204)',
defaults: {
position: {value: 'top right'},
displayTime: {value: '3'},
highlight: {value: ''},
sendall: {value: true},
outputs: {value: 0},
ok: {value: 'OK', required: true},
cancel: {value: ''},
raw: {value: false},
topic: {value: ''},
name: {value: ''}
},
inputs:1,
outputs:0,
align: "right",
icon: "ui_toast.png",
paletteLabel: 'notification',
label: function() { return this.name || (this.position==="dialog" ? "show dialog" : "show notification"); },
labelStyle: function() { return this.name?"node_label_italic":""; },
oneditprepare: function() {
$("#node-input-position").on("change", function() {
if ($("#node-input-position option:selected").val() === 'dialog') {
$("#node-toast-displaytime").hide();
$("#node-toast-highlightcolor").hide();
$("#node-toast-sendall").hide();
$("#node-dialog-displayok").show();
$("#node-dialog-displaycancel").show();
$("#node-dialog-topic").show();
}
else {
$("#node-toast-displaytime").show();
$("#node-toast-highlightcolor").show();
$("#node-toast-sendall").show();
$("#node-dialog-displayok").hide();
$("#node-dialog-displaycancel").hide();
$("#node-dialog-topic").show();
}
});
},
oneditsave: function() {
if ($("#node-input-position option:selected").val() === 'dialog') { this.outputs = 1; }
else { this.outputs = 0; }
}
});
</script>
<script type="text/html" data-template-name="ui_toast">
<div class="form-row">
<label for="node-input-position"><i class="fa fa-th-large"></i> Layout</label>
<select type="text" id="node-input-position" style="display:inline-block; width:70%; vertical-align:baseline;">
<option value="top right">Top Right</option>
<option value="bottom right">Bottom Right</option>
<option value="top left">Top Left</option>
<option value="bottom left">Bottom Left</option>
<option value="dialog">OK / Cancel Dialog</option>
</select>
</div>
<div class="form-row" id="node-toast-displaytime">
<label for="node-input-displayTime"><i class="fa fa-clock-o"></i> Timeout (S)</label>
<input type="text" id="node-input-displayTime" placeholder="e.g. 3 seconds">
</div>
<div class="form-row" id="node-toast-highlightcolor">
<label for="node-input-highlight"><i class="fa fa-square-o"></i> Border</label>
<input type="text" id="node-input-highlight" placeholder="(optional) border highlight colour">
</div>
<div class="form-row" id="node-toast-sendtoall">
<label style="width:auto" for="node-input-sendall"><i class="fa fa-arrow-right"></i> Send to all browser sessions. </label>
<input type="checkbox" checked id="node-input-sendall" style="display:inline-block; width:auto; vertical-align:baseline;">
</div>
<div class="form-row" id="node-dialog-displayok">
<label for="node-input-ok"><i class="fa fa-check"></i> Default action label</label>
<input type="text" id="node-input-ok" placeholder="label for OK button">
</div>
<div class="form-row" id="node-dialog-displaycancel">
<label for="node-input-cancel"><i class="fa fa-times"></i> Secondary action label</label>
<input type="text" id="node-input-cancel" placeholder="(optional label for Cancel button)">
</div>
<div class="form-row" id="node-toast-raw">
<label style="width:auto" for="node-input-raw"><i class="fa fa-exclamation-triangle"></i> Accept raw HTML/JavaScript input. </label>
<input type="checkbox" id="node-input-raw" style="display:inline-block; width:auto; vertical-align:baseline;">
</div>
<div class="form-row" id="node-dialog-topic">
<label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
<input type="text" id="node-input-topic" placeholder="(optional msg.topic)">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-tips"><b>Note</b>: checking <i>Accept raw HTML/JavaScript</i> can allow injection of code.
Ensure the input comes from trusted sources.</span></div>
</script>
<script type="text/html" data-help-name="ui_toast">
<p>Shows <code>msg.payload</code> as a popup notification or OK / Cancel dialog
message on the user interface.</p>
<p>If a <code>msg.topic</code> is available it will be used as the title.</p>
<p>If you do not set an optional border highlight colour, then it can be set dynamically by <code>msg.highlight</code>.</p>
<p>You may also configure the position and duration of the notification.</p>
<p>The dialog returns a <code>msg.payload</code> string of whatever you configure
the button label(s) to be. The second (cancel) button is optional, as is the return
value of <code>msg.topic</code>.</p>
<p>Sending a blank payload will remove any active dialog without sending any data.</p>
</script>

View File

@@ -0,0 +1,65 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function ToastNode(config) {
RED.nodes.createNode(this, config);
if (config.hasOwnProperty("displayTime") && (config.displayTime.length > 0)) {
try { this.displayTime = parseFloat(config.displayTime) * 1000; }
catch(e) { this.displayTime = 3000; }
}
else { this.displayTime = 3000; }
if (this.displayTime <= 0) { this.displayTime = 1; }
this.position = config.position || "top right";
this.highlight = config.highlight;
this.ok = config.ok;
this.cancel = config.cancel;
this.topic = config.topic;
if (config.sendall === undefined) { this.sendall = true; }
else { this.sendall = config.sendall; }
this.raw = config.raw || false;
var node = this;
// var noscript = function (content) {
// if (typeof content === "object") { return null; }
// content = '' + content;
// content = content.replace(/<.*cript.*/ig, '');
// content = content.replace(/.on\w+=.*".*"/g, '');
// content = content.replace(/.on\w+=.*\'.*\'/g, '');
// return content;
// }
var done = ui.add({
node: node,
control: {},
storeFrontEndInputAsState: false,
forwardInputMessages: false,
beforeSend: function (msg) {
var m = msg.payload.msg;
m.topic = node.topic || m.topic;
return m;
}
});
node.on('input', function(msg) {
if (node.sendall === true) { delete msg.socketid; }
//msg.payload = noscript(msg.payload);
ui.emitSocket('show-toast', {
title: node.topic || msg.topic,
message: msg.payload,
highlight: node.highlight || msg.highlight,
displayTime: node.displayTime,
position: node.position,
id: node.id,
dialog: (node.position === "dialog") || false,
ok: node.ok,
cancel: node.cancel,
socketid: msg.socketid,
raw: node.raw,
msg: msg
});
});
node.on("close", done);
}
RED.nodes.registerType("ui_toast", ToastNode);
};

View File

@@ -0,0 +1,64 @@
<script type="text/javascript">
// convert to i18 text
function c_(x) {
return RED._("node-red-dashboard/ui_ui_control:ui_ui_control."+x);
}
RED.nodes.registerType('ui_ui_control', {
category: c_("label.category"),
color: 'rgb( 63, 173, 181)',
defaults: {
name: {value:""},
events: {value:"all"}
},
inputs:1,
outputs:1,
align: "right",
icon: "ui_link.png",
paletteLabel: 'ui control',
label: function() { return this.name || "ui control"; },
labelStyle: function() { return this.name?"node_label_italic":""; },
outputLabels: function() { return this.events; },
});
</script>
<script type="text/html" data-template-name="ui_ui_control">
<div class="form-row">
<label for="node-input-events"><i class="fa fa-sign-out"></i> Output</label>
<select id="node-input-events" style="width:70%;">
<option value="all">Connect, lost, change events</option>
<option value="connect">Connect event only</option>
<option value="change">Change tab event only</option>
</select>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="ui_ui_control.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]ui_ui_control.placeholder.name">
</div>
</script>
<script type="text/html" data-help-name="ui_ui_control">
<p>Allows dynamic control of the Dashboard.</p>
<p>The default function is to change the currently displayed tab. <code>msg.payload</code>
should either be an object of the form <code>{"tab":"my_tab_name"}</code>, or just be the <b>tab name</b>
or <b>numeric index</b> (from 0) of the tab or link to be displayed.</p>
<p>Sending a blank tab name "" will refresh the current page.
You can also send "+1" for next tab and "-1" for previous tab.</p>
<p>Dashboard pages (i.e. "tabs") can be controlled by sending a <code>msg.payload</code> object with the format
<pre>{"tabs": {"hide": "tab_name_to_hide", "disable": ["secret_tab", "unused_stuff"]}}</pre>.
There are 2 toggle states available: <b>show</b>/<b>hide</b> and <b>enable</b>/<b>disable</b></p>
<p>Visibility of individual groups of widgets can controlled by a payload like
<pre>{"group": {"hide": ["tab_name_group_name_with_underscores"], "show": ["reveal_another_group"], "focus": true}}</pre>
where <b>focus</b> is optional and will cause the screen to scroll to show that group if required. The group
names are the IDs of the groups and are typically formed from the <i>tab name</i> plus <i>group name</i> joined with
underscores replacing all spaces.</p>
<p>When any browser client connects or loses connection, or changes tab, this node will emit a <code>msg</code> containing:</p>
<ul>
<li><code>payload</code> - <i>connect</i>, <i>lost</i>, or <i>change</i>.
<li><code>socketid</code> - the ID of the socket (this will change every time the browser reloads the page).
<li><code>socketip</code> - the ip address from where the connection originated.
<li><code>tab</code> - the number of the tab. (Only for 'change' event).
<li><code>name</code> - the name of the tab. (Only for 'change' event).
</ul>
<p>Optional - report only connect events - useful to use to trigger a resend of data to a new client without needing to filter out other events.</p>
</script>

View File

@@ -0,0 +1,64 @@
module.exports = function(RED) {
var ui = require('../ui')(RED);
function UiControlNode(config) {
RED.nodes.createNode(this, config);
this.events = config.events || "all";
var node = this;
this.on('input', function(msg) {
if (typeof msg.payload !== "object") { msg.payload = {tab:msg.payload}; }
// show/hide or enable/disable tabs
if (msg.payload.hasOwnProperty("tabs")) {
ui.emit('ui-control', {tabs:msg.payload.tabs, socketid:msg.socketid});
}
// switch to tab name (or number)
if (msg.payload.hasOwnProperty("tab")) {
ui.emit('ui-control', {tab:msg.payload.tab, socketid:msg.socketid});
}
// show or hide ui groups
if (msg.payload.hasOwnProperty("group")) {
ui.emit('ui-control', {group:msg.payload.group, socketid:msg.socketid});
}
});
var sendconnect = function(id, ip) {
node.send({payload:"connect", socketid:id, socketip:ip});
};
var sendlost = function(id, ip) {
node.send({payload:"lost", socketid:id, socketip:ip});
};
var sendchange = function(index, name, id, ip, p) {
node.send({payload:"change", tab:index, name:name, socketid:id, socketip:ip, params:p});
}
if (node.events === "connect") {
ui.ev.on('newsocket', sendconnect);
}
else if (node.events === "change") {
ui.ev.on('changetab', sendchange);
}
else {
ui.ev.on('newsocket', sendconnect);
ui.ev.on('changetab', sendchange);
ui.ev.on('endsocket', sendlost);
}
this.on('close', function() {
if (node.events === "connect") {
ui.ev.removeListener('newsocket', sendconnect);
}
else if (node.events === "change") {
ui.ev.removeListener('changetab', sendchange);
}
else {
ui.ev.removeListener('newsocket', sendconnect);
ui.ev.removeListener('changetab', sendchange);
ui.ev.removeListener('endsocket', sendlost);
}
})
}
RED.nodes.registerType("ui_ui_control", UiControlNode);
};