@aaqu/fromcubes-portal-react 0.1.0-alpha.20 → 0.1.0-alpha.22

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.
@@ -3,7 +3,7 @@
3
3
  ============================================================ -->
4
4
  <script type="text/javascript">
5
5
  (function () {
6
- var PREFIX = "[FC-Monaco]";
6
+ const PREFIX = "[FC-Monaco]";
7
7
 
8
8
  // Shared editor options — used by both component and portal-react editors
9
9
  window.__fcEditorOpts = {
@@ -20,7 +20,7 @@
20
20
  };
21
21
 
22
22
  window.__fcTwClasses = null;
23
- var jsxSetupDone = false;
23
+ let jsxSetupDone = false;
24
24
 
25
25
  // One-time Monaco setup: compiler opts, diagnostics, extra libs, completion provider.
26
26
  // Idempotent — safe to call multiple times, only runs setup once.
@@ -36,7 +36,7 @@
36
36
  }
37
37
  jsxSetupDone = true;
38
38
 
39
- var jsDef = monaco.typescript.javascriptDefaults;
39
+ const jsDef = monaco.typescript.javascriptDefaults;
40
40
 
41
41
  // Log initial state
42
42
  console.log(
@@ -53,9 +53,9 @@
53
53
  applyCompilerAndDiag();
54
54
 
55
55
  // Global type stubs
56
- var libContent = [
57
- "declare var React: any;",
58
- "declare var ReactDOM: any;",
56
+ const libContent = [
57
+ "declare const React: any;",
58
+ "declare const ReactDOM: any;",
59
59
  "declare function useNodeRed(opts?: { ignoreRecovery?: boolean }): { data: any; send: (payload: any, topic?: string) => void; user: { userId?: string; userName?: string; username?: string; email?: string; role?: string; groups?: any[] } | null; portalClient: string | null };",
60
60
  ].join("\n");
61
61
  console.log(PREFIX, "addExtraLib globals.d.ts");
@@ -68,44 +68,44 @@
68
68
  monaco.languages.registerCompletionItemProvider("javascript", {
69
69
  triggerCharacters: ['"', "'", "`", " "],
70
70
  provideCompletionItems: function (model, position) {
71
- var line = model.getLineContent(position.lineNumber);
72
- var before = line.substring(0, position.column - 1);
71
+ const line = model.getLineContent(position.lineNumber);
72
+ const before = line.substring(0, position.column - 1);
73
73
 
74
74
  // 1) className="..." or className={`...`}
75
- var isClassName =
75
+ const isClassName =
76
76
  before.match(/className\s*=\s*["'][^"']*$/) ||
77
77
  before.match(/className\s*=\s*\{`[^`]*$/);
78
78
 
79
79
  // 2) Any open string literal: "..., '..., or `...
80
- var inString = !isClassName && before.match(/["'`][^"'`]*$/);
80
+ const inString = !isClassName && before.match(/["'`][^"'`]*$/);
81
81
 
82
82
  if (!isClassName && !inString) return { suggestions: [] };
83
83
 
84
84
  // Extract text inside the string
85
- var raw = (isClassName || inString)[0];
86
- var inside;
85
+ const raw = (isClassName || inString)[0];
86
+ let inside;
87
87
  if (isClassName) {
88
88
  inside = raw.replace(/^className\s*=\s*(?:\{`|["'])/, "");
89
89
  } else {
90
90
  inside = raw.substring(1); // skip opening quote
91
91
  }
92
92
  inside = inside.replace(/\$\{[^}]*\}/g, " ");
93
- var parts = inside.split(/\s+/);
94
- var word = parts[parts.length - 1] || "";
93
+ const parts = inside.split(/\s+/);
94
+ const word = parts[parts.length - 1] || "";
95
95
 
96
96
  // For non-className strings, only activate if at least one existing
97
97
  // token in the string looks like a TW class (avoid noise in random strings)
98
98
  if (!isClassName && parts.length > 0) {
99
- var classes = window.__fcTwClasses || [];
100
- var twSet = window.__fcTwSet;
99
+ const classes = window.__fcTwClasses || [];
100
+ let twSet = window.__fcTwSet;
101
101
  if (!twSet && classes.length) {
102
102
  twSet = new Set(classes);
103
103
  window.__fcTwSet = twSet;
104
104
  console.log(PREFIX, "TW: built lookup Set, size=" + twSet.size);
105
105
  }
106
106
  if (twSet) {
107
- var hasTwToken = false;
108
- for (var p = 0; p < parts.length; p++) {
107
+ let hasTwToken = false;
108
+ for (let p = 0; p < parts.length; p++) {
109
109
  if (parts[p] && twSet.has(parts[p])) {
110
110
  hasTwToken = true;
111
111
  break;
@@ -113,7 +113,7 @@
113
113
  }
114
114
  // If typing the very first word, accept if it looks like a prefix
115
115
  if (!hasTwToken && parts.length === 1 && word.length >= 2) {
116
- for (var j = 0; j < classes.length; j++) {
116
+ for (let j = 0; j < classes.length; j++) {
117
117
  if (classes[j].indexOf(word) === 0) {
118
118
  hasTwToken = true;
119
119
  break;
@@ -124,17 +124,17 @@
124
124
  }
125
125
  }
126
126
 
127
- var range = {
127
+ const range = {
128
128
  startLineNumber: position.lineNumber,
129
129
  endLineNumber: position.lineNumber,
130
130
  startColumn: position.column - word.length,
131
131
  endColumn: position.column,
132
132
  };
133
133
 
134
- var classes = window.__fcTwClasses || [];
135
- var suggestions = [];
136
- for (var i = 0; i < classes.length; i++) {
137
- var cls = classes[i];
134
+ const classes = window.__fcTwClasses || [];
135
+ const suggestions = [];
136
+ for (let i = 0; i < classes.length; i++) {
137
+ const cls = classes[i];
138
138
  if (!word.length || cls.indexOf(word) === 0) {
139
139
  suggestions.push({
140
140
  label: cls,
@@ -162,7 +162,7 @@
162
162
 
163
163
  // JSX/HTML tag completion — type tag name, Tab → <tag>|</tag>
164
164
  console.log(PREFIX, "registering JSX tag completion provider");
165
- var HTML_TAGS = [
165
+ const HTML_TAGS = [
166
166
  "div",
167
167
  "span",
168
168
  "p",
@@ -238,40 +238,40 @@
238
238
  "use",
239
239
  "text",
240
240
  ];
241
- var VOID_TAGS = new Set(["br", "hr", "img", "input", "wbr", "source"]);
241
+ const VOID_TAGS = new Set(["br", "hr", "img", "input", "wbr", "source"]);
242
242
 
243
243
  monaco.languages.registerCompletionItemProvider("javascript", {
244
244
  triggerCharacters: ["<"],
245
245
  provideCompletionItems: function (model, position) {
246
- var line = model.getLineContent(position.lineNumber);
247
- var before = line.substring(0, position.column - 1);
246
+ const line = model.getLineContent(position.lineNumber);
247
+ const before = line.substring(0, position.column - 1);
248
248
 
249
249
  // After "<" — suggest tags
250
- var afterBracket = before.match(/<([a-zA-Z]*)$/);
250
+ const afterBracket = before.match(/<([a-zA-Z]*)$/);
251
251
  // Bare word at line start or after whitespace/{ — Emmet-like (lower or upper)
252
- var bareWord =
252
+ const bareWord =
253
253
  !afterBracket &&
254
254
  before.match(/(?:^|[\s{(,])([a-zA-Z][a-zA-Z0-9]*)$/);
255
255
 
256
256
  if (!afterBracket && !bareWord) return { suggestions: [] };
257
257
 
258
- var typed = (afterBracket ? afterBracket[1] : bareWord[1]) || "";
259
- var replaceStart =
258
+ const typed = (afterBracket ? afterBracket[1] : bareWord[1]) || "";
259
+ const replaceStart =
260
260
  position.column - typed.length - (afterBracket ? 1 : 0);
261
261
 
262
- var range = {
262
+ const range = {
263
263
  startLineNumber: position.lineNumber,
264
264
  endLineNumber: position.lineNumber,
265
265
  startColumn: replaceStart,
266
266
  endColumn: position.column,
267
267
  };
268
268
 
269
- var suggestions = [];
270
- var seen = new Set();
269
+ const suggestions = [];
270
+ const seen = new Set();
271
271
 
272
272
  // HTML tags
273
- for (var i = 0; i < HTML_TAGS.length; i++) {
274
- var tag = HTML_TAGS[i];
273
+ for (let i = 0; i < HTML_TAGS.length; i++) {
274
+ const tag = HTML_TAGS[i];
275
275
  if (typed && tag.indexOf(typed) !== 0) continue;
276
276
  seen.add(tag);
277
277
  if (VOID_TAGS.has(tag)) {
@@ -310,11 +310,11 @@
310
310
  }
311
311
 
312
312
  // Custom components from registry (PascalCase)
313
- var upperMatch =
313
+ const upperMatch =
314
314
  afterBracket && before.match(/<([A-Z][a-zA-Z0-9]*)$/);
315
- var bareUpper =
315
+ const bareUpper =
316
316
  !afterBracket && before.match(/(?:^|[\s{(,])([A-Z][a-zA-Z0-9]*)$/);
317
- var compTyped = upperMatch
317
+ const compTyped = upperMatch
318
318
  ? upperMatch[1]
319
319
  : bareUpper
320
320
  ? bareUpper[1]
@@ -322,9 +322,9 @@
322
322
 
323
323
  if (compTyped || (!typed && afterBracket)) {
324
324
  // Fetch component names from registry (cached on window)
325
- var reg = window.__fcComponentNames || [];
326
- for (var c = 0; c < reg.length; c++) {
327
- var name = reg[c];
325
+ const reg = window.__fcComponentNames || [];
326
+ for (let c = 0; c < reg.length; c++) {
327
+ const name = reg[c];
328
328
  if (seen.has(name)) continue;
329
329
  if (compTyped && name.indexOf(compTyped) !== 0) continue;
330
330
  if (
@@ -396,16 +396,16 @@
396
396
  window.__fcAttachSelfClose = function (editor) {
397
397
  editor.onDidChangeModelContent(function (e) {
398
398
  if (e.changes.length !== 1) return;
399
- var ch = e.changes[0];
399
+ const ch = e.changes[0];
400
400
  if (ch.text !== "/") return;
401
- var model = editor.getModel();
401
+ const model = editor.getModel();
402
402
  if (!model) return;
403
- var lineNum = ch.range.startLineNumber;
404
- var line = model.getLineContent(lineNum);
403
+ const lineNum = ch.range.startLineNumber;
404
+ const line = model.getLineContent(lineNum);
405
405
  // Pattern 1: <tag>/</tag> (cursor was between > and </)
406
406
  // Pattern 2: <tag/></tag> (cursor was before > in opening tag)
407
- var m = line.match(/^(.*)<([a-zA-Z][a-zA-Z0-9]*)>\/<\/\2>(.*)$/);
408
- var matchStr, tag, prefix;
407
+ let m = line.match(/^(.*)<([a-zA-Z][a-zA-Z0-9]*)>\/<\/\2>(.*)$/);
408
+ let matchStr, tag, prefix;
409
409
  if (m) {
410
410
  prefix = m[1];
411
411
  tag = m[2];
@@ -417,10 +417,10 @@
417
417
  tag = m[2];
418
418
  matchStr = "<" + tag + "/></" + tag + ">";
419
419
  }
420
- var startCol = prefix.length + 1;
421
- var endCol = startCol + matchStr.length;
422
- var replacement = "<" + tag + " />";
423
- var cursorCol = startCol + replacement.length - 2;
420
+ const startCol = prefix.length + 1;
421
+ const endCol = startCol + matchStr.length;
422
+ const replacement = "<" + tag + " />";
423
+ const cursorCol = startCol + replacement.length - 2;
424
424
  setTimeout(function () {
425
425
  editor.executeEdits("self-close-collapse", [
426
426
  {
@@ -454,10 +454,72 @@
454
454
  }
455
455
  refreshComponentNames();
456
456
 
457
+ // Fetch utility nodes + their parsed top-level symbols.
458
+ // Window cache shape: { byNode: { utilName: [symbol, ...] }, allSymbols: [..] }
459
+ window.__fcRefreshUtilities = function () {
460
+ $.getJSON("portal-react/utilities", function (reg) {
461
+ const byNode = {};
462
+ const all = new Set();
463
+ Object.keys(reg).forEach(function (n) {
464
+ const syms = reg[n].symbols || [];
465
+ byNode[n] = syms;
466
+ syms.forEach(function (s) { all.add(s); });
467
+ });
468
+ window.__fcUtilities = { byNode: byNode, allSymbols: [...all] };
469
+ console.log(
470
+ PREFIX,
471
+ "utilities loaded: " + Object.keys(byNode).length + " nodes, " + all.size + " symbols",
472
+ );
473
+ }).fail(function () {
474
+ window.__fcUtilities = { byNode: {}, allSymbols: [] };
475
+ });
476
+ };
477
+ window.__fcRefreshUtilities();
478
+
479
+ // Utility-symbol completion: suggests bare identifiers anywhere in JS
480
+ // context EXCEPT after "<" (that's JSX tag completion, handled below).
481
+ monaco.languages.registerCompletionItemProvider("javascript", {
482
+ triggerCharacters: ["."], // jQuery-ish — but we mostly fire from word context
483
+ provideCompletionItems: function (model, position) {
484
+ const line = model.getLineContent(position.lineNumber);
485
+ const before = line.substring(0, position.column - 1);
486
+ // Skip JSX tag context — covered by other providers
487
+ if (/<[a-zA-Z]*$/.test(before)) return { suggestions: [] };
488
+ // Skip property access — let Monaco's language service handle it
489
+ if (/\.\s*\w*$/.test(before)) return { suggestions: [] };
490
+ // Skip inside strings
491
+ if (/["'`][^"'`]*$/.test(before)) return { suggestions: [] };
492
+ const wordMatch = before.match(/([A-Za-z_$][\w$]*)$/);
493
+ const word = wordMatch ? wordMatch[1] : "";
494
+ const all = (window.__fcUtilities && window.__fcUtilities.allSymbols) || [];
495
+ if (!all.length) return { suggestions: [] };
496
+ const range = {
497
+ startLineNumber: position.lineNumber,
498
+ endLineNumber: position.lineNumber,
499
+ startColumn: position.column - word.length,
500
+ endColumn: position.column,
501
+ };
502
+ const suggestions = [];
503
+ for (let i = 0; i < all.length; i++) {
504
+ const sym = all[i];
505
+ if (word && sym.indexOf(word) !== 0) continue;
506
+ suggestions.push({
507
+ label: sym,
508
+ kind: monaco.languages.CompletionItemKind.Function,
509
+ insertText: sym,
510
+ range: range,
511
+ detail: "fc-portal-utility",
512
+ sortText: "1" + sym,
513
+ });
514
+ }
515
+ return { suggestions: suggestions };
516
+ },
517
+ });
518
+
457
519
  // Marker listener for debugging
458
520
  monaco.editor.onDidChangeMarkers(function (uris) {
459
521
  uris.forEach(function (uri) {
460
- var markers = monaco.editor.getModelMarkers({ resource: uri });
522
+ const markers = monaco.editor.getModelMarkers({ resource: uri });
461
523
  if (markers.length > 0) {
462
524
  console.group(PREFIX + " MARKERS on " + uri.toString());
463
525
  markers.forEach(function (m) {
@@ -486,8 +548,8 @@
486
548
  }
487
549
 
488
550
  function applyCompilerAndDiag() {
489
- var jsDef = monaco.typescript.javascriptDefaults;
490
- var compilerOpts = {
551
+ const jsDef = monaco.typescript.javascriptDefaults;
552
+ const compilerOpts = {
491
553
  jsx: monaco.typescript.JsxEmit.React,
492
554
  target: monaco.typescript.ScriptTarget.ESNext,
493
555
  module: monaco.typescript.ModuleKind.ESNext,
@@ -497,9 +559,12 @@
497
559
  console.log(PREFIX, "setCompilerOptions:", JSON.stringify(compilerOpts));
498
560
  jsDef.setCompilerOptions(compilerOpts);
499
561
 
500
- var diagOpts = {
562
+ const diagOpts = {
501
563
  noSemanticValidation: true,
502
- noSyntaxValidation: false,
564
+ // Server-side esbuild does the real syntax check at deploy time;
565
+ // Monaco's TS parser produces noisy false positives on raw JSX
566
+ // (1109, 1005, 1128 etc.), so we silence its squiggles entirely.
567
+ noSyntaxValidation: true,
503
568
  noSuggestionDiagnostics: true,
504
569
  diagnosticCodesToIgnore: [17004],
505
570
  };
@@ -534,7 +599,7 @@
534
599
  cb();
535
600
  return;
536
601
  }
537
- var s = document.createElement("script");
602
+ const s = document.createElement("script");
538
603
  s.src = "portal-react/vs/loader.js";
539
604
  s.onload = function () {
540
605
  console.log(PREFIX, "loader.js loaded, configuring require paths");
@@ -587,9 +652,9 @@
587
652
 
588
653
  <script type="text/javascript">
589
654
  (function () {
590
- var compEditorInstance = null;
655
+ let compEditorInstance = null;
591
656
 
592
- var COMP_STARTER = [
657
+ const COMP_STARTER = [
593
658
  "function StatusCard({ label, value, unit }) {",
594
659
  " return (",
595
660
  ' <div className="rounded-2xl bg-zinc-900 border border-zinc-800 p-6">',
@@ -619,8 +684,8 @@
619
684
  },
620
685
 
621
686
  oneditprepare: function () {
622
- var node = this;
623
- var code = node.compCode || COMP_STARTER;
687
+ const node = this;
688
+ const code = node.compCode || COMP_STARTER;
624
689
  console.log("[FC-Monaco] COMP oneditprepare, node.id=" + node.id);
625
690
 
626
691
  if (!window.__fcTwClasses) {
@@ -654,14 +719,14 @@
654
719
  );
655
720
  window.__fcApplyJsxDefaults();
656
721
 
657
- var compUri = monaco.Uri.parse("file:///fc-comp-" + node.id + ".jsx");
722
+ const compUri = monaco.Uri.parse("file:///fc-comp-" + node.id + ".jsx");
658
723
  console.log("[FC-Monaco] COMP: model URI=" + compUri.toString());
659
- var existingModel = monaco.editor.getModel(compUri);
724
+ const existingModel = monaco.editor.getModel(compUri);
660
725
  if (existingModel) {
661
726
  console.log("[FC-Monaco] COMP: disposing existing model");
662
727
  existingModel.dispose();
663
728
  }
664
- var compModel = monaco.editor.createModel(
729
+ const compModel = monaco.editor.createModel(
665
730
  code,
666
731
  "javascript",
667
732
  compUri,
@@ -679,7 +744,7 @@
679
744
 
680
745
  // Log markers after a short delay (diagnostics are async)
681
746
  setTimeout(function () {
682
- var markers = monaco.editor.getModelMarkers({ resource: compUri });
747
+ const markers = monaco.editor.getModelMarkers({ resource: compUri });
683
748
  console.log(
684
749
  "[FC-Monaco] COMP: markers after 500ms, count=" + markers.length,
685
750
  );
@@ -707,7 +772,7 @@
707
772
 
708
773
  oneditsave: function () {
709
774
  console.log("[FC-Monaco] COMP oneditsave");
710
- var code = compEditorInstance
775
+ const code = compEditorInstance
711
776
  ? compEditorInstance.getValue()
712
777
  : $("#fcc-fallback").val();
713
778
  $("#node-input-compCode").val(code);
@@ -731,7 +796,7 @@
731
796
  },
732
797
 
733
798
  oneditresize: function (size) {
734
- var h = size.height - 180;
799
+ let h = size.height - 180;
735
800
  if (h < 150) h = 150;
736
801
  $("#fcc-editor-wrap").css("height", h + "px");
737
802
  if (compEditorInstance) compEditorInstance.layout();
@@ -769,6 +834,198 @@
769
834
  </p>
770
835
  </script>
771
836
 
837
+ <!-- ============================================================
838
+ fc-portal-utility – canvas node (helpers / hooks / constants)
839
+ ============================================================ -->
840
+ <script type="text/html" data-template-name="fc-portal-utility">
841
+ <div class="form-row">
842
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
843
+ <input type="text" id="node-input-name" placeholder="Label on canvas" />
844
+ </div>
845
+ <div class="form-row">
846
+ <label for="node-input-utilName"><i class="fa fa-wrench"></i> Module name</label>
847
+ <input type="text" id="node-input-utilName" placeholder="mathHelpers" />
848
+ </div>
849
+ <div class="form-row" style="margin-bottom:4px;">
850
+ <label><i class="fa fa-code"></i> Code</label>
851
+ <div style="font-size:11px;opacity:.5;margin-top:2px;">
852
+ Top-level helpers, custom hooks and constants. Available globally to portals
853
+ that reference any of the symbols declared here.
854
+ </div>
855
+ </div>
856
+ <div class="form-row node-text-editor-row">
857
+ <div
858
+ id="fcu-editor-wrap"
859
+ style="width:100%;height:350px;border:1px solid var(--red-ui-form-input-border-color,#555);border-radius:4px;overflow:hidden;position:relative;"
860
+ >
861
+ <div id="fcu-monaco" style="width:100%;height:100%;"></div>
862
+ <textarea
863
+ id="fcu-fallback"
864
+ 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;"
865
+ ></textarea>
866
+ </div>
867
+ </div>
868
+ <input type="hidden" id="node-input-utilCode" />
869
+ </script>
870
+
871
+ <script type="text/javascript">
872
+ (function () {
873
+ let utilEditorInstance = null;
874
+
875
+ const UTIL_STARTER = [
876
+ "// Helpers / custom hooks / constants — top-level, no React component.",
877
+ "// Each symbol declared here is available globally in any portal that",
878
+ "// references it (selective inclusion: unused utility nodes are skipped).",
879
+ "",
880
+ "const PI2 = Math.PI * 2;",
881
+ "",
882
+ "function clamp(n, min, max) {",
883
+ " return Math.max(min, Math.min(max, n));",
884
+ "}",
885
+ "",
886
+ "// Custom React hook — call from inside App() or library components",
887
+ "function useDebounce(value, ms = 300) {",
888
+ " const [v, setV] = React.useState(value);",
889
+ " React.useEffect(() => {",
890
+ " const t = setTimeout(() => setV(value), ms);",
891
+ " return () => clearTimeout(t);",
892
+ " }, [value, ms]);",
893
+ " return v;",
894
+ "}",
895
+ ].join("\n");
896
+
897
+ RED.nodes.registerType("fc-portal-utility", {
898
+ category: "fromcubes",
899
+ color: "#fbbf24",
900
+ defaults: {
901
+ name: { value: "" },
902
+ utilName: { value: "myHelpers", required: true },
903
+ utilCode: { value: UTIL_STARTER },
904
+ },
905
+ inputs: 0,
906
+ outputs: 0,
907
+ icon: "font-awesome/fa-wrench",
908
+ paletteLabel: "fromcubes utility",
909
+ label: function () {
910
+ return this.name || this.utilName || "utility";
911
+ },
912
+
913
+ oneditprepare: function () {
914
+ const node = this;
915
+ const code = node.utilCode || UTIL_STARTER;
916
+ console.log("[FC-Monaco] UTIL oneditprepare, node.id=" + node.id);
917
+
918
+ if (!window.__fcTwClasses) {
919
+ $.getJSON("portal-react/tw-classes", function (classes) {
920
+ window.__fcTwClasses = classes;
921
+ });
922
+ }
923
+
924
+ window.__fcLoadMonaco(function (failed) {
925
+ if (failed) {
926
+ $("#fcu-monaco").hide();
927
+ $("#fcu-fallback").show().val(code);
928
+ return;
929
+ }
930
+ window.__fcApplyJsxDefaults();
931
+
932
+ const utilUri = monaco.Uri.parse("file:///fc-util-" + node.id + ".js");
933
+ const existingModel = monaco.editor.getModel(utilUri);
934
+ if (existingModel) existingModel.dispose();
935
+ const utilModel = monaco.editor.createModel(code, "javascript", utilUri);
936
+
937
+ utilEditorInstance = monaco.editor.create(
938
+ document.getElementById("fcu-monaco"),
939
+ Object.assign({ model: utilModel }, window.__fcEditorOpts),
940
+ );
941
+ });
942
+ },
943
+
944
+ oneditsave: function () {
945
+ const code = utilEditorInstance
946
+ ? utilEditorInstance.getValue()
947
+ : $("#fcu-fallback").val();
948
+ $("#node-input-utilCode").val(code);
949
+ this.utilCode = code;
950
+ if (utilEditorInstance) {
951
+ utilEditorInstance.getModel().dispose();
952
+ utilEditorInstance.dispose();
953
+ utilEditorInstance = null;
954
+ }
955
+ },
956
+
957
+ oneditcancel: function () {
958
+ if (utilEditorInstance) {
959
+ utilEditorInstance.getModel().dispose();
960
+ utilEditorInstance.dispose();
961
+ utilEditorInstance = null;
962
+ }
963
+ },
964
+
965
+ oneditresize: function (size) {
966
+ let h = size.height - 200;
967
+ if (h < 150) h = 150;
968
+ $("#fcu-editor-wrap").css("height", h + "px");
969
+ if (utilEditorInstance) utilEditorInstance.layout();
970
+ },
971
+ });
972
+ })();
973
+ </script>
974
+
975
+ <script type="text/html" data-help-name="fc-portal-utility">
976
+ <p>
977
+ Defines shared <strong>helpers</strong>, <strong>custom hooks</strong> and
978
+ <strong>constants</strong> available to all <strong>portal-react</strong>
979
+ nodes. Unlike <code>fc-portal-component</code>, the code is injected raw at
980
+ top level (no IIFE wrapper) — a single utility node may declare many symbols.
981
+ </p>
982
+ <h3>Properties</h3>
983
+ <dl>
984
+ <dt>Module name</dt>
985
+ <dd>
986
+ Identifier for this utility node. Shares a namespace with
987
+ <code>fc-portal-component</code> names — must be unique across all
988
+ components and utilities.
989
+ </dd>
990
+ <dt>Code</dt>
991
+ <dd>
992
+ Top-level JavaScript: <code>const</code>, <code>let</code>,
993
+ <code>function</code>, <code>class</code> declarations. <code>import</code>
994
+ statements at the top are auto-hoisted into the bundle.
995
+ </dd>
996
+ </dl>
997
+ <h3>Selective inclusion</h3>
998
+ <p>
999
+ A utility node is bundled into a portal only when the portal's JSX (or any
1000
+ of its referenced library components) mentions at least one of the symbols
1001
+ declared in the utility. Unused utility nodes are skipped.
1002
+ </p>
1003
+ <p>
1004
+ Group related symbols in one node — referencing any one symbol pulls in the
1005
+ whole node's code. Split unrelated helpers across multiple nodes for finer
1006
+ granularity.
1007
+ </p>
1008
+ <h3>Example</h3>
1009
+ <pre>function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
1010
+
1011
+ function useDebounce(value, ms = 300) {
1012
+ const [v, setV] = React.useState(value);
1013
+ React.useEffect(() => {
1014
+ const t = setTimeout(() => setV(value), ms);
1015
+ return () => clearTimeout(t);
1016
+ }, [value, ms]);
1017
+ return v;
1018
+ }</pre>
1019
+ <p>
1020
+ Then in any <strong>portal-react</strong> node:
1021
+ </p>
1022
+ <pre>function App() {
1023
+ const { data } = useNodeRed();
1024
+ const slow = useDebounce(data?.value);
1025
+ return &lt;div&gt;{clamp(slow ?? 0, 0, 100)}&lt;/div&gt;;
1026
+ }</pre>
1027
+ </script>
1028
+
772
1029
  <!-- ============================================================
773
1030
  portal-react – main node
774
1031
  ============================================================ -->
@@ -799,6 +1056,9 @@
799
1056
  <button type="button" class="red-ui-button" id="fc-btn-components">
800
1057
  <i class="fa fa-cube"></i> Components
801
1058
  </button>
1059
+ <button type="button" class="red-ui-button" id="fc-btn-utilities">
1060
+ <i class="fa fa-wrench"></i> Utilities
1061
+ </button>
802
1062
  <span style="flex:1"></span>
803
1063
  <button type="button" class="red-ui-button" id="fc-btn-preview">
804
1064
  <i class="fa fa-eye"></i> Preview
@@ -906,10 +1166,10 @@
906
1166
 
907
1167
  <script type="text/javascript">
908
1168
  (function () {
909
- var editorInstance = null;
910
- var headEditorInstance = null;
1169
+ let editorInstance = null;
1170
+ let headEditorInstance = null;
911
1171
 
912
- var STARTER = [
1172
+ const STARTER = [
913
1173
  "// useNodeRed() \u2192 { data, send, portalClient }",
914
1174
  "// data = last msg.payload from input wire",
915
1175
  "// send(payload, topic?) = push msg to output wire",
@@ -946,15 +1206,15 @@
946
1206
  required: true,
947
1207
  validate: function (v) {
948
1208
  if (typeof v !== "string") return false;
949
- var t = v.trim();
1209
+ const t = v.trim();
950
1210
  if (t.length === 0) return false;
951
1211
  if (/\s/.test(t)) return false;
952
1212
  if (t.charAt(0) === "/" || t.charAt(t.length - 1) === "/") return false;
953
- var segs = t.split("/");
954
- for (var i = 0; i < segs.length; i++) {
955
- var s = segs[i];
1213
+ const segs = t.split("/");
1214
+ for (let i = 0; i < segs.length; i++) {
1215
+ const s = segs[i];
956
1216
  if (!s || s === "." || s === "..") return false;
957
- var lower = s.toLowerCase();
1217
+ const lower = s.toLowerCase();
958
1218
  if (lower === "public" || lower === "_ws") return false;
959
1219
  if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(s)) return false;
960
1220
  }
@@ -978,9 +1238,9 @@
978
1238
  },
979
1239
 
980
1240
  oneditprepare: function () {
981
- var node = this;
982
- var code = node.componentCode || STARTER;
983
- var headCode = node.customHead || "";
1241
+ const node = this;
1242
+ const code = node.componentCode || STARTER;
1243
+ const headCode = node.customHead || "";
984
1244
  console.log("[FC-Monaco] PORTAL oneditprepare, node.id=" + node.id);
985
1245
 
986
1246
  if (!window.__fcTwClasses) {
@@ -1000,7 +1260,7 @@
1000
1260
  }
1001
1261
 
1002
1262
  // Tabs
1003
- var fcTabs = RED.tabs.create({
1263
+ const fcTabs = RED.tabs.create({
1004
1264
  id: "fc-tabs",
1005
1265
  onchange: function (tab) {
1006
1266
  $(".fc-tab-pane").hide();
@@ -1018,8 +1278,8 @@
1018
1278
 
1019
1279
  // Legacy endpoint detection + convenience pre-fill
1020
1280
  if (node.endpoint && typeof node.endpoint === "string") {
1021
- var legacy = node.endpoint;
1022
- var prefix = "/fromcubes/";
1281
+ const legacy = node.endpoint;
1282
+ const prefix = "/fromcubes/";
1023
1283
  if (legacy.indexOf(prefix) === 0) {
1024
1284
  if (!node.subPath) {
1025
1285
  $("#node-input-subPath").val(legacy.slice(prefix.length));
@@ -1037,8 +1297,8 @@
1037
1297
 
1038
1298
  // URL hint
1039
1299
  function updateHint() {
1040
- var sp = ($("#node-input-subPath").val() || "").trim();
1041
- var root = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
1300
+ const sp = ($("#node-input-subPath").val() || "").trim();
1301
+ const root = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
1042
1302
  if (sp) {
1043
1303
  $("#fc-url-hint")
1044
1304
  .css("color", "")
@@ -1058,15 +1318,15 @@
1058
1318
  updateHint();
1059
1319
 
1060
1320
  // Modules editableList (like function node's libs)
1061
- var libsList = node.libs || [];
1321
+ const libsList = node.libs || [];
1062
1322
  $("#node-input-libs-container").css("min-height","68px").editableList({
1063
1323
  addItem: function(container, i, opt) {
1064
- var lib = opt || {};
1065
- var row = $('<div/>',{style:"display:flex;gap:8px;align-items:center;"}).appendTo(container);
1066
- var modInput = $('<input/>',{type:"text",placeholder:"e.g. chart.js/auto@^4.4.0",style:"flex:1;"}).appendTo(row);
1067
- var varInput = $('<input/>',{type:"text",placeholder:"Import as (e.g. Chart)",style:"width:140px;"}).appendTo(row);
1324
+ const lib = opt || {};
1325
+ const row = $('<div/>',{style:"display:flex;gap:8px;align-items:center;"}).appendTo(container);
1326
+ const modInput = $('<input/>',{type:"text",placeholder:"e.g. chart.js/auto@^4.4.0",style:"flex:1;"}).appendTo(row);
1327
+ const varInput = $('<input/>',{type:"text",placeholder:"Import as (e.g. Chart)",style:"width:140px;"}).appendTo(row);
1068
1328
  modInput.val(lib.module || "");
1069
- varInput.val(lib.var || "");
1329
+ varInput.val(lib.const || "");
1070
1330
  container.data("mod", modInput);
1071
1331
  container.data("var", varInput);
1072
1332
  },
@@ -1094,18 +1354,18 @@
1094
1354
  "[FC-Monaco] PORTAL: applying JSX defaults before model creation",
1095
1355
  );
1096
1356
  window.__fcApplyJsxDefaults();
1097
- var opts = window.__fcEditorOpts;
1357
+ const opts = window.__fcEditorOpts;
1098
1358
 
1099
- var jsxUri = monaco.Uri.parse(
1359
+ const jsxUri = monaco.Uri.parse(
1100
1360
  "file:///fc-portal-" + node.id + ".jsx",
1101
1361
  );
1102
1362
  console.log("[FC-Monaco] PORTAL: JSX model URI=" + jsxUri.toString());
1103
- var existingJsx = monaco.editor.getModel(jsxUri);
1363
+ const existingJsx = monaco.editor.getModel(jsxUri);
1104
1364
  if (existingJsx) {
1105
1365
  console.log("[FC-Monaco] PORTAL: disposing existing JSX model");
1106
1366
  existingJsx.dispose();
1107
1367
  }
1108
- var jsxModel = monaco.editor.createModel(code, "javascript", jsxUri);
1368
+ const jsxModel = monaco.editor.createModel(code, "javascript", jsxUri);
1109
1369
  console.log(
1110
1370
  "[FC-Monaco] PORTAL: JSX model created, language=" +
1111
1371
  jsxModel.getLanguageId(),
@@ -1118,12 +1378,12 @@
1118
1378
  if (window.__fcAttachSelfClose)
1119
1379
  window.__fcAttachSelfClose(editorInstance);
1120
1380
 
1121
- var headUri = monaco.Uri.parse(
1381
+ const headUri = monaco.Uri.parse(
1122
1382
  "file:///fc-head-" + node.id + ".html",
1123
1383
  );
1124
- var existingHead = monaco.editor.getModel(headUri);
1384
+ const existingHead = monaco.editor.getModel(headUri);
1125
1385
  if (existingHead) existingHead.dispose();
1126
- var headModel = monaco.editor.createModel(headCode, "html", headUri);
1386
+ const headModel = monaco.editor.createModel(headCode, "html", headUri);
1127
1387
  headEditorInstance = monaco.editor.create(
1128
1388
  document.getElementById("fc-head-monaco"),
1129
1389
  Object.assign({ model: headModel }, opts),
@@ -1132,7 +1392,7 @@
1132
1392
 
1133
1393
  // Log markers after a short delay (diagnostics are async)
1134
1394
  setTimeout(function () {
1135
- var markers = monaco.editor.getModelMarkers({ resource: jsxUri });
1395
+ const markers = monaco.editor.getModelMarkers({ resource: jsxUri });
1136
1396
  console.log(
1137
1397
  "[FC-Monaco] PORTAL: markers after 500ms, count=" +
1138
1398
  markers.length,
@@ -1181,14 +1441,14 @@
1181
1441
  });
1182
1442
 
1183
1443
  $("#fc-btn-preview").on("click", function () {
1184
- var sp = ($("#node-input-subPath").val() || "").trim();
1185
- var root = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
1444
+ const sp = ($("#node-input-subPath").val() || "").trim();
1445
+ const root = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
1186
1446
  if (sp) window.open(root + "/fromcubes/" + sp, "_blank");
1187
1447
  });
1188
1448
 
1189
1449
  $("#fc-btn-components").on("click", function () {
1190
1450
  $.getJSON("portal-react/registry", function (reg) {
1191
- var names = Object.keys(reg).sort();
1451
+ const names = Object.keys(reg).sort();
1192
1452
  if (!names.length) {
1193
1453
  RED.notify("No component nodes on canvas.", "warning");
1194
1454
  return;
@@ -1196,12 +1456,12 @@
1196
1456
 
1197
1457
  function extractProps(code) {
1198
1458
  if (!code) return [];
1199
- var m = code.match(/function\s+\w+\s*\(\s*\{([^}]*)\}/);
1459
+ const m = code.match(/function\s+\w+\s*\(\s*\{([^}]*)\}/);
1200
1460
  if (!m) return [];
1201
1461
  return m[1].split(",").map(function (s) { return s.trim().split(/\s*=\s*/)[0]; }).filter(Boolean);
1202
1462
  }
1203
1463
 
1204
- var html =
1464
+ let html =
1205
1465
  '<div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">' +
1206
1466
  '<input type="text" id="fc-comp-search" placeholder="Search..." ' +
1207
1467
  'style="width:100%;box-sizing:border-box;margin-bottom:6px;padding:5px 8px;border:1px solid rgba(128,128,128,.4);border-radius:3px;' +
@@ -1209,16 +1469,16 @@
1209
1469
  '<div id="fc-comp-list" style="flex:1;overflow-y:auto;">';
1210
1470
 
1211
1471
  names.forEach(function (n) {
1212
- var c = reg[n];
1213
- var props = extractProps(c.code);
1214
- var detailParts = [];
1472
+ const c = reg[n];
1473
+ const props = extractProps(c.code);
1474
+ const detailParts = [];
1215
1475
  if (props.length) {
1216
1476
  detailParts.push('<div style="margin:4px 0 0 0;font-size:11px;opacity:.6;">' +
1217
1477
  props.map(function (p) {
1218
1478
  return '<code style="background:rgba(128,128,128,.15);padding:0 4px;border-radius:2px;margin-right:3px;">' + p + '</code>';
1219
1479
  }).join("") + '</div>');
1220
1480
  }
1221
- var hasDetail = detailParts.length > 0;
1481
+ const hasDetail = detailParts.length > 0;
1222
1482
 
1223
1483
  html +=
1224
1484
  '<div class="fc-comp-item" data-name="' + n + '" data-search="' + n.toLowerCase() + '" ' +
@@ -1232,7 +1492,7 @@
1232
1492
  });
1233
1493
  html += '</div></div>';
1234
1494
 
1235
- var $dlg = $("<div></div>").html(html).dialog({
1495
+ const $dlg = $("<div></div>").html(html).dialog({
1236
1496
  title: "Components",
1237
1497
  modal: true,
1238
1498
  width: 380,
@@ -1244,7 +1504,7 @@
1244
1504
 
1245
1505
  // Search
1246
1506
  $dlg.find("#fc-comp-search").on("input", function () {
1247
- var q = $(this).val().toLowerCase();
1507
+ const q = $(this).val().toLowerCase();
1248
1508
  $dlg.find(".fc-comp-item").each(function () {
1249
1509
  $(this).toggle($(this).data("search").indexOf(q) !== -1);
1250
1510
  });
@@ -1260,24 +1520,24 @@
1260
1520
  // Arrow toggle detail
1261
1521
  $dlg.on("click", ".fc-comp-arrow", function (e) {
1262
1522
  e.stopPropagation();
1263
- var $item = $(this).closest(".fc-comp-item");
1264
- var $detail = $item.find(".fc-comp-detail");
1265
- var open = $detail.is(":visible");
1523
+ const $item = $(this).closest(".fc-comp-item");
1524
+ const $detail = $item.find(".fc-comp-detail");
1525
+ const open = $detail.is(":visible");
1266
1526
  $detail.slideToggle(100);
1267
1527
  $(this).css("transform", open ? "" : "rotate(90deg)");
1268
1528
  });
1269
1529
 
1270
1530
  // Click name: delete selection, insert <Tag></Tag> in one line, cursor between tags
1271
1531
  $dlg.on("click", ".fc-comp-name", function () {
1272
- var name = $(this).data("name");
1273
- var openTag = "<" + name + ">";
1274
- var closeTag = "</" + name + ">";
1275
- var text = openTag + closeTag;
1532
+ const name = $(this).data("name");
1533
+ const openTag = "<" + name + ">";
1534
+ const closeTag = "</" + name + ">";
1535
+ const text = openTag + closeTag;
1276
1536
  $dlg.dialog("close");
1277
1537
  if (editorInstance) {
1278
- var sel = editorInstance.getSelection();
1279
- var startLine = sel.startLineNumber;
1280
- var startCol = sel.startColumn;
1538
+ const sel = editorInstance.getSelection();
1539
+ const startLine = sel.startLineNumber;
1540
+ const startCol = sel.startColumn;
1281
1541
  editorInstance.executeEdits("fc-components", [{
1282
1542
  range: sel,
1283
1543
  text: text,
@@ -1287,8 +1547,8 @@
1287
1547
  editorInstance.focus();
1288
1548
  }, 50);
1289
1549
  } else {
1290
- var ta = $("#fc-fallback")[0];
1291
- var s = ta.selectionStart, e = ta.selectionEnd, v = ta.value;
1550
+ const ta = $("#fc-fallback")[0];
1551
+ const s = ta.selectionStart, e = ta.selectionEnd, v = ta.value;
1292
1552
  ta.value = v.slice(0, s) + text + v.slice(e);
1293
1553
  setTimeout(function () {
1294
1554
  ta.selectionStart = ta.selectionEnd = s + openTag.length;
@@ -1298,6 +1558,118 @@
1298
1558
  });
1299
1559
  });
1300
1560
  });
1561
+
1562
+ $("#fc-btn-utilities").on("click", function () {
1563
+ $.getJSON("portal-react/utilities", function (reg) {
1564
+ // Refresh global cache too so completion provider picks up changes
1565
+ if (window.__fcRefreshUtilities) window.__fcRefreshUtilities();
1566
+ const names = Object.keys(reg).sort();
1567
+ if (!names.length) {
1568
+ RED.notify("No utility nodes on canvas.", "warning");
1569
+ return;
1570
+ }
1571
+
1572
+ let html =
1573
+ '<div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">' +
1574
+ '<input type="text" id="fc-util-search" placeholder="Search..." ' +
1575
+ 'style="width:100%;box-sizing:border-box;margin-bottom:6px;padding:5px 8px;border:1px solid rgba(128,128,128,.4);border-radius:3px;' +
1576
+ 'background:var(--red-ui-form-input-background-color,#fff);color:var(--red-ui-form-text-color,#333);font-size:12px;flex-shrink:0;" />' +
1577
+ '<div id="fc-util-list" style="flex:1;overflow-y:auto;">';
1578
+
1579
+ names.forEach(function (n) {
1580
+ const u = reg[n];
1581
+ const syms = u.symbols || [];
1582
+ const errBadge = u.error
1583
+ ? '<span style="margin-left:6px;font-size:10px;color:#ef4444;" title="' +
1584
+ String(u.error).replace(/"/g, "&quot;") + '">syntax error</span>'
1585
+ : "";
1586
+ const symBadges = syms.map(function (s) {
1587
+ return '<span class="fc-util-sym" data-sym="' + s +
1588
+ '" style="display:inline-block;background:rgba(251,191,36,.15);border:1px solid rgba(251,191,36,.35);' +
1589
+ 'padding:1px 6px;border-radius:3px;margin:2px 4px 2px 0;font-size:11px;font-family:monospace;cursor:pointer;">' +
1590
+ s + '</span>';
1591
+ }).join("");
1592
+ const detail = syms.length
1593
+ ? '<div style="margin:4px 0 0 0;">' + symBadges + '</div>'
1594
+ : '<div style="margin:4px 0 0 0;font-size:11px;opacity:.5;">(no top-level symbols detected)</div>';
1595
+ const searchKey = (n + " " + syms.join(" ")).toLowerCase();
1596
+
1597
+ html +=
1598
+ '<div class="fc-util-item" data-name="' + n + '" data-search="' + searchKey + '" ' +
1599
+ 'style="border-bottom:1px solid rgba(128,128,128,.1);">' +
1600
+ '<div style="display:flex;align-items:center;padding:4px 6px;cursor:pointer;" class="fc-util-row">' +
1601
+ '<i class="fa fa-caret-right fc-util-arrow" style="width:14px;opacity:.4;font-size:12px;transition:transform .15s;"></i>' +
1602
+ '<i class="fa fa-wrench" style="width:14px;color:#fbbf24;font-size:11px;"></i>' +
1603
+ '<span style="font-weight:600;font-size:12px;flex:1;margin-left:4px;">' + n + '</span>' +
1604
+ errBadge +
1605
+ '</div>' +
1606
+ '<div class="fc-util-detail" style="display:none;padding:0 6px 6px 32px;">' + detail + '</div>' +
1607
+ '</div>';
1608
+ });
1609
+ html += '</div></div>';
1610
+
1611
+ const $dlg = $("<div></div>").html(html).dialog({
1612
+ title: "Utilities",
1613
+ modal: true,
1614
+ width: 420,
1615
+ buttons: [
1616
+ { text: "Close", click: function () { $(this).dialog("close"); } },
1617
+ ],
1618
+ close: function () { $(this).remove(); },
1619
+ });
1620
+
1621
+ $dlg.find("#fc-util-search").on("input", function () {
1622
+ const q = $(this).val().toLowerCase();
1623
+ $dlg.find(".fc-util-item").each(function () {
1624
+ $(this).toggle($(this).data("search").indexOf(q) !== -1);
1625
+ });
1626
+ }).focus();
1627
+
1628
+ $dlg.on("mouseenter", ".fc-util-row", function () {
1629
+ $(this).css("background", "rgba(251,191,36,.08)");
1630
+ }).on("mouseleave", ".fc-util-row", function () {
1631
+ $(this).css("background", "");
1632
+ });
1633
+
1634
+ $dlg.on("click", ".fc-util-row", function (e) {
1635
+ if ($(e.target).closest(".fc-util-sym").length) return;
1636
+ const $item = $(this).closest(".fc-util-item");
1637
+ const $detail = $item.find(".fc-util-detail");
1638
+ const $arrow = $item.find(".fc-util-arrow");
1639
+ const open = $detail.is(":visible");
1640
+ $detail.slideToggle(100);
1641
+ $arrow.css("transform", open ? "" : "rotate(90deg)");
1642
+ });
1643
+
1644
+ // Click symbol → insert bare identifier at cursor
1645
+ $dlg.on("click", ".fc-util-sym", function (e) {
1646
+ e.stopPropagation();
1647
+ const sym = $(this).data("sym");
1648
+ $dlg.dialog("close");
1649
+ if (editorInstance) {
1650
+ const sel = editorInstance.getSelection();
1651
+ const startLine = sel.startLineNumber;
1652
+ const startCol = sel.startColumn;
1653
+ editorInstance.executeEdits("fc-utilities", [{
1654
+ range: sel,
1655
+ text: sym,
1656
+ }]);
1657
+ setTimeout(function () {
1658
+ editorInstance.setPosition({ lineNumber: startLine, column: startCol + sym.length });
1659
+ editorInstance.focus();
1660
+ }, 50);
1661
+ } else {
1662
+ const ta = $("#fc-fallback")[0];
1663
+ const s = ta.selectionStart, en = ta.selectionEnd, v = ta.value;
1664
+ ta.value = v.slice(0, s) + sym + v.slice(en);
1665
+ setTimeout(function () {
1666
+ ta.selectionStart = ta.selectionEnd = s + sym.length;
1667
+ ta.focus();
1668
+ }, 50);
1669
+ }
1670
+ });
1671
+ });
1672
+ });
1301
1673
  },
1302
1674
 
1303
1675
  oneditsave: function () {
@@ -1309,24 +1681,24 @@
1309
1681
  this.subPath = ($("#node-input-subPath").val() || "").trim();
1310
1682
 
1311
1683
  // Collect libs from editableList
1312
- var libs = [];
1313
- var items = $("#node-input-libs-container").editableList("items");
1684
+ const libs = [];
1685
+ const items = $("#node-input-libs-container").editableList("items");
1314
1686
  items.each(function() {
1315
- var mod = $(this).data("mod").val().trim();
1316
- var v = $(this).data("var").val().trim();
1687
+ const mod = $(this).data("mod").val().trim();
1688
+ const v = $(this).data("var").val().trim();
1317
1689
  if (mod) {
1318
1690
  libs.push({ module: mod, var: v });
1319
1691
  }
1320
1692
  });
1321
1693
  this.libs = libs;
1322
1694
 
1323
- var code = editorInstance
1695
+ const code = editorInstance
1324
1696
  ? editorInstance.getValue()
1325
1697
  : $("#fc-fallback").val();
1326
1698
  $("#node-input-componentCode").val(code);
1327
1699
  this.componentCode = code;
1328
1700
 
1329
- var head = headEditorInstance
1701
+ const head = headEditorInstance
1330
1702
  ? headEditorInstance.getValue()
1331
1703
  : $("#fc-head-fallback").val();
1332
1704
  $("#node-input-customHead").val(head);
@@ -1363,11 +1735,11 @@
1363
1735
  },
1364
1736
 
1365
1737
  oneditresize: function (size) {
1366
- var tabsH = $("#fc-tabs").outerHeight(true) || 0;
1367
- var rows = $(
1738
+ const tabsH = $("#fc-tabs").outerHeight(true) || 0;
1739
+ const rows = $(
1368
1740
  "#dialog-form>div:not(#fc-tabs-content):not(:has(#fc-tabs))",
1369
1741
  );
1370
- var h = size.height;
1742
+ let h = size.height;
1371
1743
  rows.each(function () {
1372
1744
  h -= $(this).outerHeight(true);
1373
1745
  });
@@ -1504,8 +1876,8 @@ const { data, send, user, portalClient } = useNodeRed();
1504
1876
  <script type="text/javascript">
1505
1877
  (function () {
1506
1878
  // Resolve httpNodeRoot for public URL prefix
1507
- var nodeRoot = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
1508
- var publicBase = nodeRoot + "/fromcubes/public/";
1879
+ const nodeRoot = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
1880
+ const publicBase = nodeRoot + "/fromcubes/public/";
1509
1881
 
1510
1882
  function formatSize(bytes) {
1511
1883
  if (bytes < 1024) return bytes + " B";
@@ -1513,15 +1885,15 @@ const { data, send, user, portalClient } = useNodeRed();
1513
1885
  return (bytes / (1024 * 1024)).toFixed(1) + " MB";
1514
1886
  }
1515
1887
 
1516
- var allEntries = [];
1517
- var collapsed = {};
1888
+ let allEntries = [];
1889
+ const collapsed = {};
1518
1890
 
1519
- var content = $('<div class="red-ui-sidebar-info" style="height:100%;overflow:auto;padding:0;"></div>');
1520
- var fileList = $('<div style="padding:0;"></div>').appendTo(content);
1891
+ const content = $('<div class="red-ui-sidebar-info" style="height:100%;overflow:auto;padding:0;"></div>');
1892
+ const fileList = $('<div style="padding:0;"></div>').appendTo(content);
1521
1893
 
1522
1894
  // ── Toolbar ──
1523
- var toolbar = $('<div style="display:flex;align-items:center;gap:6px;margin:0 6px;padding:2px 0 0;"></div>');
1524
- var fileInput = $('<input type="file" multiple style="display:none;">').appendTo(toolbar);
1895
+ const toolbar = $('<div style="display:flex;align-items:center;gap:6px;margin:0 6px;padding:2px 0 0;"></div>');
1896
+ const fileInput = $('<input type="file" multiple style="display:none;">').appendTo(toolbar);
1525
1897
  $('<button class="red-ui-button red-ui-button-small" style="flex-shrink:0;"><i class="fa fa-upload"></i> Upload</button>')
1526
1898
  .on("click", function (e) { e.preventDefault(); fileInput.trigger("click"); })
1527
1899
  .appendTo(toolbar);
@@ -1531,7 +1903,7 @@ const { data, send, user, portalClient } = useNodeRed();
1531
1903
 
1532
1904
  // ── Helpers ──
1533
1905
  function pathExists(p) {
1534
- for (var i = 0; i < allEntries.length; i++) {
1906
+ for (let i = 0; i < allEntries.length; i++) {
1535
1907
  if (allEntries[i].name === p) return true;
1536
1908
  }
1537
1909
  return false;
@@ -1539,10 +1911,10 @@ const { data, send, user, portalClient } = useNodeRed();
1539
1911
 
1540
1912
  function uploadFiles(files, targetDir) {
1541
1913
  if (!files || files.length === 0) return;
1542
- var toUpload = [];
1543
- var duplicates = [];
1544
- for (var i = 0; i < files.length; i++) {
1545
- var uploadPath = targetDir ? targetDir + "/" + files[i].name : files[i].name;
1914
+ let toUpload = [];
1915
+ const duplicates = [];
1916
+ for (let i = 0; i < files.length; i++) {
1917
+ const uploadPath = targetDir ? targetDir + "/" + files[i].name : files[i].name;
1546
1918
  if (pathExists(uploadPath)) {
1547
1919
  duplicates.push({ file: files[i], path: uploadPath });
1548
1920
  } else {
@@ -1550,15 +1922,15 @@ const { data, send, user, portalClient } = useNodeRed();
1550
1922
  }
1551
1923
  }
1552
1924
  if (duplicates.length > 0) {
1553
- var names = duplicates.map(function (d) { return d.file.name; }).join(", ");
1925
+ const names = duplicates.map(function (d) { return d.file.name; }).join(", ");
1554
1926
  if (confirm("These files already exist: " + names + "\nOverwrite?")) {
1555
1927
  toUpload = toUpload.concat(duplicates);
1556
1928
  }
1557
1929
  }
1558
1930
  if (toUpload.length === 0) return;
1559
- var pending = toUpload.length;
1931
+ let pending = toUpload.length;
1560
1932
  toUpload.forEach(function (item) {
1561
- var reader = new FileReader();
1933
+ const reader = new FileReader();
1562
1934
  reader.onload = function () {
1563
1935
  $.ajax({
1564
1936
  type: "POST",
@@ -1598,15 +1970,15 @@ const { data, send, user, portalClient } = useNodeRed();
1598
1970
  e.preventDefault();
1599
1971
  e.stopPropagation();
1600
1972
  content.css("background", "");
1601
- var dt = e.originalEvent.dataTransfer;
1973
+ const dt = e.originalEvent.dataTransfer;
1602
1974
  if (dt.files && dt.files.length > 0) {
1603
1975
  uploadFiles(dt.files, "");
1604
1976
  }
1605
1977
  });
1606
1978
 
1607
1979
  function moveItem(fromPath, toDir) {
1608
- var filename = fromPath.split("/").pop();
1609
- var newPath = toDir ? toDir + "/" + filename : filename;
1980
+ const filename = fromPath.split("/").pop();
1981
+ const newPath = toDir ? toDir + "/" + filename : filename;
1610
1982
  if (fromPath === newPath) return;
1611
1983
  if (pathExists(newPath)) {
1612
1984
  if (!confirm("'" + filename + "' already exists in this folder. Overwrite?")) return;
@@ -1617,7 +1989,7 @@ const { data, send, user, portalClient } = useNodeRed();
1617
1989
  data: JSON.stringify({ from: fromPath, to: newPath }),
1618
1990
  success: function () { refreshList(); },
1619
1991
  error: function (xhr) {
1620
- var msg = xhr.responseJSON ? xhr.responseJSON.error : "move failed";
1992
+ const msg = xhr.responseJSON ? xhr.responseJSON.error : "move failed";
1621
1993
  RED.notify("Move failed: " + msg, "error");
1622
1994
  },
1623
1995
  });
@@ -1625,22 +1997,22 @@ const { data, send, user, portalClient } = useNodeRed();
1625
1997
 
1626
1998
  // ── Inline new-folder input ──
1627
1999
  function showNewFolderInput(parentDir) {
1628
- var existingInput = fileList.find(".fc-new-folder-row");
2000
+ const existingInput = fileList.find(".fc-new-folder-row");
1629
2001
  if (existingInput.length) existingInput.remove();
1630
2002
 
1631
- var depth = parentDir ? parentDir.split("/").length : 0;
1632
- var indent = 8 + depth * 18;
1633
- var row = $('<div class="fc-new-folder-row" style="display:flex;align-items:center;gap:6px;padding:8px;border-bottom:1px solid var(--red-ui-secondary-border-color);background:var(--red-ui-tertiary-background);"></div>');
2003
+ const depth = parentDir ? parentDir.split("/").length : 0;
2004
+ const indent = 8 + depth * 18;
2005
+ const row = $('<div class="fc-new-folder-row" style="display:flex;align-items:center;gap:6px;padding:8px;border-bottom:1px solid var(--red-ui-secondary-border-color);background:var(--red-ui-tertiary-background);"></div>');
1634
2006
  row.css("padding-left", indent + "px");
1635
2007
  $('<i class="fa fa-folder" style="color:#fbbf24;font-size:13px;width:16px;text-align:center;"></i>').appendTo(row);
1636
- var inp = $('<input type="text" placeholder="folder name" class="red-ui-searchBox-input" style="flex:1;padding:3px 6px;font-size:12px;">');
2008
+ const inp = $('<input type="text" placeholder="folder name" class="red-ui-searchBox-input" style="flex:1;padding:3px 6px;font-size:12px;">');
1637
2009
  inp.appendTo(row);
1638
- var okBtn = $('<button class="red-ui-button red-ui-button-small" style="color:var(--red-ui-text-color-success);" title="Create"><i class="fa fa-check"></i></button>');
1639
- var cancelBtn = $('<button class="red-ui-button red-ui-button-small" title="Cancel"><i class="fa fa-times"></i></button>');
2010
+ const okBtn = $('<button class="red-ui-button red-ui-button-small" style="color:var(--red-ui-text-color-success);" title="Create"><i class="fa fa-check"></i></button>');
2011
+ const cancelBtn = $('<button class="red-ui-button red-ui-button-small" title="Cancel"><i class="fa fa-times"></i></button>');
1640
2012
  function submit() {
1641
- var name = inp.val().trim();
2013
+ const name = inp.val().trim();
1642
2014
  if (!name) { row.remove(); return; }
1643
- var p = parentDir ? parentDir + "/" + name : name;
2015
+ const p = parentDir ? parentDir + "/" + name : name;
1644
2016
  if (pathExists(p)) {
1645
2017
  RED.notify("Folder '" + name + "' already exists", "warning");
1646
2018
  return;
@@ -1661,7 +2033,7 @@ const { data, send, user, portalClient } = useNodeRed();
1661
2033
 
1662
2034
  // Insert after the parent folder row, or at top
1663
2035
  if (parentDir) {
1664
- var parentRow = fileList.find('[data-path="' + parentDir + '"]');
2036
+ const parentRow = fileList.find('[data-path="' + parentDir + '"]');
1665
2037
  if (parentRow.length) { row.insertAfter(parentRow); } else { fileList.prepend(row); }
1666
2038
  } else {
1667
2039
  fileList.prepend(row);
@@ -1670,7 +2042,7 @@ const { data, send, user, portalClient } = useNodeRed();
1670
2042
  }
1671
2043
 
1672
2044
  // ── Context menu (native Node-RED style) ──
1673
- var activeMenu = null;
2045
+ let activeMenu = null;
1674
2046
  function closeMenu() {
1675
2047
  if (activeMenu) { activeMenu.remove(); activeMenu = null; }
1676
2048
  }
@@ -1678,14 +2050,14 @@ const { data, send, user, portalClient } = useNodeRed();
1678
2050
 
1679
2051
  function showMenu(anchor, items) {
1680
2052
  closeMenu();
1681
- var menu = $('<ul class="red-ui-menu red-ui-menu-dropdown" style="position:absolute;z-index:10000;display:block;"></ul>');
2053
+ const menu = $('<ul class="red-ui-menu red-ui-menu-dropdown" style="position:absolute;z-index:10000;display:block;"></ul>');
1682
2054
  items.forEach(function (item) {
1683
2055
  if (item.divider) {
1684
2056
  menu.append('<li class="red-ui-menu-divider"></li>');
1685
2057
  return;
1686
2058
  }
1687
- var li = $('<li></li>');
1688
- var a = $('<a href="#"></a>');
2059
+ const li = $('<li></li>');
2060
+ const a = $('<a href="#"></a>');
1689
2061
  if (item.danger) a.css("color", "var(--red-ui-text-color-error)");
1690
2062
  a.append('<i class="fa ' + item.icon + '" style="width:18px;text-align:center;"></i> ');
1691
2063
  a.append($('<span class="red-ui-menu-label"></span>').text(item.label));
@@ -1703,32 +2075,32 @@ const { data, send, user, portalClient } = useNodeRed();
1703
2075
  activeMenu = menu;
1704
2076
 
1705
2077
  // Position near the anchor
1706
- var off = anchor.offset();
1707
- var top = off.top + anchor.outerHeight() + 2;
1708
- var left = off.left - menu.outerWidth() + anchor.outerWidth();
2078
+ const off = anchor.offset();
2079
+ let top = off.top + anchor.outerHeight() + 2;
2080
+ let left = off.left - menu.outerWidth() + anchor.outerWidth();
1709
2081
  if (left < 0) left = off.left;
1710
2082
  if (top + menu.outerHeight() > $(window).height()) top = off.top - menu.outerHeight() - 2;
1711
2083
  menu.css({ top: top, left: left });
1712
2084
  }
1713
2085
 
1714
- var adminRoot = (RED.settings.httpAdminRoot || "/").replace(/\/$/, "");
2086
+ const adminRoot = (RED.settings.httpAdminRoot || "/").replace(/\/$/, "");
1715
2087
 
1716
2088
  function showRenameInput(rowEl, fullPath, currentName) {
1717
- var nameSpan = rowEl.find("span").filter(function () { return $(this).text() === currentName; }).first();
2089
+ const nameSpan = rowEl.find("span").filter(function () { return $(this).text() === currentName; }).first();
1718
2090
  if (!nameSpan.length) return;
1719
- var origText = nameSpan.text();
1720
- var inp = $('<input type="text" class="red-ui-searchBox-input" style="flex:1;padding:3px 6px;font-size:12px;">').val(origText);
1721
- var okBtn = $('<button class="red-ui-button red-ui-button-small" style="color:var(--red-ui-text-color-success);" title="Save"><i class="fa fa-check"></i></button>');
1722
- var cancelBtn = $('<button class="red-ui-button red-ui-button-small" title="Cancel"><i class="fa fa-times"></i></button>');
1723
- var dotIdx = origText.lastIndexOf(".");
1724
- var hasExt = dotIdx > 0; // has extension (not hidden file)
1725
- var origExt = hasExt ? origText.slice(dotIdx) : "";
2091
+ const origText = nameSpan.text();
2092
+ const inp = $('<input type="text" class="red-ui-searchBox-input" style="flex:1;padding:3px 6px;font-size:12px;">').val(origText);
2093
+ const okBtn = $('<button class="red-ui-button red-ui-button-small" style="color:var(--red-ui-text-color-success);" title="Save"><i class="fa fa-check"></i></button>');
2094
+ const cancelBtn = $('<button class="red-ui-button red-ui-button-small" title="Cancel"><i class="fa fa-times"></i></button>');
2095
+ const dotIdx = origText.lastIndexOf(".");
2096
+ const hasExt = dotIdx > 0; // has extension (not hidden file)
2097
+ const origExt = hasExt ? origText.slice(dotIdx) : "";
1726
2098
 
1727
2099
  nameSpan.replaceWith(inp);
1728
2100
  inp.after(cancelBtn).after(okBtn);
1729
2101
  inp.focus();
1730
2102
  // Select only the name part before extension
1731
- var el = inp[0];
2103
+ const el = inp[0];
1732
2104
  if (hasExt && el.setSelectionRange) {
1733
2105
  el.setSelectionRange(0, dotIdx);
1734
2106
  } else {
@@ -1741,22 +2113,22 @@ const { data, send, user, portalClient } = useNodeRed();
1741
2113
  inp.replaceWith($('<span style="flex:1;font-size:12px;word-break:break-all;"></span>').text(origText));
1742
2114
  }
1743
2115
  function submit() {
1744
- var newName = inp.val().trim();
2116
+ const newName = inp.val().trim();
1745
2117
  if (!newName) {
1746
2118
  RED.notify("Name cannot be empty", "warning");
1747
2119
  return;
1748
2120
  }
1749
2121
  // Warn if extension changed or removed
1750
2122
  if (hasExt) {
1751
- var newDot = newName.lastIndexOf(".");
1752
- var newExt = newDot > 0 ? newName.slice(newDot) : "";
2123
+ const newDot = newName.lastIndexOf(".");
2124
+ const newExt = newDot > 0 ? newName.slice(newDot) : "";
1753
2125
  if (newExt.toLowerCase() !== origExt.toLowerCase()) {
1754
2126
  if (!confirm("Extension changed from '" + origExt + "' to '" + (newExt || "none") + "'. Continue?")) return;
1755
2127
  }
1756
2128
  }
1757
2129
  if (newName === origText) { restore(); return; }
1758
- var parentDir = fullPath.indexOf("/") >= 0 ? fullPath.slice(0, fullPath.lastIndexOf("/")) : "";
1759
- var newPath = parentDir ? parentDir + "/" + newName : newName;
2130
+ const parentDir = fullPath.indexOf("/") >= 0 ? fullPath.slice(0, fullPath.lastIndexOf("/")) : "";
2131
+ const newPath = parentDir ? parentDir + "/" + newName : newName;
1760
2132
  if (pathExists(newPath)) {
1761
2133
  RED.notify("'" + newName + "' already exists", "warning");
1762
2134
  return;
@@ -1767,7 +2139,7 @@ const { data, send, user, portalClient } = useNodeRed();
1767
2139
  data: JSON.stringify({ from: fullPath, to: newPath }),
1768
2140
  success: function () { refreshList(); },
1769
2141
  error: function (xhr) {
1770
- var msg = xhr.responseJSON ? xhr.responseJSON.error : "rename failed";
2142
+ const msg = xhr.responseJSON ? xhr.responseJSON.error : "rename failed";
1771
2143
  RED.notify("Rename failed: " + msg, "error");
1772
2144
  restore();
1773
2145
  },
@@ -1784,11 +2156,11 @@ const { data, send, user, portalClient } = useNodeRed();
1784
2156
  // ── Tree rendering ──
1785
2157
  function buildTree(entries) {
1786
2158
  // Build nested structure: { children: { name: { type, children, entry } } }
1787
- var root = { children: {} };
2159
+ const root = { children: {} };
1788
2160
  entries.forEach(function (e) {
1789
- var parts = e.name.split("/");
1790
- var node = root;
1791
- for (var i = 0; i < parts.length; i++) {
2161
+ const parts = e.name.split("/");
2162
+ let node = root;
2163
+ for (let i = 0; i < parts.length; i++) {
1792
2164
  if (!node.children[parts[i]]) {
1793
2165
  node.children[parts[i]] = { children: {} };
1794
2166
  }
@@ -1799,15 +2171,15 @@ const { data, send, user, portalClient } = useNodeRed();
1799
2171
  return root;
1800
2172
  }
1801
2173
 
1802
- var ROOT_KEY = "__root__";
2174
+ const ROOT_KEY = "__root__";
1803
2175
 
1804
2176
  function renderTree() {
1805
2177
  fileList.empty();
1806
- var isOpen = !collapsed[ROOT_KEY];
2178
+ const isOpen = !collapsed[ROOT_KEY];
1807
2179
 
1808
2180
  // Root folder row — always visible
1809
- var rootRow = $('<div style="display:flex;align-items:center;gap:5px;padding:4px 8px;border-bottom:1px solid var(--red-ui-secondary-border-color);"></div>');
1810
- var rootArrow = $('<i class="fa ' + (isOpen ? 'fa-caret-down' : 'fa-caret-right') + '" style="color:var(--red-ui-secondary-text-color);font-size:11px;width:10px;text-align:center;cursor:pointer;"></i>');
2181
+ const rootRow = $('<div style="display:flex;align-items:center;gap:5px;padding:4px 8px;border-bottom:1px solid var(--red-ui-secondary-border-color);"></div>');
2182
+ const rootArrow = $('<i class="fa ' + (isOpen ? 'fa-caret-down' : 'fa-caret-right') + '" style="color:var(--red-ui-secondary-text-color);font-size:11px;width:10px;text-align:center;cursor:pointer;"></i>');
1811
2183
  rootArrow.on("click", function () { collapsed[ROOT_KEY] = isOpen; renderTree(); });
1812
2184
  rootRow.append(rootArrow);
1813
2185
  $('<i class="fa ' + (isOpen ? 'fa-folder-open' : 'fa-folder') + '" style="color:#fbbf24;font-size:12px;width:16px;text-align:center;"></i>').appendTo(rootRow);
@@ -1822,35 +2194,35 @@ const { data, send, user, portalClient } = useNodeRed();
1822
2194
  fileList.append($('<div style="padding:16px;color:var(--red-ui-secondary-text-color);text-align:center;">No files uploaded.<br><span style="font-size:11px;">Drop files here or click Upload.</span></div>'));
1823
2195
  return;
1824
2196
  }
1825
- var tree = buildTree(allEntries);
2197
+ const tree = buildTree(allEntries);
1826
2198
  renderNode(tree, "", 1);
1827
2199
  }
1828
2200
 
1829
2201
  function renderNode(node, parentPath, depth) {
1830
2202
  // Collect and sort: dirs first, then files
1831
- var names = Object.keys(node.children).sort(function (a, b) {
1832
- var aIsDir = node.children[a].entry && node.children[a].entry.type === "dir";
1833
- var bIsDir = node.children[b].entry && node.children[b].entry.type === "dir";
2203
+ const names = Object.keys(node.children).sort(function (a, b) {
2204
+ const aIsDir = node.children[a].entry && node.children[a].entry.type === "dir";
2205
+ const bIsDir = node.children[b].entry && node.children[b].entry.type === "dir";
1834
2206
  if (aIsDir && !bIsDir) return -1;
1835
2207
  if (!aIsDir && bIsDir) return 1;
1836
2208
  return a.localeCompare(b);
1837
2209
  });
1838
2210
 
1839
2211
  names.forEach(function (name) {
1840
- var child = node.children[name];
1841
- var e = child.entry;
2212
+ const child = node.children[name];
2213
+ const e = child.entry;
1842
2214
  if (!e) return;
1843
- var fullPath = e.name;
1844
- var indent = 8 + depth * 18;
1845
- var isDir = e.type === "dir";
1846
- var isOpen = !collapsed[fullPath];
2215
+ const fullPath = e.name;
2216
+ const indent = 8 + depth * 18;
2217
+ const isDir = e.type === "dir";
2218
+ const isOpen = !collapsed[fullPath];
1847
2219
 
1848
- var row = $('<div data-path="' + fullPath + '" style="display:flex;align-items:center;gap:5px;padding:4px 8px;border-bottom:1px solid var(--red-ui-secondary-border-color);"></div>');
2220
+ const row = $('<div data-path="' + fullPath + '" style="display:flex;align-items:center;gap:5px;padding:4px 8px;border-bottom:1px solid var(--red-ui-secondary-border-color);"></div>');
1849
2221
  row.css("padding-left", indent + "px");
1850
2222
 
1851
2223
  if (isDir) {
1852
2224
  // Expand/collapse arrow
1853
- var arrow = $('<i class="fa ' + (isOpen ? 'fa-caret-down' : 'fa-caret-right') + '" style="color:var(--red-ui-secondary-text-color);font-size:11px;width:10px;text-align:center;cursor:pointer;"></i>');
2225
+ const arrow = $('<i class="fa ' + (isOpen ? 'fa-caret-down' : 'fa-caret-right') + '" style="color:var(--red-ui-secondary-text-color);font-size:11px;width:10px;text-align:center;cursor:pointer;"></i>');
1854
2226
  arrow.on("click", function (e) {
1855
2227
  e.stopPropagation();
1856
2228
  collapsed[fullPath] = isOpen;
@@ -1863,7 +2235,7 @@ const { data, send, user, portalClient } = useNodeRed();
1863
2235
  .appendTo(row);
1864
2236
 
1865
2237
  // Context menu trigger
1866
- var dirMenuBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;"><i class="fa fa-ellipsis-v"></i></button>');
2238
+ const dirMenuBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;"><i class="fa fa-ellipsis-v"></i></button>');
1867
2239
  (function (fp, nm) {
1868
2240
  dirMenuBtn.on("click", function (ev) {
1869
2241
  ev.preventDefault();
@@ -1902,7 +2274,7 @@ const { data, send, user, portalClient } = useNodeRed();
1902
2274
  ev.preventDefault();
1903
2275
  ev.stopPropagation();
1904
2276
  row.css("background", "");
1905
- var srcPath = ev.originalEvent.dataTransfer.getData("text/x-asset-path");
2277
+ const srcPath = ev.originalEvent.dataTransfer.getData("text/x-asset-path");
1906
2278
  if (srcPath) {
1907
2279
  moveItem(srcPath, fullPath);
1908
2280
  } else if (ev.originalEvent.dataTransfer.files && ev.originalEvent.dataTransfer.files.length > 0) {
@@ -1930,13 +2302,13 @@ const { data, send, user, portalClient } = useNodeRed();
1930
2302
  });
1931
2303
  row.on("dragend", function () { row.css("opacity", "1"); });
1932
2304
 
1933
- var copyBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;" title="Copy public path"><i class="fa fa-clipboard"></i></button>');
1934
- var fileMenuBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;"><i class="fa fa-ellipsis-v"></i></button>');
2305
+ const copyBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;" title="Copy public path"><i class="fa fa-clipboard"></i></button>');
2306
+ const fileMenuBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;"><i class="fa fa-ellipsis-v"></i></button>');
1935
2307
  (function (fp, nm) {
1936
2308
  copyBtn.on("click", function (ev) {
1937
2309
  ev.preventDefault();
1938
2310
  ev.stopPropagation();
1939
- var url = publicBase + fp;
2311
+ const url = publicBase + fp;
1940
2312
  navigator.clipboard.writeText(url).then(function () {
1941
2313
  RED.notify("Copied: " + url, { type: "success", timeout: 2000 });
1942
2314
  });
@@ -1949,7 +2321,7 @@ const { data, send, user, portalClient } = useNodeRed();
1949
2321
  showRenameInput(row, fp, nm);
1950
2322
  }},
1951
2323
  { icon: "fa-download", label: "Download", action: function () {
1952
- var a = document.createElement("a");
2324
+ const a = document.createElement("a");
1953
2325
  a.href = adminRoot + "/portal-react/assets/download/" + fp.split("/").map(encodeURIComponent).join("/");
1954
2326
  a.download = nm;
1955
2327
  document.body.appendChild(a);