feat(ui/design): Monaco editor for script code fields

Vendors Monaco 0.55.1 min/vs/ (~15 MB) at
wwwroot/lib/monaco/vs/. No CDN dependency; works on air-gapped
deployments. Loaded lazily on first script-edit via the AMD loader.

wwwroot/js/monaco-init.js exposes window.MonacoBlazor with
createEditor / setValue / getValue / setMarkers / dispose. Handles
loader bootstrap, DotNet round-trip on content change, and marker
sets for later diagnostic wiring.

Components/Shared/MonacoEditor.razor is a Blazor wrapper with
Value / ValueChanged / Language / Height / ReadOnly parameters and
IAsyncDisposable teardown. Bidirectional binding tracks
_lastSentValue to avoid push/pull loops.

Replaces the plain textareas in SharedScriptForm, TemplateEdit's
Add-Script form, and ApiMethodForm. Default height 320px ≈ the
previous rows=10. Build / tests / dialog flow unaffected.

Wave 1 of three. Roslyn-backed completions and SCADA-specific
extensions follow in subsequent commits.
This commit is contained in:
Joseph Doherty
2026-05-12 04:34:41 -04:00
parent e667ea2b50
commit 7f01c5547a
127 changed files with 71464 additions and 5 deletions
@@ -0,0 +1,91 @@
// Blazor bridge for Monaco editor.
// Exposes window.MonacoBlazor with createEditor / setValue / getValue / dispose / setMarkers.
// Lazy-loads Monaco's AMD bundle the first time createEditor is called.
(function () {
const VS_BASE = "/_content/ScadaLink.CentralUI/lib/monaco/vs";
const editors = {};
let readyPromise = null;
function ensureLoaded() {
if (readyPromise) return readyPromise;
readyPromise = new Promise(function (resolve, reject) {
const loader = document.createElement("script");
loader.src = VS_BASE + "/loader.js";
loader.onload = function () {
// eslint-disable-next-line no-undef
require.config({ paths: { vs: VS_BASE } });
// eslint-disable-next-line no-undef
require(["vs/editor/editor.main"], function () {
resolve();
});
};
loader.onerror = function (e) { reject(e); };
document.head.appendChild(loader);
});
return readyPromise;
}
async function createEditor(id, host, options, dotNetRef) {
await ensureLoaded();
if (!host) return;
const editor = monaco.editor.create(host, {
value: options.value || "",
language: options.language || "csharp",
theme: "vs",
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
fontSize: 13,
lineNumbers: "on",
renderLineHighlight: "line",
readOnly: !!options.readOnly,
tabSize: 4,
insertSpaces: true,
wordWrap: "off",
fixedOverflowWidgets: true
});
editor.onDidChangeModelContent(function () {
const value = editor.getValue();
dotNetRef.invokeMethodAsync("OnValueChanged", value).catch(function () {});
});
editors[id] = { editor: editor, dotNetRef: dotNetRef };
}
function setValue(id, value) {
const entry = editors[id];
if (!entry) return;
if (entry.editor.getValue() !== value) {
entry.editor.setValue(value || "");
}
}
function getValue(id) {
const entry = editors[id];
return entry ? entry.editor.getValue() : null;
}
function setMarkers(id, markers) {
const entry = editors[id];
if (!entry) return;
const model = entry.editor.getModel();
if (!model) return;
monaco.editor.setModelMarkers(model, "scadalink", markers || []);
}
function dispose(id) {
const entry = editors[id];
if (!entry) return;
try { entry.editor.dispose(); } catch (e) {}
delete editors[id];
}
window.MonacoBlazor = {
createEditor: createEditor,
setValue: setValue,
getValue: getValue,
setMarkers: setMarkers,
dispose: dispose
};
})();