@aaqu/fromcubes-portal-react 0.1.0-alpha.1
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 +190 -0
- package/README.md +120 -0
- package/examples/sensor-portal-flow.json +71 -0
- package/nodes/portal-react.html +1059 -0
- package/nodes/portal-react.js +633 -0
- package/nodes/tw-candidates.js +976 -0
- package/nodes/vendor/react-19.production.min.js +55 -0
- package/package.json +49 -0
- package/scripts/bundle-react.js +31 -0
|
@@ -0,0 +1,1059 @@
|
|
|
1
|
+
<!-- ============================================================
|
|
2
|
+
Shared: Monaco loader (used by both node editors)
|
|
3
|
+
============================================================ -->
|
|
4
|
+
<script type="text/javascript">
|
|
5
|
+
(function () {
|
|
6
|
+
var PREFIX = "[FC-Monaco]";
|
|
7
|
+
|
|
8
|
+
// Shared editor options — used by both component and portal-react editors
|
|
9
|
+
window.__fcEditorOpts = {
|
|
10
|
+
theme: "vs-dark",
|
|
11
|
+
minimap: { enabled: false },
|
|
12
|
+
fontSize: 13,
|
|
13
|
+
fontFamily: "'Cascadia Code','Fira Code',Consolas,monospace",
|
|
14
|
+
fontLigatures: true,
|
|
15
|
+
scrollBeyondLastLine: false,
|
|
16
|
+
automaticLayout: true,
|
|
17
|
+
tabSize: 2,
|
|
18
|
+
padding: { top: 8, bottom: 8 },
|
|
19
|
+
quickSuggestions: { strings: true },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
window.__fcTwClasses = null;
|
|
23
|
+
var jsxSetupDone = false;
|
|
24
|
+
|
|
25
|
+
// One-time Monaco setup: compiler opts, diagnostics, extra libs, completion provider.
|
|
26
|
+
// Idempotent — safe to call multiple times, only runs setup once.
|
|
27
|
+
function ensureJsxSetup() {
|
|
28
|
+
if (jsxSetupDone) {
|
|
29
|
+
console.log(PREFIX, "ensureJsxSetup: already done, re-applying compiler+diag only");
|
|
30
|
+
// Always re-apply compiler/diag in case Node-RED reset them
|
|
31
|
+
applyCompilerAndDiag();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
jsxSetupDone = true;
|
|
35
|
+
|
|
36
|
+
var jsDef = monaco.typescript.javascriptDefaults;
|
|
37
|
+
|
|
38
|
+
// Log initial state
|
|
39
|
+
console.log(PREFIX, "INITIAL compilerOptions:", JSON.stringify(jsDef.getCompilerOptions()));
|
|
40
|
+
console.log(PREFIX, "INITIAL diagnosticsOptions:", JSON.stringify(jsDef.getDiagnosticsOptions()));
|
|
41
|
+
|
|
42
|
+
applyCompilerAndDiag();
|
|
43
|
+
|
|
44
|
+
// Global type stubs
|
|
45
|
+
var libContent = [
|
|
46
|
+
"declare var React: any;",
|
|
47
|
+
"declare var ReactDOM: any;",
|
|
48
|
+
"declare function useNodeRed(): { data: any; send: (payload: any, topic?: string) => void };",
|
|
49
|
+
].join("\n");
|
|
50
|
+
console.log(PREFIX, "addExtraLib globals.d.ts");
|
|
51
|
+
jsDef.addExtraLib(libContent, "file:///globals.d.ts");
|
|
52
|
+
|
|
53
|
+
// Tailwind className completion provider
|
|
54
|
+
// Triggers in: className="...", className={`...`}, and any string literal
|
|
55
|
+
// containing at least one known TW class (e.g. "bg-red-500 text-white")
|
|
56
|
+
console.log(PREFIX, "registering Tailwind completion provider");
|
|
57
|
+
monaco.languages.registerCompletionItemProvider("javascript", {
|
|
58
|
+
triggerCharacters: ['"', "'", "`", " "],
|
|
59
|
+
provideCompletionItems: function (model, position) {
|
|
60
|
+
var line = model.getLineContent(position.lineNumber);
|
|
61
|
+
var before = line.substring(0, position.column - 1);
|
|
62
|
+
|
|
63
|
+
// 1) className="..." or className={`...`}
|
|
64
|
+
var isClassName =
|
|
65
|
+
before.match(/className\s*=\s*["'][^"']*$/) ||
|
|
66
|
+
before.match(/className\s*=\s*\{`[^`]*$/);
|
|
67
|
+
|
|
68
|
+
// 2) Any open string literal: "..., '..., or `...
|
|
69
|
+
var inString = !isClassName && before.match(/["'`][^"'`]*$/);
|
|
70
|
+
|
|
71
|
+
if (!isClassName && !inString) return { suggestions: [] };
|
|
72
|
+
|
|
73
|
+
// Extract text inside the string
|
|
74
|
+
var raw = (isClassName || inString)[0];
|
|
75
|
+
var inside;
|
|
76
|
+
if (isClassName) {
|
|
77
|
+
inside = raw.replace(/^className\s*=\s*(?:\{`|["'])/, "");
|
|
78
|
+
} else {
|
|
79
|
+
inside = raw.substring(1); // skip opening quote
|
|
80
|
+
}
|
|
81
|
+
inside = inside.replace(/\$\{[^}]*\}/g, " ");
|
|
82
|
+
var parts = inside.split(/\s+/);
|
|
83
|
+
var word = parts[parts.length - 1] || "";
|
|
84
|
+
|
|
85
|
+
// For non-className strings, only activate if at least one existing
|
|
86
|
+
// token in the string looks like a TW class (avoid noise in random strings)
|
|
87
|
+
if (!isClassName && parts.length > 0) {
|
|
88
|
+
var classes = window.__fcTwClasses || [];
|
|
89
|
+
var twSet = window.__fcTwSet;
|
|
90
|
+
if (!twSet && classes.length) {
|
|
91
|
+
twSet = new Set(classes);
|
|
92
|
+
window.__fcTwSet = twSet;
|
|
93
|
+
console.log(PREFIX, "TW: built lookup Set, size=" + twSet.size);
|
|
94
|
+
}
|
|
95
|
+
if (twSet) {
|
|
96
|
+
var hasTwToken = false;
|
|
97
|
+
for (var p = 0; p < parts.length; p++) {
|
|
98
|
+
if (parts[p] && twSet.has(parts[p])) {
|
|
99
|
+
hasTwToken = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// If typing the very first word, accept if it looks like a prefix
|
|
104
|
+
if (!hasTwToken && parts.length === 1 && word.length >= 2) {
|
|
105
|
+
for (var j = 0; j < classes.length; j++) {
|
|
106
|
+
if (classes[j].indexOf(word) === 0) {
|
|
107
|
+
hasTwToken = true;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!hasTwToken) return { suggestions: [] };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
var range = {
|
|
117
|
+
startLineNumber: position.lineNumber,
|
|
118
|
+
endLineNumber: position.lineNumber,
|
|
119
|
+
startColumn: position.column - word.length,
|
|
120
|
+
endColumn: position.column,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
var classes = window.__fcTwClasses || [];
|
|
124
|
+
var suggestions = [];
|
|
125
|
+
for (var i = 0; i < classes.length; i++) {
|
|
126
|
+
var cls = classes[i];
|
|
127
|
+
if (!word.length || cls.indexOf(word) === 0) {
|
|
128
|
+
suggestions.push({
|
|
129
|
+
label: cls,
|
|
130
|
+
kind: monaco.languages.CompletionItemKind.Value,
|
|
131
|
+
insertText: cls,
|
|
132
|
+
range: range,
|
|
133
|
+
sortText: cls.padStart(60, "0"),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
console.log(PREFIX, "TW completion: ctx=" + (isClassName ? "className" : "string") + " word='" + word + "', matched=" + suggestions.length + "/" + classes.length);
|
|
138
|
+
return { suggestions: suggestions };
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// JSX/HTML tag completion — type tag name, Tab → <tag>|</tag>
|
|
143
|
+
console.log(PREFIX, "registering JSX tag completion provider");
|
|
144
|
+
var HTML_TAGS = [
|
|
145
|
+
"div","span","p","a","button","input","img","form","label",
|
|
146
|
+
"h1","h2","h3","h4","h5","h6",
|
|
147
|
+
"ul","ol","li","dl","dt","dd",
|
|
148
|
+
"table","thead","tbody","tfoot","tr","th","td",
|
|
149
|
+
"section","article","aside","header","footer","nav","main",
|
|
150
|
+
"strong","em","b","i","u","s","small","mark","code","pre","blockquote",
|
|
151
|
+
"br","hr","wbr",
|
|
152
|
+
"select","option","optgroup","textarea",
|
|
153
|
+
"fieldset","legend","details","summary","dialog",
|
|
154
|
+
"canvas","video","audio","source","picture","figure","figcaption",
|
|
155
|
+
"svg","path","circle","rect","line","g","defs","use","text",
|
|
156
|
+
];
|
|
157
|
+
var VOID_TAGS = new Set(["br","hr","img","input","wbr","source"]);
|
|
158
|
+
|
|
159
|
+
monaco.languages.registerCompletionItemProvider("javascript", {
|
|
160
|
+
triggerCharacters: ["<"],
|
|
161
|
+
provideCompletionItems: function (model, position) {
|
|
162
|
+
var line = model.getLineContent(position.lineNumber);
|
|
163
|
+
var before = line.substring(0, position.column - 1);
|
|
164
|
+
|
|
165
|
+
// After "<" — suggest tags
|
|
166
|
+
var afterBracket = before.match(/<([a-zA-Z]*)$/);
|
|
167
|
+
// Bare word at line start or after whitespace/{ — Emmet-like (lower or upper)
|
|
168
|
+
var bareWord = !afterBracket && before.match(/(?:^|[\s{(,])([a-zA-Z][a-zA-Z0-9]*)$/);
|
|
169
|
+
|
|
170
|
+
if (!afterBracket && !bareWord) return { suggestions: [] };
|
|
171
|
+
|
|
172
|
+
var typed = (afterBracket ? afterBracket[1] : bareWord[1]) || "";
|
|
173
|
+
var replaceStart = position.column - typed.length - (afterBracket ? 1 : 0);
|
|
174
|
+
|
|
175
|
+
var range = {
|
|
176
|
+
startLineNumber: position.lineNumber,
|
|
177
|
+
endLineNumber: position.lineNumber,
|
|
178
|
+
startColumn: replaceStart,
|
|
179
|
+
endColumn: position.column,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
var suggestions = [];
|
|
183
|
+
var seen = new Set();
|
|
184
|
+
|
|
185
|
+
// HTML tags
|
|
186
|
+
for (var i = 0; i < HTML_TAGS.length; i++) {
|
|
187
|
+
var tag = HTML_TAGS[i];
|
|
188
|
+
if (typed && tag.indexOf(typed) !== 0) continue;
|
|
189
|
+
seen.add(tag);
|
|
190
|
+
if (VOID_TAGS.has(tag)) {
|
|
191
|
+
suggestions.push({
|
|
192
|
+
label: tag,
|
|
193
|
+
kind: monaco.languages.CompletionItemKind.Property,
|
|
194
|
+
insertText: "<" + tag + " $0/>",
|
|
195
|
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
196
|
+
range: range,
|
|
197
|
+
detail: "<" + tag + " />",
|
|
198
|
+
sortText: "0" + tag,
|
|
199
|
+
});
|
|
200
|
+
} else {
|
|
201
|
+
suggestions.push({
|
|
202
|
+
label: tag,
|
|
203
|
+
kind: monaco.languages.CompletionItemKind.Property,
|
|
204
|
+
insertText: "<" + tag + ">$0</" + tag + ">",
|
|
205
|
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
206
|
+
range: range,
|
|
207
|
+
detail: "<" + tag + ">…</" + tag + ">",
|
|
208
|
+
sortText: "0" + tag + "a",
|
|
209
|
+
});
|
|
210
|
+
suggestions.push({
|
|
211
|
+
label: tag,
|
|
212
|
+
kind: monaco.languages.CompletionItemKind.Property,
|
|
213
|
+
insertText: "<" + tag + " $0/>",
|
|
214
|
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
215
|
+
range: range,
|
|
216
|
+
detail: "<" + tag + " />",
|
|
217
|
+
sortText: "0" + tag + "b",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Custom components from registry (PascalCase)
|
|
223
|
+
var upperMatch = afterBracket && before.match(/<([A-Z][a-zA-Z0-9]*)$/);
|
|
224
|
+
var bareUpper = !afterBracket && before.match(/(?:^|[\s{(,])([A-Z][a-zA-Z0-9]*)$/);
|
|
225
|
+
var compTyped = upperMatch ? upperMatch[1] : (bareUpper ? bareUpper[1] : "");
|
|
226
|
+
|
|
227
|
+
if (compTyped || (!typed && afterBracket)) {
|
|
228
|
+
// Fetch component names from registry (cached on window)
|
|
229
|
+
var reg = window.__fcComponentNames || [];
|
|
230
|
+
for (var c = 0; c < reg.length; c++) {
|
|
231
|
+
var name = reg[c];
|
|
232
|
+
if (seen.has(name)) continue;
|
|
233
|
+
if (compTyped && name.indexOf(compTyped) !== 0) continue;
|
|
234
|
+
if (!compTyped && typed && name.toLowerCase().indexOf(typed) !== 0) continue;
|
|
235
|
+
seen.add(name);
|
|
236
|
+
suggestions.push({
|
|
237
|
+
label: name,
|
|
238
|
+
kind: monaco.languages.CompletionItemKind.Class,
|
|
239
|
+
insertText: "<" + name + ">$0</" + name + ">",
|
|
240
|
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
241
|
+
range: range,
|
|
242
|
+
detail: "<" + name + ">…</" + name + ">",
|
|
243
|
+
sortText: "0" + name + "a",
|
|
244
|
+
});
|
|
245
|
+
suggestions.push({
|
|
246
|
+
label: name,
|
|
247
|
+
kind: monaco.languages.CompletionItemKind.Class,
|
|
248
|
+
insertText: "<" + name + " $0/>",
|
|
249
|
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
250
|
+
range: range,
|
|
251
|
+
detail: "<" + name + " />",
|
|
252
|
+
sortText: "0" + name + "b",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
// Any PascalCase word not in registry — offer generic JSX wrap
|
|
256
|
+
if (compTyped && !seen.has(compTyped)) {
|
|
257
|
+
suggestions.push({
|
|
258
|
+
label: compTyped,
|
|
259
|
+
kind: monaco.languages.CompletionItemKind.Class,
|
|
260
|
+
insertText: "<" + compTyped + ">$0</" + compTyped + ">",
|
|
261
|
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
262
|
+
range: range,
|
|
263
|
+
detail: "<" + compTyped + ">…</" + compTyped + ">",
|
|
264
|
+
sortText: "0" + compTyped + "a",
|
|
265
|
+
});
|
|
266
|
+
suggestions.push({
|
|
267
|
+
label: compTyped,
|
|
268
|
+
kind: monaco.languages.CompletionItemKind.Class,
|
|
269
|
+
insertText: "<" + compTyped + " $0/>",
|
|
270
|
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
271
|
+
range: range,
|
|
272
|
+
detail: "<" + compTyped + " />",
|
|
273
|
+
sortText: "0" + compTyped + "b",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(PREFIX, "JSX tag completion: typed='" + typed + "', suggestions=" + suggestions.length);
|
|
279
|
+
return { suggestions: suggestions };
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Collapse <tag>|</tag> → <tag /> when "/" typed inside empty tag pair
|
|
284
|
+
console.log(PREFIX, "registering JSX self-close collapse handler");
|
|
285
|
+
window.__fcAttachSelfClose = function (editor) {
|
|
286
|
+
editor.onDidChangeModelContent(function (e) {
|
|
287
|
+
if (e.changes.length !== 1) return;
|
|
288
|
+
var ch = e.changes[0];
|
|
289
|
+
if (ch.text !== "/") return;
|
|
290
|
+
var model = editor.getModel();
|
|
291
|
+
if (!model) return;
|
|
292
|
+
var lineNum = ch.range.startLineNumber;
|
|
293
|
+
var line = model.getLineContent(lineNum);
|
|
294
|
+
// Pattern 1: <tag>/</tag> (cursor was between > and </)
|
|
295
|
+
// Pattern 2: <tag/></tag> (cursor was before > in opening tag)
|
|
296
|
+
var m = line.match(/^(.*)<([a-zA-Z][a-zA-Z0-9]*)>\/<\/\2>(.*)$/);
|
|
297
|
+
var matchStr, tag, prefix;
|
|
298
|
+
if (m) {
|
|
299
|
+
prefix = m[1]; tag = m[2];
|
|
300
|
+
matchStr = "<" + tag + ">/</" + tag + ">";
|
|
301
|
+
} else {
|
|
302
|
+
m = line.match(/^(.*)<([a-zA-Z][a-zA-Z0-9]*)\/><\/\2>(.*)$/);
|
|
303
|
+
if (!m) return;
|
|
304
|
+
prefix = m[1]; tag = m[2];
|
|
305
|
+
matchStr = "<" + tag + "/></" + tag + ">";
|
|
306
|
+
}
|
|
307
|
+
var startCol = prefix.length + 1;
|
|
308
|
+
var endCol = startCol + matchStr.length;
|
|
309
|
+
var replacement = "<" + tag + " />";
|
|
310
|
+
var cursorCol = startCol + replacement.length - 2;
|
|
311
|
+
setTimeout(function () {
|
|
312
|
+
editor.executeEdits("self-close-collapse", [{
|
|
313
|
+
range: {
|
|
314
|
+
startLineNumber: lineNum,
|
|
315
|
+
endLineNumber: lineNum,
|
|
316
|
+
startColumn: startCol,
|
|
317
|
+
endColumn: endCol,
|
|
318
|
+
},
|
|
319
|
+
text: replacement,
|
|
320
|
+
}]);
|
|
321
|
+
editor.setPosition({ lineNumber: lineNum, column: cursorCol });
|
|
322
|
+
}, 0);
|
|
323
|
+
});
|
|
324
|
+
console.log(PREFIX, "self-close collapse attached to editor");
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Fetch component names for tag completion
|
|
328
|
+
function refreshComponentNames() {
|
|
329
|
+
$.getJSON("portal-react/registry", function (reg) {
|
|
330
|
+
window.__fcComponentNames = Object.keys(reg);
|
|
331
|
+
console.log(PREFIX, "component names loaded:", window.__fcComponentNames);
|
|
332
|
+
}).fail(function () {
|
|
333
|
+
window.__fcComponentNames = [];
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
refreshComponentNames();
|
|
337
|
+
|
|
338
|
+
// Marker listener for debugging
|
|
339
|
+
monaco.editor.onDidChangeMarkers(function (uris) {
|
|
340
|
+
uris.forEach(function (uri) {
|
|
341
|
+
var markers = monaco.editor.getModelMarkers({ resource: uri });
|
|
342
|
+
if (markers.length > 0) {
|
|
343
|
+
console.group(PREFIX + " MARKERS on " + uri.toString());
|
|
344
|
+
markers.forEach(function (m) {
|
|
345
|
+
console.log(
|
|
346
|
+
" code=" + (m.code || "?") +
|
|
347
|
+
" severity=" + m.severity +
|
|
348
|
+
" source=" + (m.source || "?") +
|
|
349
|
+
" msg=" + m.message +
|
|
350
|
+
" [L" + m.startLineNumber + ":" + m.startColumn + "]"
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
console.groupEnd();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
console.log(PREFIX, "one-time setup complete");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function applyCompilerAndDiag() {
|
|
362
|
+
var jsDef = monaco.typescript.javascriptDefaults;
|
|
363
|
+
var compilerOpts = {
|
|
364
|
+
jsx: monaco.typescript.JsxEmit.React,
|
|
365
|
+
target: monaco.typescript.ScriptTarget.ESNext,
|
|
366
|
+
module: monaco.typescript.ModuleKind.ESNext,
|
|
367
|
+
allowNonTsExtensions: true,
|
|
368
|
+
allowJs: true,
|
|
369
|
+
};
|
|
370
|
+
console.log(PREFIX, "setCompilerOptions:", JSON.stringify(compilerOpts));
|
|
371
|
+
jsDef.setCompilerOptions(compilerOpts);
|
|
372
|
+
|
|
373
|
+
var diagOpts = {
|
|
374
|
+
noSemanticValidation: true,
|
|
375
|
+
noSyntaxValidation: false,
|
|
376
|
+
noSuggestionDiagnostics: true,
|
|
377
|
+
diagnosticCodesToIgnore: [17004],
|
|
378
|
+
};
|
|
379
|
+
console.log(PREFIX, "setDiagnosticsOptions:", JSON.stringify(diagOpts));
|
|
380
|
+
jsDef.setDiagnosticsOptions(diagOpts);
|
|
381
|
+
|
|
382
|
+
console.log(PREFIX, "readback compilerOptions:", JSON.stringify(jsDef.getCompilerOptions()));
|
|
383
|
+
console.log(PREFIX, "readback diagnosticsOptions:", JSON.stringify(jsDef.getDiagnosticsOptions()));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Exported for oneditprepare to call before model creation
|
|
387
|
+
window.__fcApplyJsxDefaults = function () {
|
|
388
|
+
ensureJsxSetup();
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
window.__fcLoadMonaco = function (cb) {
|
|
392
|
+
console.log(PREFIX, "loadMonaco called, monaco exists:", !!window.monaco);
|
|
393
|
+
if (window.monaco) {
|
|
394
|
+
console.log(PREFIX, "Monaco already loaded (by Node-RED?), running setup inline");
|
|
395
|
+
ensureJsxSetup();
|
|
396
|
+
cb();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
var s = document.createElement("script");
|
|
400
|
+
s.src = "portal-react/vs/loader.js";
|
|
401
|
+
s.onload = function () {
|
|
402
|
+
console.log(PREFIX, "loader.js loaded, configuring require paths");
|
|
403
|
+
require.config({ paths: { vs: "portal-react/vs" } });
|
|
404
|
+
require(["vs/editor/editor.main"], function () {
|
|
405
|
+
console.log(PREFIX, "editor.main loaded");
|
|
406
|
+
ensureJsxSetup();
|
|
407
|
+
console.log(PREFIX, "calling cb()");
|
|
408
|
+
cb();
|
|
409
|
+
});
|
|
410
|
+
};
|
|
411
|
+
s.onerror = function () {
|
|
412
|
+
console.error(PREFIX, "FAILED to load loader.js");
|
|
413
|
+
cb(true);
|
|
414
|
+
};
|
|
415
|
+
document.head.appendChild(s);
|
|
416
|
+
};
|
|
417
|
+
})();
|
|
418
|
+
</script>
|
|
419
|
+
|
|
420
|
+
<!-- ============================================================
|
|
421
|
+
fc-portal-component – canvas node (shared component)
|
|
422
|
+
============================================================ -->
|
|
423
|
+
<script type="text/html" data-template-name="fc-portal-component">
|
|
424
|
+
<div class="form-row">
|
|
425
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
426
|
+
<input type="text" id="node-input-name" placeholder="Label on canvas" />
|
|
427
|
+
</div>
|
|
428
|
+
<div class="form-row">
|
|
429
|
+
<label for="node-input-compName"><i class="fa fa-cube"></i> JSX Tag</label>
|
|
430
|
+
<input type="text" id="node-input-compName" placeholder="MyComponent" />
|
|
431
|
+
</div>
|
|
432
|
+
<div class="form-row" style="display:flex;gap:8px;">
|
|
433
|
+
<div style="flex:1">
|
|
434
|
+
<label><i class="fa fa-sign-in"></i> Input fields</label>
|
|
435
|
+
<input
|
|
436
|
+
type="text"
|
|
437
|
+
id="node-input-compInputs"
|
|
438
|
+
placeholder="payload,topic"
|
|
439
|
+
style="width:100%"
|
|
440
|
+
/>
|
|
441
|
+
</div>
|
|
442
|
+
<div style="flex:1">
|
|
443
|
+
<label><i class="fa fa-sign-out"></i> Output fields</label>
|
|
444
|
+
<input
|
|
445
|
+
type="text"
|
|
446
|
+
id="node-input-compOutputs"
|
|
447
|
+
placeholder="payload,topic"
|
|
448
|
+
style="width:100%"
|
|
449
|
+
/>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="form-row" style="margin-bottom:4px;">
|
|
453
|
+
<label><i class="fa fa-code"></i> JSX Code</label>
|
|
454
|
+
</div>
|
|
455
|
+
<div class="form-row node-text-editor-row">
|
|
456
|
+
<div
|
|
457
|
+
id="fcc-editor-wrap"
|
|
458
|
+
style="width:100%;height:350px;border:1px solid var(--red-ui-form-input-border-color,#555);border-radius:4px;overflow:hidden;position:relative;"
|
|
459
|
+
>
|
|
460
|
+
<div id="fcc-monaco" style="width:100%;height:100%;"></div>
|
|
461
|
+
<textarea
|
|
462
|
+
id="fcc-fallback"
|
|
463
|
+
style="display:none;width:100%;height:100%;font-family:'Cascadia Code',Consolas,monospace;font-size:13px;background:#1e1e1e;color:#d4d4d4;border:none;padding:12px;resize:none;"
|
|
464
|
+
></textarea>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
<input type="hidden" id="node-input-compCode" />
|
|
468
|
+
</script>
|
|
469
|
+
|
|
470
|
+
<script type="text/javascript">
|
|
471
|
+
(function () {
|
|
472
|
+
var compEditorInstance = null;
|
|
473
|
+
|
|
474
|
+
var COMP_STARTER = [
|
|
475
|
+
"function StatusCard({ label, value, unit }) {",
|
|
476
|
+
" return (",
|
|
477
|
+
' <div className="rounded-2xl bg-zinc-900 border border-zinc-800 p-6">',
|
|
478
|
+
' <div className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{label}</div>',
|
|
479
|
+
' <div className="text-3xl font-bold text-white">',
|
|
480
|
+
' {value}<span className="text-lg text-zinc-400 ml-1">{unit}</span>',
|
|
481
|
+
" </div>",
|
|
482
|
+
" </div>",
|
|
483
|
+
" );",
|
|
484
|
+
"}",
|
|
485
|
+
].join("\n");
|
|
486
|
+
|
|
487
|
+
RED.nodes.registerType("fc-portal-component", {
|
|
488
|
+
category: "fromcubes",
|
|
489
|
+
color: "#a8d8ea",
|
|
490
|
+
defaults: {
|
|
491
|
+
name: { value: "" },
|
|
492
|
+
compName: { value: "StatusCard", required: true },
|
|
493
|
+
compCode: { value: COMP_STARTER },
|
|
494
|
+
compInputs: { value: "label,value,unit" },
|
|
495
|
+
compOutputs: { value: "" },
|
|
496
|
+
},
|
|
497
|
+
inputs: 0,
|
|
498
|
+
outputs: 0,
|
|
499
|
+
icon: "font-awesome/fa-cube",
|
|
500
|
+
paletteLabel: "component",
|
|
501
|
+
label: function () {
|
|
502
|
+
return this.name || this.compName || "component";
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
oneditprepare: function () {
|
|
506
|
+
var node = this;
|
|
507
|
+
var code = node.compCode || COMP_STARTER;
|
|
508
|
+
console.log("[FC-Monaco] COMP oneditprepare, node.id=" + node.id);
|
|
509
|
+
|
|
510
|
+
if (!window.__fcTwClasses) {
|
|
511
|
+
console.log("[FC-Monaco] loading tw-classes...");
|
|
512
|
+
$.getJSON("portal-react/tw-classes", function (classes) {
|
|
513
|
+
console.log("[FC-Monaco] tw-classes loaded, count=" + classes.length);
|
|
514
|
+
window.__fcTwClasses = classes;
|
|
515
|
+
}).fail(function (xhr) {
|
|
516
|
+
console.error("[FC-Monaco] tw-classes FAILED:", xhr.status, xhr.statusText);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
window.__fcLoadMonaco(function (failed) {
|
|
521
|
+
if (failed) {
|
|
522
|
+
console.error("[FC-Monaco] COMP: Monaco load failed, using fallback textarea");
|
|
523
|
+
$("#fcc-monaco").hide();
|
|
524
|
+
$("#fcc-fallback").show().val(code);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
console.log("[FC-Monaco] COMP: applying JSX defaults before model creation");
|
|
529
|
+
window.__fcApplyJsxDefaults();
|
|
530
|
+
|
|
531
|
+
var compUri = monaco.Uri.parse("file:///fc-comp-" + node.id + ".jsx");
|
|
532
|
+
console.log("[FC-Monaco] COMP: model URI=" + compUri.toString());
|
|
533
|
+
var existingModel = monaco.editor.getModel(compUri);
|
|
534
|
+
if (existingModel) {
|
|
535
|
+
console.log("[FC-Monaco] COMP: disposing existing model");
|
|
536
|
+
existingModel.dispose();
|
|
537
|
+
}
|
|
538
|
+
var compModel = monaco.editor.createModel(code, "javascript", compUri);
|
|
539
|
+
console.log("[FC-Monaco] COMP: model created, language=" + compModel.getLanguageId());
|
|
540
|
+
|
|
541
|
+
compEditorInstance = monaco.editor.create(
|
|
542
|
+
document.getElementById("fcc-monaco"),
|
|
543
|
+
Object.assign({ model: compModel }, window.__fcEditorOpts),
|
|
544
|
+
);
|
|
545
|
+
console.log("[FC-Monaco] COMP: editor created");
|
|
546
|
+
|
|
547
|
+
// Log markers after a short delay (diagnostics are async)
|
|
548
|
+
setTimeout(function () {
|
|
549
|
+
var markers = monaco.editor.getModelMarkers({ resource: compUri });
|
|
550
|
+
console.log("[FC-Monaco] COMP: markers after 500ms, count=" + markers.length);
|
|
551
|
+
markers.forEach(function (m) {
|
|
552
|
+
console.log("[FC-Monaco] COMP marker: code=" + m.code + " msg=" + m.message);
|
|
553
|
+
});
|
|
554
|
+
// Also log current compiler options state
|
|
555
|
+
console.log("[FC-Monaco] COMP: current compilerOptions:", JSON.stringify(monaco.typescript.javascriptDefaults.getCompilerOptions()));
|
|
556
|
+
console.log("[FC-Monaco] COMP: current diagnosticsOptions:", JSON.stringify(monaco.typescript.javascriptDefaults.getDiagnosticsOptions()));
|
|
557
|
+
}, 500);
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
oneditsave: function () {
|
|
562
|
+
console.log("[FC-Monaco] COMP oneditsave");
|
|
563
|
+
var code = compEditorInstance
|
|
564
|
+
? compEditorInstance.getValue()
|
|
565
|
+
: $("#fcc-fallback").val();
|
|
566
|
+
$("#node-input-compCode").val(code);
|
|
567
|
+
this.compCode = code;
|
|
568
|
+
if (compEditorInstance) {
|
|
569
|
+
compEditorInstance.getModel().dispose();
|
|
570
|
+
compEditorInstance.dispose();
|
|
571
|
+
compEditorInstance = null;
|
|
572
|
+
console.log("[FC-Monaco] COMP: editor + model disposed");
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
oneditcancel: function () {
|
|
577
|
+
console.log("[FC-Monaco] COMP oneditcancel");
|
|
578
|
+
if (compEditorInstance) {
|
|
579
|
+
compEditorInstance.getModel().dispose();
|
|
580
|
+
compEditorInstance.dispose();
|
|
581
|
+
compEditorInstance = null;
|
|
582
|
+
console.log("[FC-Monaco] COMP: editor + model disposed");
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
oneditresize: function (size) {
|
|
587
|
+
var h = size.height - 180;
|
|
588
|
+
if (h < 150) h = 150;
|
|
589
|
+
$("#fcc-editor-wrap").css("height", h + "px");
|
|
590
|
+
if (compEditorInstance) compEditorInstance.layout();
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
})();
|
|
594
|
+
</script>
|
|
595
|
+
|
|
596
|
+
<script type="text/html" data-help-name="fc-portal-component">
|
|
597
|
+
<p>
|
|
598
|
+
Defines a shared JSX component available to all
|
|
599
|
+
<strong>portal-react</strong> nodes.
|
|
600
|
+
</p>
|
|
601
|
+
<h3>Properties</h3>
|
|
602
|
+
<dl>
|
|
603
|
+
<dt>JSX Tag</dt>
|
|
604
|
+
<dd>
|
|
605
|
+
The name used as a JSX tag in portal-react pages (e.g.
|
|
606
|
+
<code><MyComponent /></code>).
|
|
607
|
+
</dd>
|
|
608
|
+
<dt>Input fields</dt>
|
|
609
|
+
<dd>Comma-separated list of expected input props (documentation only).</dd>
|
|
610
|
+
<dt>Output fields</dt>
|
|
611
|
+
<dd>Comma-separated list of output props (documentation only).</dd>
|
|
612
|
+
<dt>JSX Code</dt>
|
|
613
|
+
<dd>
|
|
614
|
+
The component function. Use <code>useNodeRed()</code> for data binding.
|
|
615
|
+
</dd>
|
|
616
|
+
</dl>
|
|
617
|
+
<h3>Usage</h3>
|
|
618
|
+
<p>
|
|
619
|
+
Place this node on the canvas, set the JSX tag name and code, then deploy.
|
|
620
|
+
All <strong>portal-react</strong> nodes will automatically have access to
|
|
621
|
+
this component.
|
|
622
|
+
</p>
|
|
623
|
+
</script>
|
|
624
|
+
|
|
625
|
+
<!-- ============================================================
|
|
626
|
+
portal-react – main node
|
|
627
|
+
============================================================ -->
|
|
628
|
+
<script type="text/html" data-template-name="portal-react">
|
|
629
|
+
<!-- Tabs -->
|
|
630
|
+
<div class="form-row">
|
|
631
|
+
<ul id="fc-tabs" style="min-width:600px;margin-bottom:0;"></ul>
|
|
632
|
+
</div>
|
|
633
|
+
<div id="fc-tabs-content">
|
|
634
|
+
<!-- ── Tab: Properties ── -->
|
|
635
|
+
<div id="fc-tab-props" class="fc-tab-pane" style="display:none;">
|
|
636
|
+
<div class="form-row">
|
|
637
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
638
|
+
<input type="text" id="node-input-name" placeholder="My Portal" />
|
|
639
|
+
</div>
|
|
640
|
+
<div class="form-row">
|
|
641
|
+
<label for="node-input-endpoint"
|
|
642
|
+
><i class="fa fa-globe"></i> Endpoint</label
|
|
643
|
+
>
|
|
644
|
+
<input type="text" id="node-input-endpoint" placeholder="/portal" />
|
|
645
|
+
<div
|
|
646
|
+
style="font-size:11px;opacity:.5;margin-top:2px;margin-left:105px;"
|
|
647
|
+
id="fc-url-hint"
|
|
648
|
+
></div>
|
|
649
|
+
</div>
|
|
650
|
+
<div class="form-row">
|
|
651
|
+
<label for="node-input-pageTitle"
|
|
652
|
+
><i class="fa fa-file-text-o"></i> Title</label
|
|
653
|
+
>
|
|
654
|
+
<input type="text" id="node-input-pageTitle" placeholder="Portal" />
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
|
|
658
|
+
<!-- ── Tab: JSX ── -->
|
|
659
|
+
<div id="fc-tab-jsx" class="fc-tab-pane">
|
|
660
|
+
<div class="form-row" style="margin-bottom:0;">
|
|
661
|
+
<div style="font-size:11px;opacity:.6;margin-bottom:4px;">
|
|
662
|
+
<code>useNodeRed()</code> →
|
|
663
|
+
<code>{ data, send }</code> | Components from
|
|
664
|
+
<code>fc-portal-component</code> nodes auto-imported |
|
|
665
|
+
Must export <code><App /></code> |
|
|
666
|
+
<strong>Transpiled server-side at deploy</strong>
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
<div class="form-row node-text-editor-row">
|
|
670
|
+
<div
|
|
671
|
+
id="fc-editor-wrap"
|
|
672
|
+
style="width:100%;height:420px;border:1px solid var(--red-ui-form-input-border-color,#555);border-radius:4px;overflow:hidden;position:relative;"
|
|
673
|
+
>
|
|
674
|
+
<div id="fc-monaco" style="width:100%;height:100%;"></div>
|
|
675
|
+
<textarea
|
|
676
|
+
id="fc-fallback"
|
|
677
|
+
style="display:none;width:100%;height:100%;font-family:'Cascadia Code',Consolas,monospace;font-size:13px;background:#1e1e1e;color:#d4d4d4;border:none;padding:12px;resize:none;"
|
|
678
|
+
></textarea>
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
<div class="form-row" style="display:flex;gap:8px;margin-top:4px;">
|
|
682
|
+
<button type="button" class="red-ui-button" id="fc-btn-preview">
|
|
683
|
+
<i class="fa fa-eye"></i> Preview
|
|
684
|
+
</button>
|
|
685
|
+
<button type="button" class="red-ui-button" id="fc-btn-components">
|
|
686
|
+
<i class="fa fa-cube"></i> Components
|
|
687
|
+
</button>
|
|
688
|
+
<button type="button" class="red-ui-button" id="fc-btn-starter">
|
|
689
|
+
<i class="fa fa-magic"></i> Starter
|
|
690
|
+
</button>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
|
|
694
|
+
<!-- ── Tab: Head HTML ── -->
|
|
695
|
+
<div id="fc-tab-head" class="fc-tab-pane" style="display:none;">
|
|
696
|
+
<div class="form-row">
|
|
697
|
+
<div style="font-size:11px;opacity:.6;margin-bottom:6px;">
|
|
698
|
+
Extra tags injected into <code><head></code>. CDN links, fonts,
|
|
699
|
+
stylesheets, meta tags.
|
|
700
|
+
</div>
|
|
701
|
+
<div
|
|
702
|
+
id="fc-head-wrap"
|
|
703
|
+
style="width:100%;height:420px;border:1px solid var(--red-ui-form-input-border-color,#555);border-radius:4px;overflow:hidden;position:relative;"
|
|
704
|
+
>
|
|
705
|
+
<div id="fc-head-monaco" style="width:100%;height:100%;"></div>
|
|
706
|
+
<textarea
|
|
707
|
+
id="fc-head-fallback"
|
|
708
|
+
style="display:none;width:100%;height:100%;font-family:'Cascadia Code',Consolas,monospace;font-size:13px;background:#1e1e1e;color:#d4d4d4;border:none;padding:12px;resize:none;"
|
|
709
|
+
placeholder='<link rel="stylesheet" href="...">'
|
|
710
|
+
></textarea>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
|
|
716
|
+
<input type="hidden" id="node-input-componentCode" />
|
|
717
|
+
<input type="hidden" id="node-input-customHead" />
|
|
718
|
+
</script>
|
|
719
|
+
|
|
720
|
+
<script type="text/javascript">
|
|
721
|
+
(function () {
|
|
722
|
+
var editorInstance = null;
|
|
723
|
+
var headEditorInstance = null;
|
|
724
|
+
|
|
725
|
+
var STARTER = [
|
|
726
|
+
"// useNodeRed() \u2192 { data, send }",
|
|
727
|
+
"// data = last msg.payload from input wire",
|
|
728
|
+
"// send(payload, topic?) = push msg to output wire",
|
|
729
|
+
"// Components from fc-portal-component nodes are available by name.",
|
|
730
|
+
"",
|
|
731
|
+
"function App() {",
|
|
732
|
+
" const { data, send } = useNodeRed();",
|
|
733
|
+
" const d = data || {};",
|
|
734
|
+
"",
|
|
735
|
+
" return (",
|
|
736
|
+
' <div className="min-h-screen bg-zinc-950 p-8">',
|
|
737
|
+
' <h1 className="text-2xl font-light text-cyan-400 mb-6">Portal</h1>',
|
|
738
|
+
' <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">',
|
|
739
|
+
' <StatusCard label="Value" value={d.value ?? "—"} unit="" />',
|
|
740
|
+
" </div>",
|
|
741
|
+
" <button",
|
|
742
|
+
' className="mt-6 px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-500 transition-colors"',
|
|
743
|
+
" onClick={() => send({ clicked: true })}",
|
|
744
|
+
" >",
|
|
745
|
+
" Send to Node-RED",
|
|
746
|
+
" </button>",
|
|
747
|
+
" </div>",
|
|
748
|
+
" );",
|
|
749
|
+
"}",
|
|
750
|
+
].join("\n");
|
|
751
|
+
|
|
752
|
+
RED.nodes.registerType("portal-react", {
|
|
753
|
+
category: "fromcubes",
|
|
754
|
+
color: "#61dafb",
|
|
755
|
+
defaults: {
|
|
756
|
+
name: { value: "" },
|
|
757
|
+
endpoint: { value: "/portal", required: true },
|
|
758
|
+
pageTitle: { value: "Portal" },
|
|
759
|
+
componentCode: { value: STARTER },
|
|
760
|
+
customHead: { value: "" },
|
|
761
|
+
},
|
|
762
|
+
inputs: 1,
|
|
763
|
+
outputs: 1,
|
|
764
|
+
icon: "font-awesome/fa-desktop",
|
|
765
|
+
paletteLabel: "portal react",
|
|
766
|
+
label: function () {
|
|
767
|
+
return this.name || this.endpoint || "portal react";
|
|
768
|
+
},
|
|
769
|
+
|
|
770
|
+
oneditprepare: function () {
|
|
771
|
+
var node = this;
|
|
772
|
+
var code = node.componentCode || STARTER;
|
|
773
|
+
var headCode = node.customHead || "";
|
|
774
|
+
console.log("[FC-Monaco] PORTAL oneditprepare, node.id=" + node.id);
|
|
775
|
+
|
|
776
|
+
if (!window.__fcTwClasses) {
|
|
777
|
+
console.log("[FC-Monaco] loading tw-classes...");
|
|
778
|
+
$.getJSON("portal-react/tw-classes", function (classes) {
|
|
779
|
+
console.log("[FC-Monaco] tw-classes loaded, count=" + classes.length);
|
|
780
|
+
window.__fcTwClasses = classes;
|
|
781
|
+
}).fail(function (xhr) {
|
|
782
|
+
console.error("[FC-Monaco] tw-classes FAILED:", xhr.status, xhr.statusText);
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Tabs
|
|
787
|
+
var fcTabs = RED.tabs.create({
|
|
788
|
+
id: "fc-tabs",
|
|
789
|
+
onchange: function (tab) {
|
|
790
|
+
$(".fc-tab-pane").hide();
|
|
791
|
+
$("#" + tab.id).show();
|
|
792
|
+
setTimeout(function () {
|
|
793
|
+
if (editorInstance) editorInstance.layout();
|
|
794
|
+
if (headEditorInstance) headEditorInstance.layout();
|
|
795
|
+
}, 0);
|
|
796
|
+
},
|
|
797
|
+
});
|
|
798
|
+
fcTabs.addTab({ id: "fc-tab-jsx", label: "JSX" });
|
|
799
|
+
fcTabs.addTab({ id: "fc-tab-props", label: "Properties" });
|
|
800
|
+
fcTabs.addTab({ id: "fc-tab-head", label: "Head HTML" });
|
|
801
|
+
fcTabs.activateTab("fc-tab-jsx");
|
|
802
|
+
|
|
803
|
+
// URL hint
|
|
804
|
+
function updateHint() {
|
|
805
|
+
var ep = $("#node-input-endpoint").val() || "/portal";
|
|
806
|
+
$("#fc-url-hint").text("Page served at: http://<host>:1880" + ep);
|
|
807
|
+
}
|
|
808
|
+
$("#node-input-endpoint").on("input", updateHint);
|
|
809
|
+
updateHint();
|
|
810
|
+
|
|
811
|
+
// Monaco
|
|
812
|
+
window.__fcLoadMonaco(function (failed) {
|
|
813
|
+
if (failed) {
|
|
814
|
+
console.error("[FC-Monaco] PORTAL: Monaco load failed, using fallback textarea");
|
|
815
|
+
$("#fc-monaco").hide();
|
|
816
|
+
$("#fc-fallback").show().val(code);
|
|
817
|
+
$("#fc-head-monaco").hide();
|
|
818
|
+
$("#fc-head-fallback").show().val(headCode);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
console.log("[FC-Monaco] PORTAL: applying JSX defaults before model creation");
|
|
823
|
+
window.__fcApplyJsxDefaults();
|
|
824
|
+
var opts = window.__fcEditorOpts;
|
|
825
|
+
|
|
826
|
+
var jsxUri = monaco.Uri.parse("file:///fc-portal-" + node.id + ".jsx");
|
|
827
|
+
console.log("[FC-Monaco] PORTAL: JSX model URI=" + jsxUri.toString());
|
|
828
|
+
var existingJsx = monaco.editor.getModel(jsxUri);
|
|
829
|
+
if (existingJsx) {
|
|
830
|
+
console.log("[FC-Monaco] PORTAL: disposing existing JSX model");
|
|
831
|
+
existingJsx.dispose();
|
|
832
|
+
}
|
|
833
|
+
var jsxModel = monaco.editor.createModel(code, "javascript", jsxUri);
|
|
834
|
+
console.log("[FC-Monaco] PORTAL: JSX model created, language=" + jsxModel.getLanguageId());
|
|
835
|
+
editorInstance = monaco.editor.create(
|
|
836
|
+
document.getElementById("fc-monaco"),
|
|
837
|
+
Object.assign({ model: jsxModel }, opts),
|
|
838
|
+
);
|
|
839
|
+
console.log("[FC-Monaco] PORTAL: JSX editor created");
|
|
840
|
+
if (window.__fcAttachSelfClose) window.__fcAttachSelfClose(editorInstance);
|
|
841
|
+
|
|
842
|
+
var headUri = monaco.Uri.parse("file:///fc-head-" + node.id + ".html");
|
|
843
|
+
var existingHead = monaco.editor.getModel(headUri);
|
|
844
|
+
if (existingHead) existingHead.dispose();
|
|
845
|
+
var headModel = monaco.editor.createModel(headCode, "html", headUri);
|
|
846
|
+
headEditorInstance = monaco.editor.create(
|
|
847
|
+
document.getElementById("fc-head-monaco"),
|
|
848
|
+
Object.assign({ model: headModel }, opts),
|
|
849
|
+
);
|
|
850
|
+
console.log("[FC-Monaco] PORTAL: Head editor created");
|
|
851
|
+
|
|
852
|
+
// Log markers after a short delay (diagnostics are async)
|
|
853
|
+
setTimeout(function () {
|
|
854
|
+
var markers = monaco.editor.getModelMarkers({ resource: jsxUri });
|
|
855
|
+
console.log("[FC-Monaco] PORTAL: markers after 500ms, count=" + markers.length);
|
|
856
|
+
markers.forEach(function (m) {
|
|
857
|
+
console.log("[FC-Monaco] PORTAL marker: code=" + m.code + " severity=" + m.severity + " msg=" + m.message);
|
|
858
|
+
});
|
|
859
|
+
console.log("[FC-Monaco] PORTAL: current compilerOptions:", JSON.stringify(monaco.typescript.javascriptDefaults.getCompilerOptions()));
|
|
860
|
+
console.log("[FC-Monaco] PORTAL: current diagnosticsOptions:", JSON.stringify(monaco.typescript.javascriptDefaults.getDiagnosticsOptions()));
|
|
861
|
+
}, 500);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// Buttons
|
|
865
|
+
$("#fc-btn-starter").on("click", function () {
|
|
866
|
+
if (editorInstance) editorInstance.setValue(STARTER);
|
|
867
|
+
else $("#fc-fallback").val(STARTER);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
$("#fc-btn-preview").on("click", function () {
|
|
871
|
+
var ep = $("#node-input-endpoint").val();
|
|
872
|
+
if (ep) window.open(ep, "_blank");
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
$("#fc-btn-components").on("click", function () {
|
|
876
|
+
$.getJSON("portal-react/registry", function (reg) {
|
|
877
|
+
var names = Object.keys(reg);
|
|
878
|
+
if (!names.length) {
|
|
879
|
+
RED.notify("No component nodes on canvas.", "warning");
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
var html = "<p>Click to insert:</p>";
|
|
883
|
+
names.forEach(function (n) {
|
|
884
|
+
var c = reg[n];
|
|
885
|
+
html +=
|
|
886
|
+
'<div style="padding:4px 0;border-bottom:1px solid rgba(128,128,128,.2)">' +
|
|
887
|
+
'<button class="red-ui-button fc-lib-ins" data-name="' +
|
|
888
|
+
n +
|
|
889
|
+
'">' +
|
|
890
|
+
n +
|
|
891
|
+
"</button>" +
|
|
892
|
+
' <span style="opacity:.5;font-size:11px">in:[' +
|
|
893
|
+
(c.inputs || []).join(",") +
|
|
894
|
+
"] out:[" +
|
|
895
|
+
(c.outputs || []).join(",") +
|
|
896
|
+
"]</span></div>";
|
|
897
|
+
});
|
|
898
|
+
$("<div></div>")
|
|
899
|
+
.html(html)
|
|
900
|
+
.dialog({
|
|
901
|
+
title: "Available Components",
|
|
902
|
+
modal: true,
|
|
903
|
+
width: 400,
|
|
904
|
+
buttons: [
|
|
905
|
+
{
|
|
906
|
+
text: "Close",
|
|
907
|
+
click: function () {
|
|
908
|
+
$(this).dialog("close");
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
],
|
|
912
|
+
close: function () {
|
|
913
|
+
$(this).remove();
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
$(document).on("click", ".fc-lib-ins", function () {
|
|
917
|
+
var tag = "<" + $(this).data("name") + " />";
|
|
918
|
+
if (editorInstance) {
|
|
919
|
+
editorInstance.trigger("keyboard", "type", { text: tag });
|
|
920
|
+
editorInstance.focus();
|
|
921
|
+
} else {
|
|
922
|
+
var ta = $("#fc-fallback")[0];
|
|
923
|
+
var p = ta.selectionStart,
|
|
924
|
+
v = ta.value;
|
|
925
|
+
ta.value = v.slice(0, p) + tag + v.slice(p);
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
},
|
|
931
|
+
|
|
932
|
+
oneditsave: function () {
|
|
933
|
+
console.log("[FC-Monaco] PORTAL oneditsave");
|
|
934
|
+
var code = editorInstance
|
|
935
|
+
? editorInstance.getValue()
|
|
936
|
+
: $("#fc-fallback").val();
|
|
937
|
+
$("#node-input-componentCode").val(code);
|
|
938
|
+
this.componentCode = code;
|
|
939
|
+
|
|
940
|
+
var head = headEditorInstance
|
|
941
|
+
? headEditorInstance.getValue()
|
|
942
|
+
: $("#fc-head-fallback").val();
|
|
943
|
+
$("#node-input-customHead").val(head);
|
|
944
|
+
this.customHead = head;
|
|
945
|
+
|
|
946
|
+
if (editorInstance) {
|
|
947
|
+
editorInstance.getModel().dispose();
|
|
948
|
+
editorInstance.dispose();
|
|
949
|
+
editorInstance = null;
|
|
950
|
+
console.log("[FC-Monaco] PORTAL: JSX editor + model disposed");
|
|
951
|
+
}
|
|
952
|
+
if (headEditorInstance) {
|
|
953
|
+
headEditorInstance.getModel().dispose();
|
|
954
|
+
headEditorInstance.dispose();
|
|
955
|
+
headEditorInstance = null;
|
|
956
|
+
console.log("[FC-Monaco] PORTAL: Head editor + model disposed");
|
|
957
|
+
}
|
|
958
|
+
},
|
|
959
|
+
|
|
960
|
+
oneditcancel: function () {
|
|
961
|
+
console.log("[FC-Monaco] PORTAL oneditcancel");
|
|
962
|
+
if (editorInstance) {
|
|
963
|
+
editorInstance.getModel().dispose();
|
|
964
|
+
editorInstance.dispose();
|
|
965
|
+
editorInstance = null;
|
|
966
|
+
console.log("[FC-Monaco] PORTAL: JSX editor + model disposed");
|
|
967
|
+
}
|
|
968
|
+
if (headEditorInstance) {
|
|
969
|
+
headEditorInstance.getModel().dispose();
|
|
970
|
+
headEditorInstance.dispose();
|
|
971
|
+
headEditorInstance = null;
|
|
972
|
+
console.log("[FC-Monaco] PORTAL: Head editor + model disposed");
|
|
973
|
+
}
|
|
974
|
+
},
|
|
975
|
+
|
|
976
|
+
oneditresize: function (size) {
|
|
977
|
+
var tabsH = $("#fc-tabs").outerHeight(true) || 0;
|
|
978
|
+
var rows = $(
|
|
979
|
+
"#dialog-form>div:not(#fc-tabs-content):not(:has(#fc-tabs))",
|
|
980
|
+
);
|
|
981
|
+
var h = size.height;
|
|
982
|
+
rows.each(function () {
|
|
983
|
+
h -= $(this).outerHeight(true);
|
|
984
|
+
});
|
|
985
|
+
h -= tabsH + 30;
|
|
986
|
+
if (h < 200) h = 200;
|
|
987
|
+
$("#fc-editor-wrap").css("height", h + "px");
|
|
988
|
+
$("#fc-head-wrap").css("height", h + "px");
|
|
989
|
+
if (editorInstance) editorInstance.layout();
|
|
990
|
+
if (headEditorInstance) headEditorInstance.layout();
|
|
991
|
+
},
|
|
992
|
+
});
|
|
993
|
+
})();
|
|
994
|
+
</script>
|
|
995
|
+
|
|
996
|
+
<!-- ============================================================
|
|
997
|
+
Help
|
|
998
|
+
============================================================ -->
|
|
999
|
+
<script type="text/html" data-help-name="portal-react">
|
|
1000
|
+
<p>
|
|
1001
|
+
Renders a React application on a configurable HTTP endpoint with live
|
|
1002
|
+
WebSocket data binding.
|
|
1003
|
+
</p>
|
|
1004
|
+
<h3>Key difference from Dashboard 2.0</h3>
|
|
1005
|
+
<p>
|
|
1006
|
+
JSX is transpiled <strong>server-side at deploy time</strong> using Sucrase.
|
|
1007
|
+
The browser receives plain JS — no Babel, no runtime compilation. Page
|
|
1008
|
+
weight: ~45 KB (React production).
|
|
1009
|
+
</p>
|
|
1010
|
+
|
|
1011
|
+
<h3>Inputs</h3>
|
|
1012
|
+
<p>
|
|
1013
|
+
<code>msg.payload</code> is pushed to all connected clients via WebSocket.
|
|
1014
|
+
</p>
|
|
1015
|
+
<pre>
|
|
1016
|
+
const { data, send } = useNodeRed();
|
|
1017
|
+
// data = last msg.payload</pre
|
|
1018
|
+
>
|
|
1019
|
+
|
|
1020
|
+
<h3>Outputs</h3>
|
|
1021
|
+
<p>
|
|
1022
|
+
<code>send(payload, topic?)</code> emits a <code>msg</code> on the node's
|
|
1023
|
+
output wire.
|
|
1024
|
+
</p>
|
|
1025
|
+
|
|
1026
|
+
<h3>Deploy behavior</h3>
|
|
1027
|
+
<ul>
|
|
1028
|
+
<li>
|
|
1029
|
+
Each deploy re-transpiles JSX (cached by content hash — unchanged code is
|
|
1030
|
+
instant)
|
|
1031
|
+
</li>
|
|
1032
|
+
<li>
|
|
1033
|
+
Active WebSocket clients receive close code 1001 and auto-reconnect
|
|
1034
|
+
(exponential backoff)
|
|
1035
|
+
</li>
|
|
1036
|
+
<li>
|
|
1037
|
+
Stale HTTP routes and WS handlers are cleaned up before new ones register
|
|
1038
|
+
</li>
|
|
1039
|
+
<li>
|
|
1040
|
+
Transpile errors show in node status and as an error page on the endpoint
|
|
1041
|
+
</li>
|
|
1042
|
+
</ul>
|
|
1043
|
+
|
|
1044
|
+
<h3>Shared Components</h3>
|
|
1045
|
+
<p>
|
|
1046
|
+
Components defined in <strong>fc-portal-component</strong> nodes on the
|
|
1047
|
+
canvas are auto-injected into every portal-react page. Use them as JSX tags
|
|
1048
|
+
by their component name.
|
|
1049
|
+
</p>
|
|
1050
|
+
|
|
1051
|
+
<h3>Custom Head HTML</h3>
|
|
1052
|
+
<p>
|
|
1053
|
+
Inject CDN links, fonts, or extra stylesheets into
|
|
1054
|
+
<code><head></code>. Example:
|
|
1055
|
+
</p>
|
|
1056
|
+
<pre>
|
|
1057
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></pre
|
|
1058
|
+
>
|
|
1059
|
+
</script>
|