node-red-contrib-notification-better 0.0.0-stage → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -2
- package/locales/en-US/noti-better.html +0 -0
- package/locales/en-US/noti-better.json +63 -0
- package/locales/ja/noti-better.json +63 -0
- package/locales/zh-CN/noti-better.json +63 -0
- package/locales/zh-TW/noti-better.html +0 -0
- package/locales/zh-TW/noti-better.json +63 -0
- package/package.json +36 -4
- package/ui_notification.html +353 -0
- package/ui_notification.js +297 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,86 @@
|
|
|
1
|
-
#
|
|
1
|
+
# node-red-contrib-notification-better
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
一個為 [Node-RED Dashboard](https://flows.nodered.org/node/node-red-dashboard) 設計的進階通知(Notification / Toast)與互動對話框(Dialog / Prompt)節點:**`noti-better`**。
|
|
4
|
+
|
|
5
|
+
徹底解決按鈕強制大寫的問題,支援完美暗黑主題相容介面、完整多語言、通知等級色彩、提示音效與倒數進度條!
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ 核心特色 (Key Features)
|
|
10
|
+
|
|
11
|
+
- 🔤 **忠實保留按鈕原始大小寫 (Preserve Button Case)**:
|
|
12
|
+
- 徹底解除 Angular Material 強制全大寫(`text-transform: uppercase`)的限制。
|
|
13
|
+
- 輸入 `Ok` 顯示 `Ok`、輸入 `Cancel` 顯示 `Cancel`、輸入 `確定` 或 `Close Now` 皆忠實依字樣呈現!
|
|
14
|
+
- 🌓 **完美相容深色 / 淺色主題 (Dark & Light Theme Friendly)**:
|
|
15
|
+
- 表單標題與排版無縫融入 Node-RED 介面,在暗黑模式下無突兀白底,文字清晰舒適。
|
|
16
|
+
- 🛡️ **雙重多語言防呆機制 (Robust i18n & Fallback)**:
|
|
17
|
+
- 完整支援 **繁體中文 (zh-TW)**、**English (en-US)**、**日本語 (ja)**、**簡體中文 (zh-CN)**。
|
|
18
|
+
- 所有表單欄位與畫布標籤皆內嵌完整的預設降級文字,即使語系載入延遲也絕不出現空白或程式代碼字樣。
|
|
19
|
+
- 🎯 **五種通知等級預設 (Notification Levels)**:
|
|
20
|
+
- 內建 **Info (資訊)**、**Success (成功)**、**Warning (警告)**、**Error (錯誤)** 與 **Default (預設)**。
|
|
21
|
+
- 自動套用專屬主題色彩與外框線,亦可透過 `msg.notificationType` 動態切換。
|
|
22
|
+
- 🔔 **圖示支援 (Icon Support)**:
|
|
23
|
+
- 自動根據通知等級配對 Font Awesome 圖示,亦可手動填寫或透過 `msg.icon` 自訂圖示。
|
|
24
|
+
- 🔊 **音效提示 (Audio Chime Alert)**:
|
|
25
|
+
- 內建 Web Audio API 音效合成技術,無須外掛任何音訊檔案,勾選或由 `msg.playSound: true` 觸發即可發出清脆提示聲。
|
|
26
|
+
- ⏳ **倒數進度條 (Countdown Progress Bar)**:
|
|
27
|
+
- 在卡片底部以視覺化動態進度條呈現剩餘時間,何時關閉一目了然。
|
|
28
|
+
- 💬 **互動式對話框與輸入框 (Dialog & Prompt)**:
|
|
29
|
+
- 支援確認 / 取消對話框,以及帶輸入框的 Prompt 模式,支援自訂預設值與 Placeholder。
|
|
30
|
+
- 🚀 **原生 HTML 與 JavaScript 支援**:
|
|
31
|
+
- 可勾選開啟,直接在通知內容中渲染自訂 HTML、動畫或樣式。
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 📦 安裝方式 (Installation)
|
|
36
|
+
|
|
37
|
+
### 依賴需求 (Prerequisites)
|
|
38
|
+
此節點搭配 **`node-red-dashboard`** 使用,請先確保已安裝 Dashboard 套件:
|
|
39
|
+
```bash
|
|
40
|
+
npm install node-red-dashboard
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 透過 Node-RED 調色盤管理員安裝 (推薦)
|
|
44
|
+
1. 開啟 Node-RED 編輯器。
|
|
45
|
+
2. 點擊右上角漢堡選單 ➔ **節點管理 (Manage palette)** ➔ **安裝 (Install)**。
|
|
46
|
+
3. 搜尋 `node-red-contrib-notification-better` 並點選安裝。
|
|
47
|
+
|
|
48
|
+
### 透過 npm 命令列安裝
|
|
49
|
+
進入您的 Node-RED 使用者目錄(通常為 `~/.node-red`):
|
|
50
|
+
```bash
|
|
51
|
+
cd ~/.node-red
|
|
52
|
+
npm install node-red-contrib-notification-better
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 🛠️ 使用與訊息屬性說明 (Message Properties)
|
|
58
|
+
|
|
59
|
+
安裝完成後,會在左側節點列表(Palette)的 **dashboard** 分類中顯示 **`noti-better`** 節點。
|
|
60
|
+
|
|
61
|
+
### 支援的輸入訊息屬性 (Inputs)
|
|
62
|
+
|
|
63
|
+
| 屬性名稱 | 型別 | 預設來源 | 說明 |
|
|
64
|
+
| :--- | :--- | :--- | :--- |
|
|
65
|
+
| `msg.payload` | `string` \| `object` | 必填 | 通知的內文訊息。若開啟原始 HTML 模式,將忠實解析標籤。 |
|
|
66
|
+
| `msg.topic` / `msg.title` | `string` | 可選 | 通知的頂部標題。 |
|
|
67
|
+
| `msg.notificationType` | `string` | 可選 | 通知等級:`info`、`success`、`warning`、`error` 或 `default`。 |
|
|
68
|
+
| `msg.timeout` | `number` | 可選 | 顯示秒數(例如 `5` 代表 5 秒;設為 `0` 則立即消失)。 |
|
|
69
|
+
| `msg.ok` | `string` | 可選 | 確認按鈕標籤文字(大小寫忠實保留)。 |
|
|
70
|
+
| `msg.cancel` | `string` | 可選 | 取消按鈕標籤文字(大小寫忠實保留)。 |
|
|
71
|
+
| `msg.highlight` / `msg.border` | `string` | 可選 | 外框高亮顏色(例如 `#4CAF50` 或 `red`)。 |
|
|
72
|
+
| `msg.icon` | `string` | 可選 | 自訂 Font Awesome 圖示名稱(例如 `fa-bell` 或 `fa-check`)。 |
|
|
73
|
+
| `msg.playSound` | `boolean` | 可選 | 是否在此次通知觸發時播放提示音效。 |
|
|
74
|
+
| `msg.position` | `string` | 可選 | 覆蓋顯示位置(如 `top right`、`dialog`、`prompt` 等)。 |
|
|
75
|
+
| `msg.className` | `string` | 可選 | 附加自訂 CSS 類別名稱。 |
|
|
76
|
+
| `msg.socketid` | `string` | 可選 | 當關閉廣播所有連線時,指定特定使用者之 Socket ID。 |
|
|
77
|
+
|
|
78
|
+
### 輸出訊息 (Outputs)
|
|
79
|
+
當佈局選擇 **Dialog (對話框)** 或 **Prompt (文字輸入框)** 時:
|
|
80
|
+
- 使用者點擊按鈕或送出輸入文字後,結果將由節點輸出點送出(確認按鈕輸出 `msg.ok` 或輸入文字;取消按鈕輸出 `msg.cancel`)。
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 📄 授權 (License)
|
|
85
|
+
|
|
86
|
+
[MIT](LICENSE)
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"noti-better": {
|
|
3
|
+
"paletteLabel": "noti-better",
|
|
4
|
+
"label": {
|
|
5
|
+
"position": "Layout",
|
|
6
|
+
"notificationType": "Notification Type",
|
|
7
|
+
"displayTime": "Timeout (sec)",
|
|
8
|
+
"highlight": "Border Color",
|
|
9
|
+
"showIcon": "Show Icon",
|
|
10
|
+
"customIcon": "Custom Icon",
|
|
11
|
+
"playSound": "Play Sound Alert",
|
|
12
|
+
"showCountdown": "Countdown Progress Bar",
|
|
13
|
+
"preserveCase": "Preserve Button Casing (No Force Uppercase)",
|
|
14
|
+
"sendall": "Send to all browser sessions",
|
|
15
|
+
"ok": "Default Action Button",
|
|
16
|
+
"cancel": "Secondary Action Button",
|
|
17
|
+
"promptDefault": "Default Input Text",
|
|
18
|
+
"promptPlaceholder": "Input Placeholder",
|
|
19
|
+
"raw": "Accept raw HTML / JavaScript in msg.payload",
|
|
20
|
+
"className": "Custom CSS Class",
|
|
21
|
+
"topic": "Topic / Title",
|
|
22
|
+
"name": "Node Name",
|
|
23
|
+
"showDialog": "show dialog",
|
|
24
|
+
"showNotification": "noti-better"
|
|
25
|
+
},
|
|
26
|
+
"option": {
|
|
27
|
+
"topRight": "Top Right",
|
|
28
|
+
"bottomRight": "Bottom Right",
|
|
29
|
+
"topLeft": "Top Left",
|
|
30
|
+
"bottomLeft": "Bottom Left",
|
|
31
|
+
"dialog": "OK / Cancel Confirmation Dialog",
|
|
32
|
+
"prompt": "Dialog with Text Input (Prompt)",
|
|
33
|
+
"typeDefault": "Default (Neutral)",
|
|
34
|
+
"typeInfo": "ℹ Info (Blue)",
|
|
35
|
+
"typeSuccess": "✓ Success (Green)",
|
|
36
|
+
"typeWarning": "⚠ Warning (Amber)",
|
|
37
|
+
"typeError": "✕ Error (Red)"
|
|
38
|
+
},
|
|
39
|
+
"placeholder": {
|
|
40
|
+
"timeout": "e.g. 3 or [msg.timeout]",
|
|
41
|
+
"highlight": "e.g. #ff0000 or red",
|
|
42
|
+
"customIcon": "e.g. fa-bell, fa-check",
|
|
43
|
+
"ok": "e.g. Ok, Confirm, 確定",
|
|
44
|
+
"cancel": "e.g. Cancel, 取消",
|
|
45
|
+
"promptDefault": "Optional default value",
|
|
46
|
+
"promptPlaceholder": "Placeholder in text box",
|
|
47
|
+
"className": "[msg.className]",
|
|
48
|
+
"topic": "[msg.topic] or custom title",
|
|
49
|
+
"name": "Node name"
|
|
50
|
+
},
|
|
51
|
+
"section": {
|
|
52
|
+
"basic": "General Settings",
|
|
53
|
+
"appearance": "Appearance & Styling",
|
|
54
|
+
"dialog": "Dialog Configuration",
|
|
55
|
+
"advanced": "Behavior & Advanced"
|
|
56
|
+
},
|
|
57
|
+
"tip": {
|
|
58
|
+
"raw": "<b>Security Notice</b>: Allowing raw HTML/JavaScript can enable code execution. Only enable this if the message payload originates from a trusted source.",
|
|
59
|
+
"icon": "Enter Font Awesome 4 icon name (e.g. fa-bell). If enabled without custom icon, an icon matching the notification type will automatically be displayed.",
|
|
60
|
+
"preserveCase": "When checked, button text will exactly match what you typed instead of being forced into uppercase by Angular Material."
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"noti-better": {
|
|
3
|
+
"paletteLabel": "noti-better",
|
|
4
|
+
"label": {
|
|
5
|
+
"position": "表示位置",
|
|
6
|
+
"notificationType": "通知タイプ",
|
|
7
|
+
"displayTime": "表示時間 (秒)",
|
|
8
|
+
"highlight": "枠線の色",
|
|
9
|
+
"showIcon": "アイコンを表示",
|
|
10
|
+
"customIcon": "カスタムアイコン",
|
|
11
|
+
"playSound": "通知音を再生",
|
|
12
|
+
"showCountdown": "カウントダウンバーを表示",
|
|
13
|
+
"preserveCase": "ボタンの大文字強制を無効化 (入力通り表示)",
|
|
14
|
+
"sendall": "全ブラウザセッションに送信",
|
|
15
|
+
"ok": "確認ボタン",
|
|
16
|
+
"cancel": "キャンセルボタン",
|
|
17
|
+
"promptDefault": "入力欄の初期値",
|
|
18
|
+
"promptPlaceholder": "入力欄のプレースホルダー",
|
|
19
|
+
"raw": "msg.payload の生 HTML / JS を許可",
|
|
20
|
+
"className": "カスタム CSS クラス",
|
|
21
|
+
"topic": "トピック / タイトル",
|
|
22
|
+
"name": "ノード名",
|
|
23
|
+
"showDialog": "ダイアログを表示",
|
|
24
|
+
"showNotification": "noti-better"
|
|
25
|
+
},
|
|
26
|
+
"option": {
|
|
27
|
+
"topRight": "右上 (Top Right)",
|
|
28
|
+
"bottomRight": "右下 (Bottom Right)",
|
|
29
|
+
"topLeft": "左上 (Top Left)",
|
|
30
|
+
"bottomLeft": "左下 (Bottom Left)",
|
|
31
|
+
"dialog": "OK / キャンセル 確認ダイアログ",
|
|
32
|
+
"prompt": "テキスト入力付きダイアログ",
|
|
33
|
+
"typeDefault": "デフォルト (標準)",
|
|
34
|
+
"typeInfo": "ℹ 情報 (青)",
|
|
35
|
+
"typeSuccess": "✓ 成功 (緑)",
|
|
36
|
+
"typeWarning": "⚠ 警告 (黄)",
|
|
37
|
+
"typeError": "✕ エラー (赤)"
|
|
38
|
+
},
|
|
39
|
+
"placeholder": {
|
|
40
|
+
"timeout": "例: 3 または [msg.timeout]",
|
|
41
|
+
"highlight": "例: #ff0000 または red",
|
|
42
|
+
"customIcon": "例: fa-bell, fa-check",
|
|
43
|
+
"ok": "例: Ok, 確定",
|
|
44
|
+
"cancel": "例: Cancel, 中止",
|
|
45
|
+
"promptDefault": "初期入力値(任意)",
|
|
46
|
+
"promptPlaceholder": "プレースホルダー(任意)",
|
|
47
|
+
"className": "[msg.className]",
|
|
48
|
+
"topic": "[msg.topic] またはカスタムタイトル",
|
|
49
|
+
"name": "ノード名"
|
|
50
|
+
},
|
|
51
|
+
"section": {
|
|
52
|
+
"basic": "基本設定",
|
|
53
|
+
"appearance": "外観とスタイル",
|
|
54
|
+
"dialog": "ダイアログ設定",
|
|
55
|
+
"advanced": "動作と詳細設定"
|
|
56
|
+
},
|
|
57
|
+
"tip": {
|
|
58
|
+
"raw": "<b>セキュリティの注意</b>:生の HTML/JS を許可するとスクリプト実行のリスクがあります。信頼できる入力の場合のみ有効にしてください。",
|
|
59
|
+
"icon": "Font Awesome 4 のアイコン名を入力してください (例: fa-bell)。空欄の場合は通知タイプに応じたアイコンが表示されます。",
|
|
60
|
+
"preserveCase": "チェックを入れると、ボタン文字が Angular Material により強制的に大文字化されるのを防ぎ、入力した通りの大文字・小文字で表示されます。"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"noti-better": {
|
|
3
|
+
"paletteLabel": "noti-better",
|
|
4
|
+
"label": {
|
|
5
|
+
"position": "布局位置",
|
|
6
|
+
"notificationType": "通知类型",
|
|
7
|
+
"displayTime": "显示时间 (秒)",
|
|
8
|
+
"highlight": "边框颜色",
|
|
9
|
+
"showIcon": "显示图标",
|
|
10
|
+
"customIcon": "自定义图标",
|
|
11
|
+
"playSound": "播放提示音效",
|
|
12
|
+
"showCountdown": "显示倒计时进度条",
|
|
13
|
+
"preserveCase": "保留按钮原始大小写 (不强制大写)",
|
|
14
|
+
"sendall": "发送至所有浏览器会话",
|
|
15
|
+
"ok": "确认按钮标签",
|
|
16
|
+
"cancel": "取消按钮标签",
|
|
17
|
+
"promptDefault": "默认输入文本",
|
|
18
|
+
"promptPlaceholder": "输入框提示字",
|
|
19
|
+
"raw": "允许 msg.payload 中的原始 HTML / JavaScript",
|
|
20
|
+
"className": "CSS 类名",
|
|
21
|
+
"topic": "主题标题",
|
|
22
|
+
"name": "节点名称",
|
|
23
|
+
"showDialog": "显示对话框",
|
|
24
|
+
"showNotification": "noti-better"
|
|
25
|
+
},
|
|
26
|
+
"option": {
|
|
27
|
+
"topRight": "右上角 (Top Right)",
|
|
28
|
+
"bottomRight": "右下角 (Bottom Right)",
|
|
29
|
+
"topLeft": "左上角 (Top Left)",
|
|
30
|
+
"bottomLeft": "左下角 (Bottom Left)",
|
|
31
|
+
"dialog": "确认 / 取消 对话框 (Dialog)",
|
|
32
|
+
"prompt": "带文本输入框的对话框 (Prompt)",
|
|
33
|
+
"typeDefault": "默认 (中性)",
|
|
34
|
+
"typeInfo": "ℹ 信息 (蓝色)",
|
|
35
|
+
"typeSuccess": "✓ 成功 (绿色)",
|
|
36
|
+
"typeWarning": "⚠ 警告 (琥珀黄)",
|
|
37
|
+
"typeError": "✕ 错误 (红色)"
|
|
38
|
+
},
|
|
39
|
+
"placeholder": {
|
|
40
|
+
"timeout": "例如 3 或 [msg.timeout]",
|
|
41
|
+
"highlight": "例如 #ff0000 或 red",
|
|
42
|
+
"customIcon": "例如 fa-bell、fa-check",
|
|
43
|
+
"ok": "例如 Ok、确定、Submit",
|
|
44
|
+
"cancel": "例如 Cancel、取消",
|
|
45
|
+
"promptDefault": "可选默认输入值",
|
|
46
|
+
"promptPlaceholder": "文本框内的提示字",
|
|
47
|
+
"className": "[msg.className]",
|
|
48
|
+
"topic": "[msg.topic] 或自定义标题",
|
|
49
|
+
"name": "节点名称"
|
|
50
|
+
},
|
|
51
|
+
"section": {
|
|
52
|
+
"basic": "基本设置",
|
|
53
|
+
"appearance": "外观与样式",
|
|
54
|
+
"dialog": "对话框设置",
|
|
55
|
+
"advanced": "高级与行为设置"
|
|
56
|
+
},
|
|
57
|
+
"tip": {
|
|
58
|
+
"raw": "<b>安全提醒</b>:启用原始 HTML/JavaScript 可能允许执行任意脚本。仅在消息内容来自可信来源时开启此选项。",
|
|
59
|
+
"icon": "请输入 Font Awesome 4 图标名称(例如 fa-bell、fa-shield)。若未自定义图标,系统将依据通知类型自动呈现对应图标。",
|
|
60
|
+
"preserveCase": "勾选后,按钮文本将完全依据您所输入的内容呈现,不再被 Angular Material 强制转换为全大写。"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"noti-better": {
|
|
3
|
+
"paletteLabel": "noti-better",
|
|
4
|
+
"label": {
|
|
5
|
+
"position": "佈局位置",
|
|
6
|
+
"notificationType": "通知類型",
|
|
7
|
+
"displayTime": "顯示時間 (秒)",
|
|
8
|
+
"highlight": "外框顏色",
|
|
9
|
+
"showIcon": "顯示圖示",
|
|
10
|
+
"customIcon": "自訂圖示",
|
|
11
|
+
"playSound": "播放提示音效",
|
|
12
|
+
"showCountdown": "顯示倒數進度條",
|
|
13
|
+
"preserveCase": "保留按鈕原始大小寫 (不強制大寫)",
|
|
14
|
+
"sendall": "發送至所有瀏覽器工作階段",
|
|
15
|
+
"ok": "確認按鈕標籤",
|
|
16
|
+
"cancel": "取消按鈕標籤",
|
|
17
|
+
"promptDefault": "預設輸入文字",
|
|
18
|
+
"promptPlaceholder": "輸入框提示字",
|
|
19
|
+
"raw": "允許 msg.payload 中的原始 HTML / JavaScript",
|
|
20
|
+
"className": "CSS 類別",
|
|
21
|
+
"topic": "主題標題",
|
|
22
|
+
"name": "節點名稱",
|
|
23
|
+
"showDialog": "顯示對話框",
|
|
24
|
+
"showNotification": "進階通知"
|
|
25
|
+
},
|
|
26
|
+
"option": {
|
|
27
|
+
"topRight": "右上角 (Top Right)",
|
|
28
|
+
"bottomRight": "右下角 (Bottom Right)",
|
|
29
|
+
"topLeft": "左上角 (Top Left)",
|
|
30
|
+
"bottomLeft": "左下角 (Bottom Left)",
|
|
31
|
+
"dialog": "確認 / 取消 對話框 (Dialog)",
|
|
32
|
+
"prompt": "帶文字輸入框的對話框 (Prompt)",
|
|
33
|
+
"typeDefault": "預設 (中性)",
|
|
34
|
+
"typeInfo": "ℹ 資訊 (藍色)",
|
|
35
|
+
"typeSuccess": "✓ 成功 (綠色)",
|
|
36
|
+
"typeWarning": "⚠ 警告 (琥珀黃)",
|
|
37
|
+
"typeError": "✕ 錯誤 (紅色)"
|
|
38
|
+
},
|
|
39
|
+
"placeholder": {
|
|
40
|
+
"timeout": "例如 3 或 [msg.timeout]",
|
|
41
|
+
"highlight": "例如 #ff0000 或 red",
|
|
42
|
+
"customIcon": "例如 fa-bell、fa-check",
|
|
43
|
+
"ok": "例如 Ok、確定、Submit",
|
|
44
|
+
"cancel": "例如 Cancel、取消",
|
|
45
|
+
"promptDefault": "可選預設輸入值",
|
|
46
|
+
"promptPlaceholder": "文字框內的提示字",
|
|
47
|
+
"className": "[msg.className]",
|
|
48
|
+
"topic": "[msg.topic] 或自訂標題",
|
|
49
|
+
"name": "節點名稱"
|
|
50
|
+
},
|
|
51
|
+
"section": {
|
|
52
|
+
"basic": "基本設定",
|
|
53
|
+
"appearance": "外觀與樣式",
|
|
54
|
+
"dialog": "對話框設定",
|
|
55
|
+
"advanced": "進階與行為設定"
|
|
56
|
+
},
|
|
57
|
+
"tip": {
|
|
58
|
+
"raw": "<b>安全提醒</b>:啟用原始 HTML/JavaScript 可能允許執行任意腳本。僅在訊息內容來自可信來源時開啟此選項。",
|
|
59
|
+
"icon": "請輸入 Font Awesome 4 圖示名稱(例如 fa-bell、fa-shield)。若未自訂圖示,系統將依據通知類型自動呈現對應圖示。",
|
|
60
|
+
"preserveCase": "勾選後,按鈕文字將完全依據您所輸入的內容呈現,不再被 Angular Material 強制轉換為全大寫。"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-notification-better",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "An enhanced notification and dialog node for Node-RED Dashboard supporting exact button casing, preset notification levels, audio alerts, countdown progress bar, and multi-language UI.",
|
|
5
|
+
"main": "ui_notification.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 0"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"node-red",
|
|
11
|
+
"node-red-dashboard",
|
|
12
|
+
"notification",
|
|
13
|
+
"toast",
|
|
14
|
+
"dialog",
|
|
15
|
+
"prompt",
|
|
16
|
+
"alert",
|
|
17
|
+
"noti-better",
|
|
18
|
+
"ui"
|
|
19
|
+
],
|
|
20
|
+
"node-red": {
|
|
21
|
+
"version": ">=1.0.0",
|
|
22
|
+
"nodes": {
|
|
23
|
+
"noti-better": "ui_notification.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"files": [
|
|
29
|
+
"ui_notification.js",
|
|
30
|
+
"ui_notification.html",
|
|
31
|
+
"locales",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"node-red-dashboard": ">=2.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('noti-better', {
|
|
3
|
+
category: (RED._("node-red-dashboard/ui_base:ui_base.label.category") || 'dashboard'),
|
|
4
|
+
color: 'rgb(119, 198, 204)',
|
|
5
|
+
defaults: {
|
|
6
|
+
position: {value: 'top right'},
|
|
7
|
+
notificationType: {value: 'default'},
|
|
8
|
+
displayTime: {value: '3'},
|
|
9
|
+
highlight: {value: ''},
|
|
10
|
+
showIcon: {value: true},
|
|
11
|
+
customIcon: {value: ''},
|
|
12
|
+
playSound: {value: false},
|
|
13
|
+
showCountdown: {value: false},
|
|
14
|
+
preserveCase: {value: true},
|
|
15
|
+
sendall: {value: true},
|
|
16
|
+
outputs: {value: 0},
|
|
17
|
+
ok: {value: 'Ok', required: true},
|
|
18
|
+
cancel: {value: ''},
|
|
19
|
+
promptDefault: {value: ''},
|
|
20
|
+
promptPlaceholder: {value: ''},
|
|
21
|
+
raw: {value: false},
|
|
22
|
+
className: {value: ''},
|
|
23
|
+
topic: {value: ''},
|
|
24
|
+
name: {value: ''}
|
|
25
|
+
},
|
|
26
|
+
inputs: 1,
|
|
27
|
+
outputs: 0,
|
|
28
|
+
align: "right",
|
|
29
|
+
icon: "font-awesome/fa-bell",
|
|
30
|
+
paletteLabel: function() {
|
|
31
|
+
try {
|
|
32
|
+
var text = this._("noti-better.paletteLabel");
|
|
33
|
+
return (text && text.indexOf(".") === -1 && text.indexOf("noti-better") === -1) ? text : "noti-better";
|
|
34
|
+
} catch(e) {
|
|
35
|
+
return "noti-better";
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
label: function() {
|
|
39
|
+
if (this.name) { return this.name; }
|
|
40
|
+
var isDlg = (this.position === "prompt" || this.position === "dialog");
|
|
41
|
+
var fallback = isDlg ? "顯示對話框" : "進階通知";
|
|
42
|
+
try {
|
|
43
|
+
var key = isDlg ? "noti-better.label.showDialog" : "noti-better.label.showNotification";
|
|
44
|
+
var text = this._(key);
|
|
45
|
+
return (text && text.indexOf(".") === -1 && text !== key) ? text : fallback;
|
|
46
|
+
} catch(e) {
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
labelStyle: function() {
|
|
51
|
+
return this.name ? "node_label_italic" : "";
|
|
52
|
+
},
|
|
53
|
+
oneditprepare: function() {
|
|
54
|
+
var node = this;
|
|
55
|
+
|
|
56
|
+
function updateFormVisibility() {
|
|
57
|
+
var pos = $("#node-input-position").val();
|
|
58
|
+
var isDialog = (pos === 'dialog' || pos === 'prompt');
|
|
59
|
+
var isPrompt = (pos === 'prompt');
|
|
60
|
+
|
|
61
|
+
if (isDialog) {
|
|
62
|
+
// 對話框模式
|
|
63
|
+
$("#section-toast-settings").slideUp(150);
|
|
64
|
+
$("#section-dialog-settings").slideDown(200);
|
|
65
|
+
|
|
66
|
+
if (isPrompt) {
|
|
67
|
+
$("#node-dialog-prompt-fields").slideDown(150);
|
|
68
|
+
if (!$("#node-input-cancel").val()) {
|
|
69
|
+
$("#node-input-cancel").val("Cancel");
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
$("#node-dialog-prompt-fields").slideUp(150);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// 浮動通知模式
|
|
76
|
+
$("#section-toast-settings").slideDown(200);
|
|
77
|
+
$("#section-dialog-settings").slideUp(150);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
$("#node-input-position").on("change", updateFormVisibility);
|
|
82
|
+
updateFormVisibility();
|
|
83
|
+
|
|
84
|
+
// 通知等級切換時的顏色提示輔助
|
|
85
|
+
$("#node-input-notificationType").on("change", function() {
|
|
86
|
+
var t = $(this).val();
|
|
87
|
+
var colors = {
|
|
88
|
+
"info": "#2196F3",
|
|
89
|
+
"success": "#4CAF50",
|
|
90
|
+
"warning": "#FF9800",
|
|
91
|
+
"error": "#F44336",
|
|
92
|
+
"default": ""
|
|
93
|
+
};
|
|
94
|
+
var c = colors[t];
|
|
95
|
+
if (c && !$("#node-input-highlight").val()) {
|
|
96
|
+
$("#node-input-highlight").attr("placeholder", c + " (" + t + ")");
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 圖示勾選聯動
|
|
101
|
+
$("#node-input-showIcon").on("change", function() {
|
|
102
|
+
if ($(this).is(":checked")) {
|
|
103
|
+
$("#node-row-customIcon").slideDown(150);
|
|
104
|
+
} else {
|
|
105
|
+
$("#node-row-customIcon").slideUp(150);
|
|
106
|
+
}
|
|
107
|
+
}).trigger("change");
|
|
108
|
+
|
|
109
|
+
// 安全警語聯動
|
|
110
|
+
$("#node-input-raw").on("change", function() {
|
|
111
|
+
if ($(this).is(":checked")) {
|
|
112
|
+
$("#node-tip-raw-warning").slideDown(200);
|
|
113
|
+
} else {
|
|
114
|
+
$("#node-tip-raw-warning").slideUp(200);
|
|
115
|
+
}
|
|
116
|
+
}).trigger("change");
|
|
117
|
+
},
|
|
118
|
+
oneditsave: function() {
|
|
119
|
+
var pos = $("#node-input-position").val();
|
|
120
|
+
if (pos === 'dialog' || pos === 'prompt') {
|
|
121
|
+
this.outputs = 1;
|
|
122
|
+
} else {
|
|
123
|
+
this.outputs = 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<style>
|
|
130
|
+
.nr-noti-section-title {
|
|
131
|
+
font-weight: bold;
|
|
132
|
+
margin-top: 16px;
|
|
133
|
+
margin-bottom: 10px;
|
|
134
|
+
padding-bottom: 4px;
|
|
135
|
+
border-bottom: 2px solid #77c6cc;
|
|
136
|
+
display: inline-block;
|
|
137
|
+
letter-spacing: 0.5px;
|
|
138
|
+
}
|
|
139
|
+
.nr-noti-tip-box {
|
|
140
|
+
margin: 10px 0;
|
|
141
|
+
padding: 8px 12px;
|
|
142
|
+
background-color: rgba(255, 184, 0, 0.15);
|
|
143
|
+
border-left: 4px solid #ffb800;
|
|
144
|
+
border-radius: 3px;
|
|
145
|
+
font-size: 12px;
|
|
146
|
+
line-height: 1.5;
|
|
147
|
+
}
|
|
148
|
+
</style>
|
|
149
|
+
|
|
150
|
+
<script type="text/html" data-template-name="noti-better">
|
|
151
|
+
<!-- 基本設定區塊 -->
|
|
152
|
+
<div class="form-row">
|
|
153
|
+
<span class="nr-noti-section-title">
|
|
154
|
+
<i class="fa fa-sliders"></i> <span data-i18n="noti-better.section.basic">基本設定</span>
|
|
155
|
+
</span>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="form-row">
|
|
159
|
+
<label for="node-input-position"><i class="fa fa-th-large"></i> <span data-i18n="noti-better.label.position">佈局位置</span></label>
|
|
160
|
+
<select id="node-input-position" style="width:70%;">
|
|
161
|
+
<option value="top right" data-i18n="noti-better.option.topRight">右上角 (Top Right)</option>
|
|
162
|
+
<option value="bottom right" data-i18n="noti-better.option.bottomRight">右下角 (Bottom Right)</option>
|
|
163
|
+
<option value="top left" data-i18n="noti-better.option.topLeft">左上角 (Top Left)</option>
|
|
164
|
+
<option value="bottom left" data-i18n="noti-better.option.bottomLeft">左下角 (Bottom Left)</option>
|
|
165
|
+
<option value="dialog" data-i18n="noti-better.option.dialog">確認 / 取消 對話框 (Dialog)</option>
|
|
166
|
+
<option value="prompt" data-i18n="noti-better.option.prompt">帶文字輸入框的對話框 (Prompt)</option>
|
|
167
|
+
</select>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div class="form-row">
|
|
171
|
+
<label for="node-input-notificationType"><i class="fa fa-tag"></i> <span data-i18n="noti-better.label.notificationType">通知類型</span></label>
|
|
172
|
+
<select id="node-input-notificationType" style="width:70%;">
|
|
173
|
+
<option value="default" data-i18n="noti-better.option.typeDefault">預設 (中性)</option>
|
|
174
|
+
<option value="info" data-i18n="noti-better.option.typeInfo">ℹ 資訊 (藍色)</option>
|
|
175
|
+
<option value="success" data-i18n="noti-better.option.typeSuccess">✓ 成功 (綠色)</option>
|
|
176
|
+
<option value="warning" data-i18n="noti-better.option.typeWarning">⚠ 警告 (琥珀黃)</option>
|
|
177
|
+
<option value="error" data-i18n="noti-better.option.typeError">✕ 錯誤 (紅色)</option>
|
|
178
|
+
</select>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="form-row">
|
|
182
|
+
<label for="node-input-topic"><i class="fa fa-tasks"></i> <span data-i18n="noti-better.label.topic">主題標題</span></label>
|
|
183
|
+
<input type="text" id="node-input-topic" placeholder="[msg.topic] 或自訂標題" data-i18n="[placeholder]noti-better.placeholder.topic">
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<!-- 浮動通知專用設定 (Timeout, 進度條等) -->
|
|
187
|
+
<div id="section-toast-settings">
|
|
188
|
+
<div class="form-row">
|
|
189
|
+
<label for="node-input-displayTime"><i class="fa fa-clock-o"></i> <span data-i18n="noti-better.label.displayTime">顯示時間 (秒)</span></label>
|
|
190
|
+
<input type="text" id="node-input-displayTime" placeholder="例如 3 或 [msg.timeout]" data-i18n="[placeholder]noti-better.placeholder.timeout">
|
|
191
|
+
</div>
|
|
192
|
+
<div class="form-row">
|
|
193
|
+
<label style="width:auto;" for="node-input-showCountdown">
|
|
194
|
+
<input type="checkbox" id="node-input-showCountdown" style="display:inline-block; width:auto; vertical-align:baseline; margin-right:6px;">
|
|
195
|
+
<span data-i18n="noti-better.label.showCountdown">顯示倒數進度條</span>
|
|
196
|
+
</label>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<!-- 外觀與圖示設定區塊 -->
|
|
201
|
+
<div class="form-row">
|
|
202
|
+
<span class="nr-noti-section-title">
|
|
203
|
+
<i class="fa fa-paint-brush"></i> <span data-i18n="noti-better.section.appearance">外觀與樣式</span>
|
|
204
|
+
</span>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div class="form-row">
|
|
208
|
+
<label for="node-input-highlight"><i class="fa fa-square-o"></i> <span data-i18n="noti-better.label.highlight">外框顏色</span></label>
|
|
209
|
+
<input type="text" id="node-input-highlight" placeholder="例如 #ff0000 或 red" data-i18n="[placeholder]noti-better.placeholder.highlight">
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div class="form-row">
|
|
213
|
+
<label style="width:auto;" for="node-input-showIcon">
|
|
214
|
+
<input type="checkbox" id="node-input-showIcon" style="display:inline-block; width:auto; vertical-align:baseline; margin-right:6px;">
|
|
215
|
+
<span data-i18n="noti-better.label.showIcon">顯示圖示</span>
|
|
216
|
+
</label>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div class="form-row" id="node-row-customIcon">
|
|
220
|
+
<label for="node-input-customIcon"><i class="fa fa-font-awesome"></i> <span data-i18n="noti-better.label.customIcon">自訂圖示</span></label>
|
|
221
|
+
<input type="text" id="node-input-customIcon" placeholder="例如 fa-bell、fa-check" data-i18n="[placeholder]noti-better.placeholder.customIcon">
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div class="form-row">
|
|
225
|
+
<label for="node-input-className"><i class="fa fa-code"></i> <span data-i18n="noti-better.label.className">CSS 類別</span></label>
|
|
226
|
+
<input type="text" id="node-input-className" placeholder="[msg.className]" data-i18n="[placeholder]noti-better.placeholder.className">
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<!-- 對話框專用設定 (按鈕與大小寫) -->
|
|
230
|
+
<div id="section-dialog-settings" style="display:none;">
|
|
231
|
+
<div class="form-row">
|
|
232
|
+
<span class="nr-noti-section-title">
|
|
233
|
+
<i class="fa fa-commenting-o"></i> <span data-i18n="noti-better.section.dialog">對話框設定</span>
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div class="form-row">
|
|
238
|
+
<label for="node-input-ok"><i class="fa fa-check"></i> <span data-i18n="noti-better.label.ok">確認按鈕標籤</span></label>
|
|
239
|
+
<input type="text" id="node-input-ok" placeholder="例如 Ok、確定、Submit" data-i18n="[placeholder]noti-better.placeholder.ok">
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div class="form-row">
|
|
243
|
+
<label for="node-input-cancel"><i class="fa fa-times"></i> <span data-i18n="noti-better.label.cancel">取消按鈕標籤</span></label>
|
|
244
|
+
<input type="text" id="node-input-cancel" placeholder="例如 Cancel、取消" data-i18n="[placeholder]noti-better.placeholder.cancel">
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div id="node-dialog-prompt-fields" style="display:none;">
|
|
248
|
+
<div class="form-row">
|
|
249
|
+
<label for="node-input-promptDefault"><i class="fa fa-pencil-square-o"></i> <span data-i18n="noti-better.label.promptDefault">預設輸入文字</span></label>
|
|
250
|
+
<input type="text" id="node-input-promptDefault" placeholder="可選預設輸入值" data-i18n="[placeholder]noti-better.placeholder.promptDefault">
|
|
251
|
+
</div>
|
|
252
|
+
<div class="form-row">
|
|
253
|
+
<label for="node-input-promptPlaceholder"><i class="fa fa-i-cursor"></i> <span data-i18n="noti-better.label.promptPlaceholder">輸入框提示字</span></label>
|
|
254
|
+
<input type="text" id="node-input-promptPlaceholder" placeholder="輸入框內的提示字 (Placeholder)" data-i18n="[placeholder]noti-better.placeholder.promptPlaceholder">
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div class="form-row">
|
|
259
|
+
<label style="width:auto;" for="node-input-preserveCase">
|
|
260
|
+
<input type="checkbox" id="node-input-preserveCase" style="display:inline-block; width:auto; vertical-align:baseline; margin-right:6px;">
|
|
261
|
+
<span data-i18n="noti-better.label.preserveCase">保留按鈕原始大小寫 (不強制大寫)</span>
|
|
262
|
+
</label>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<!-- 行為與進階設定區塊 -->
|
|
267
|
+
<div class="form-row">
|
|
268
|
+
<span class="nr-noti-section-title">
|
|
269
|
+
<i class="fa fa-cogs"></i> <span data-i18n="noti-better.section.advanced">進階與行為設定</span>
|
|
270
|
+
</span>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div class="form-row">
|
|
274
|
+
<label style="width:auto;" for="node-input-playSound">
|
|
275
|
+
<input type="checkbox" id="node-input-playSound" style="display:inline-block; width:auto; vertical-align:baseline; margin-right:6px;">
|
|
276
|
+
<span data-i18n="noti-better.label.playSound">播放提示音效</span>
|
|
277
|
+
</label>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<div class="form-row">
|
|
281
|
+
<label style="width:auto;" for="node-input-sendall">
|
|
282
|
+
<input type="checkbox" id="node-input-sendall" style="display:inline-block; width:auto; vertical-align:baseline; margin-right:6px;">
|
|
283
|
+
<span data-i18n="noti-better.label.sendall">發送至所有瀏覽器工作階段</span>
|
|
284
|
+
</label>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div class="form-row">
|
|
288
|
+
<label style="width:auto;" for="node-input-raw">
|
|
289
|
+
<input type="checkbox" id="node-input-raw" style="display:inline-block; width:auto; vertical-align:baseline; margin-right:6px;">
|
|
290
|
+
<span data-i18n="noti-better.label.raw">允許 msg.payload 中的原始 HTML / JavaScript</span>
|
|
291
|
+
</label>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div id="node-tip-raw-warning" class="nr-noti-tip-box" style="display:none;" data-i18n="[html]noti-better.tip.raw">
|
|
295
|
+
<b>安全提醒</b>:啟用原始 HTML/JavaScript 可能允許執行任意腳本。僅在訊息內容來自可信來源時開啟此選項。
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<div class="form-row" style="margin-top:14px;">
|
|
299
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="noti-better.label.name">節點名稱</span></label>
|
|
300
|
+
<input type="text" id="node-input-name" placeholder="節點名稱" data-i18n="[placeholder]noti-better.placeholder.name">
|
|
301
|
+
</div>
|
|
302
|
+
</script>
|
|
303
|
+
|
|
304
|
+
<script type="text/html" data-help-name="noti-better">
|
|
305
|
+
<p>專為 <b>Node-RED Dashboard</b> 量身打造的強化版通知 (Notification / Toast) 與互動式對話框 (Dialog) 節點。</p>
|
|
306
|
+
|
|
307
|
+
<h3>核心特色</h3>
|
|
308
|
+
<ul>
|
|
309
|
+
<li><b>忠實保留按鈕大小寫</b>:徹底解除 Angular Material 強制大寫的限制,輸入「Ok」即顯示「Ok」,大小寫百分之百精確呈現。</li>
|
|
310
|
+
<li><b>通知等級預設</b>:內建 Info(資訊)、Success(成功)、Warning(警告)、Error(錯誤)四種視覺主題與自動圖示。</li>
|
|
311
|
+
<li><b>音效提醒</b>:內建 Web Audio API 音效支援,可發出清脆的提示聲。</li>
|
|
312
|
+
<li><b>豐富的自訂性</b>:支援原生 HTML / JavaScript、倒數進度條、自訂外框顏色與圖示。</li>
|
|
313
|
+
</ul>
|
|
314
|
+
|
|
315
|
+
<h3>輸入屬性 (Inputs)</h3>
|
|
316
|
+
<dl class="message-properties">
|
|
317
|
+
<dt>payload <span class="property-type">字串 | 物件</span></dt>
|
|
318
|
+
<dd>顯示在彈出框中的主要內容。若啟用「允許原始 HTML / JS」,將忠實渲染 HTML 與動畫標籤。</dd>
|
|
319
|
+
|
|
320
|
+
<dt class="optional">topic <span class="property-type">字串</span></dt>
|
|
321
|
+
<dd>通知或對話框的頂部標題。</dd>
|
|
322
|
+
|
|
323
|
+
<dt class="optional">timeout <span class="property-type">數值</span></dt>
|
|
324
|
+
<dd>自動關閉的顯示秒數(預設 3 秒),設為 0 會立即關閉。</dd>
|
|
325
|
+
|
|
326
|
+
<dt class="optional">notificationType <span class="property-type">字串</span></dt>
|
|
327
|
+
<dd>通知等級,可指定為 <code>default</code>、<code>info</code>、<code>success</code>、<code>warning</code> 或 <code>error</code>。</dd>
|
|
328
|
+
|
|
329
|
+
<dt class="optional">ok <span class="property-type">字串</span></dt>
|
|
330
|
+
<dd>確認按鈕文字(完全保留原始大小寫)。</dd>
|
|
331
|
+
|
|
332
|
+
<dt class="optional">cancel <span class="property-type">字串</span></dt>
|
|
333
|
+
<dd>取消按鈕文字(完全保留原始大小寫)。</dd>
|
|
334
|
+
|
|
335
|
+
<dt class="optional">highlight <span class="property-type">字串</span></dt>
|
|
336
|
+
<dd>外框高亮顏色(例如 <code>#2ecc71</code> 或 <code>green</code>)。</dd>
|
|
337
|
+
|
|
338
|
+
<dt class="optional">icon <span class="property-type">字串</span></dt>
|
|
339
|
+
<dd>自訂 Font Awesome 圖示名稱(例如 <code>fa-bell</code>)。</dd>
|
|
340
|
+
|
|
341
|
+
<dt class="optional">playSound <span class="property-type">布林值</span></dt>
|
|
342
|
+
<dd>是否在彈出通知時播放提示音效。</dd>
|
|
343
|
+
|
|
344
|
+
<dt class="optional">socketid <span class="property-type">字串</span></dt>
|
|
345
|
+
<dd>若未勾選廣播至所有工作階段,可指定單一瀏覽器連線的 Session ID。</dd>
|
|
346
|
+
</dl>
|
|
347
|
+
|
|
348
|
+
<h3>輸出屬性 (Outputs)</h3>
|
|
349
|
+
<dl class="message-properties">
|
|
350
|
+
<dt>payload <span class="property-type">字串</span></dt>
|
|
351
|
+
<dd>當佈局設為 <b>Dialog</b>(確認對話框)或 <b>Prompt</b>(文字輸入對話框)時,使用者點擊或輸入確認後的結果將由輸出點發送。</dd>
|
|
352
|
+
</dl>
|
|
353
|
+
</script>
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node-red-contrib-notification-better (noti-better)
|
|
3
|
+
* 專為 Node-RED Dashboard 設計的進階通知與互動對話框節點
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = function(RED) {
|
|
7
|
+
var ui;
|
|
8
|
+
try {
|
|
9
|
+
ui = RED.require("node-red-dashboard")(RED);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
try {
|
|
12
|
+
ui = require("node-red-dashboard")(RED);
|
|
13
|
+
} catch (e2) {
|
|
14
|
+
RED.log.warn("noti-better: 找不到 node-red-dashboard,請確保已安裝該套件。");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 全域 CSS 注入:徹底消除 Angular Material 的強制大寫,並為深淺主題提供美化
|
|
19
|
+
var globalStyleInjected = false;
|
|
20
|
+
var betterNotificationCSS = `
|
|
21
|
+
<style id="nr-noti-better-styles">
|
|
22
|
+
/* 徹底解除對話框與通知按鈕強制大寫的限制,忠實依輸入原樣呈現 */
|
|
23
|
+
.nr-dashboard-dialog .md-button,
|
|
24
|
+
.nr-dashboard-dialog .md-confirm-button,
|
|
25
|
+
.nr-dashboard-dialog .md-cancel-button,
|
|
26
|
+
.nr-dashboard-dialog button,
|
|
27
|
+
.nr-noti-better .md-button,
|
|
28
|
+
.nr-noti-dialog .md-button,
|
|
29
|
+
.nr-preserve-case .md-button,
|
|
30
|
+
md-dialog.nr-noti-better .md-button,
|
|
31
|
+
md-dialog.nr-preserve-case .md-button,
|
|
32
|
+
md-toast.nr-noti-better .md-button {
|
|
33
|
+
text-transform: none !important;
|
|
34
|
+
font-weight: 500 !important;
|
|
35
|
+
letter-spacing: 0.3px !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* 現代化通知卡片外觀增強 */
|
|
39
|
+
md-toast.nr-noti-better .md-toast-content {
|
|
40
|
+
border-radius: 8px !important;
|
|
41
|
+
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.28) !important;
|
|
42
|
+
padding: 12px 18px !important;
|
|
43
|
+
font-size: 14px !important;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* 通知等級外框主題色彩 */
|
|
47
|
+
.nr-type-info {
|
|
48
|
+
border-left: 6px solid #2196F3 !important;
|
|
49
|
+
}
|
|
50
|
+
.nr-type-success {
|
|
51
|
+
border-left: 6px solid #4CAF50 !important;
|
|
52
|
+
}
|
|
53
|
+
.nr-type-warning {
|
|
54
|
+
border-left: 6px solid #FF9800 !important;
|
|
55
|
+
}
|
|
56
|
+
.nr-type-error {
|
|
57
|
+
border-left: 6px solid #F44336 !important;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* 倒數進度條動畫 */
|
|
61
|
+
.nr-toast-progress-wrap {
|
|
62
|
+
position: absolute;
|
|
63
|
+
left: 0;
|
|
64
|
+
bottom: 0;
|
|
65
|
+
width: 100%;
|
|
66
|
+
height: 4px;
|
|
67
|
+
background: rgba(255, 255, 255, 0.2);
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
border-bottom-left-radius: 8px;
|
|
70
|
+
border-bottom-right-radius: 8px;
|
|
71
|
+
}
|
|
72
|
+
.nr-toast-progress-bar {
|
|
73
|
+
height: 100%;
|
|
74
|
+
background: rgba(255, 255, 255, 0.85);
|
|
75
|
+
width: 100%;
|
|
76
|
+
animation: nrProgressLinear linear forwards;
|
|
77
|
+
}
|
|
78
|
+
@keyframes nrProgressLinear {
|
|
79
|
+
from { width: 100%; }
|
|
80
|
+
to { width: 0%; }
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
// 注入全域樣式至 Dashboard
|
|
86
|
+
function injectGlobalStyles() {
|
|
87
|
+
if (!ui || globalStyleInjected) { return; }
|
|
88
|
+
try {
|
|
89
|
+
ui.emit("update-value", {
|
|
90
|
+
id: "nr_noti_better_global_style",
|
|
91
|
+
msg: {
|
|
92
|
+
templateScope: "global",
|
|
93
|
+
template: betterNotificationCSS
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
globalStyleInjected = true;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
// socket 未就緒時忽略,後續 input 會再次觸發
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ToastNode(config) {
|
|
103
|
+
RED.nodes.createNode(this, config);
|
|
104
|
+
var node = this;
|
|
105
|
+
|
|
106
|
+
if (!ui) {
|
|
107
|
+
node.error("noti-better 需要依賴 node-red-dashboard,請先在 Palette 管理員安裝 node-red-dashboard。");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 解析基本與進階設定
|
|
112
|
+
if (config.hasOwnProperty("displayTime") && (config.displayTime.length > 0)) {
|
|
113
|
+
try {
|
|
114
|
+
this.displayTime = parseFloat(config.displayTime) * 1000;
|
|
115
|
+
} catch(e) {
|
|
116
|
+
this.displayTime = 3000;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
this.position = config.position || "top right";
|
|
120
|
+
this.notificationType = config.notificationType || "default";
|
|
121
|
+
this.highlight = config.highlight || "";
|
|
122
|
+
this.showIcon = config.showIcon !== undefined ? config.showIcon : true;
|
|
123
|
+
this.customIcon = config.customIcon || "";
|
|
124
|
+
this.playSound = config.playSound || false;
|
|
125
|
+
this.showCountdown = config.showCountdown !== undefined ? config.showCountdown : false;
|
|
126
|
+
this.preserveCase = config.preserveCase !== undefined ? config.preserveCase : true;
|
|
127
|
+
this.sendall = config.sendall === undefined ? true : config.sendall;
|
|
128
|
+
|
|
129
|
+
// 對話框屬性
|
|
130
|
+
this.ok = config.ok || "Ok";
|
|
131
|
+
this.cancel = config.cancel || "";
|
|
132
|
+
this.promptDefault = config.promptDefault || "";
|
|
133
|
+
this.promptPlaceholder = config.promptPlaceholder || "";
|
|
134
|
+
|
|
135
|
+
// 進階屬性
|
|
136
|
+
this.raw = config.raw || false;
|
|
137
|
+
this.className = config.className || "";
|
|
138
|
+
this.topic = config.topic || "";
|
|
139
|
+
|
|
140
|
+
// 節點初始化時嘗試注入全域樣式
|
|
141
|
+
injectGlobalStyles();
|
|
142
|
+
|
|
143
|
+
var done = ui.add({
|
|
144
|
+
node: node,
|
|
145
|
+
control: {},
|
|
146
|
+
storeFrontEndInputAsState: false,
|
|
147
|
+
forwardInputMessages: false,
|
|
148
|
+
beforeSend: function (msg) {
|
|
149
|
+
var m = msg.payload.msg;
|
|
150
|
+
m.topic = node.topic || m.topic;
|
|
151
|
+
return m;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// 預設等級顏色與圖示映射
|
|
156
|
+
var typePresets = {
|
|
157
|
+
"info": {
|
|
158
|
+
color: "#2196F3",
|
|
159
|
+
icon: "fa-info-circle",
|
|
160
|
+
className: "nr-type-info"
|
|
161
|
+
},
|
|
162
|
+
"success": {
|
|
163
|
+
color: "#4CAF50",
|
|
164
|
+
icon: "fa-check-circle",
|
|
165
|
+
className: "nr-type-success"
|
|
166
|
+
},
|
|
167
|
+
"warning": {
|
|
168
|
+
color: "#FF9800",
|
|
169
|
+
icon: "fa-exclamation-triangle",
|
|
170
|
+
className: "nr-type-warning"
|
|
171
|
+
},
|
|
172
|
+
"error": {
|
|
173
|
+
color: "#F44336",
|
|
174
|
+
icon: "fa-times-circle",
|
|
175
|
+
className: "nr-type-error"
|
|
176
|
+
},
|
|
177
|
+
"default": {
|
|
178
|
+
color: "",
|
|
179
|
+
icon: "fa-bell",
|
|
180
|
+
className: ""
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
node.on('input', function(msg) {
|
|
185
|
+
// 處理 input 時確保樣式注入
|
|
186
|
+
injectGlobalStyles();
|
|
187
|
+
|
|
188
|
+
if (node.sendall === true && !msg.socketid) {
|
|
189
|
+
delete msg.socketid;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 動態逾時秒數計算
|
|
193
|
+
var dt = node.displayTime || 3000;
|
|
194
|
+
if (msg.hasOwnProperty("timeout") && !isNaN(parseFloat(msg.timeout))) {
|
|
195
|
+
dt = parseFloat(msg.timeout) * 1000;
|
|
196
|
+
} else if (msg.hasOwnProperty("displayTime") && !isNaN(parseFloat(msg.displayTime))) {
|
|
197
|
+
dt = parseFloat(msg.displayTime) * 1000;
|
|
198
|
+
}
|
|
199
|
+
if (dt <= 0) {
|
|
200
|
+
dt = 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 動態判定通知等級
|
|
204
|
+
var currentType = (msg.notificationType || msg.status || msg.level || node.notificationType || "default").toLowerCase();
|
|
205
|
+
var preset = typePresets[currentType] || typePresets["default"];
|
|
206
|
+
|
|
207
|
+
// 邊框高亮顏色(支援節點設定與動態輸入)
|
|
208
|
+
var currentHighlight = msg.highlight || msg.border || node.highlight || preset.color;
|
|
209
|
+
|
|
210
|
+
// 圖示判定
|
|
211
|
+
var currentIcon = msg.icon || node.customIcon || (node.showIcon ? preset.icon : "");
|
|
212
|
+
if (currentIcon && currentIcon.indexOf("fa-") === -1) {
|
|
213
|
+
currentIcon = "fa-" + currentIcon;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 佈局判定
|
|
217
|
+
var currentPosition = msg.position || node.position;
|
|
218
|
+
var isDialog = (currentPosition === "dialog" || currentPosition === "prompt");
|
|
219
|
+
var isPrompt = (currentPosition === "prompt");
|
|
220
|
+
|
|
221
|
+
// 按鈕文字判定(忠實保留原始大小寫)
|
|
222
|
+
var okLabel = msg.ok !== undefined ? msg.ok : node.ok;
|
|
223
|
+
var cancelLabel = msg.cancel !== undefined ? msg.cancel : node.cancel;
|
|
224
|
+
|
|
225
|
+
// 標題判定
|
|
226
|
+
var currentTitle = msg.topic !== undefined ? msg.topic : (msg.title !== undefined ? msg.title : node.topic);
|
|
227
|
+
|
|
228
|
+
// 訊息內容判定
|
|
229
|
+
var currentMessage = msg.payload !== undefined ? msg.payload : "";
|
|
230
|
+
if (typeof currentMessage === "object") {
|
|
231
|
+
try {
|
|
232
|
+
currentMessage = JSON.stringify(currentMessage, null, 2);
|
|
233
|
+
} catch(e) {
|
|
234
|
+
currentMessage = String(currentMessage);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
var isRaw = msg.hasOwnProperty("raw") ? Boolean(msg.raw) : Boolean(node.raw);
|
|
239
|
+
|
|
240
|
+
// 音效判定
|
|
241
|
+
var playAudio = msg.hasOwnProperty("playSound") ? Boolean(msg.playSound) : Boolean(node.playSound);
|
|
242
|
+
var soundScript = "";
|
|
243
|
+
if (playAudio) {
|
|
244
|
+
// 利用瀏覽器內建 Web Audio API 合成提示聲,無須外部音訊檔
|
|
245
|
+
soundScript = `<script>(function(){try{var A=window.AudioContext||window.webkitAudioContext;if(A){var c=new A(),o=c.createOscillator(),g=c.createGain();o.type='sine';o.frequency.setValueAtTime(${currentType==='error'?330:587},c.currentTime);o.frequency.exponentialRampToValueAtTime(${currentType==='error'?220:880},c.currentTime+0.2);g.gain.setValueAtTime(0.15,c.currentTime);g.gain.exponentialRampToValueAtTime(0.01,c.currentTime+0.25);o.connect(g);g.connect(c.destination);o.start();o.stop(c.currentTime+0.25);}}catch(e){}})();</script>`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 當啟用 raw 時組合圖示、音效與倒數進度條
|
|
249
|
+
if (isRaw) {
|
|
250
|
+
var iconHtml = (node.showIcon && currentIcon) ? `<i class="fa ${currentIcon}" style="margin-right:8px; vertical-align:middle;"></i> ` : "";
|
|
251
|
+
var progressHtml = (!isDialog && node.showCountdown) ? `<div class="nr-toast-progress-wrap"><div class="nr-toast-progress-bar" style="animation-duration:${dt}ms;"></div></div>` : "";
|
|
252
|
+
currentMessage = `${soundScript}${iconHtml}${currentMessage}${progressHtml}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 組合 CSS 類別
|
|
256
|
+
var classList = ["nr-noti-better"];
|
|
257
|
+
if (node.preserveCase) {
|
|
258
|
+
classList.push("nr-preserve-case");
|
|
259
|
+
}
|
|
260
|
+
if (isDialog) {
|
|
261
|
+
classList.push("nr-noti-dialog");
|
|
262
|
+
}
|
|
263
|
+
if (preset.className) {
|
|
264
|
+
classList.push(preset.className);
|
|
265
|
+
}
|
|
266
|
+
if (node.className) {
|
|
267
|
+
classList.push(node.className);
|
|
268
|
+
}
|
|
269
|
+
if (msg.className) {
|
|
270
|
+
classList.push(msg.className);
|
|
271
|
+
}
|
|
272
|
+
var finalToastClass = classList.join(" ").trim();
|
|
273
|
+
|
|
274
|
+
// 發送至前端 socket
|
|
275
|
+
ui.emitSocket('show-toast', {
|
|
276
|
+
title: currentTitle,
|
|
277
|
+
toastClass: finalToastClass,
|
|
278
|
+
message: currentMessage,
|
|
279
|
+
highlight: currentHighlight,
|
|
280
|
+
displayTime: dt,
|
|
281
|
+
position: currentPosition,
|
|
282
|
+
id: node.id,
|
|
283
|
+
dialog: isDialog,
|
|
284
|
+
prompt: isPrompt,
|
|
285
|
+
ok: okLabel,
|
|
286
|
+
cancel: cancelLabel,
|
|
287
|
+
socketid: msg.socketid,
|
|
288
|
+
raw: isRaw,
|
|
289
|
+
msg: msg
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
node.on("close", done);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
RED.nodes.registerType("noti-better", ToastNode);
|
|
297
|
+
};
|