feat(adminui): ScriptEdit uses MonacoEditor; drop CDN loader

This commit is contained in:
Joseph Doherty
2026-06-09 15:11:13 -04:00
parent 071bed5f94
commit 088fc50ef2
2 changed files with 2 additions and 87 deletions
@@ -13,7 +13,6 @@
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@inject IDbContextFactory<OtOpcUaConfigDbContext> DbFactory
@inject NavigationManager Nav
@inject IJSRuntime JS
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">@(IsNew ? "New script" : "Edit script")</h4>
@@ -57,15 +56,8 @@ else
<section class="panel rise mt-3">
<div class="panel-head">Source</div>
<div style="padding:1rem">
@* The textarea stays in the DOM and remains Blazor's source of truth. Monaco
mounts a <div> beside it (textarea hides), and the loader's onDidChangeModelContent
handler mirrors edits back into the textarea + fires the input event so @bind
picks them up. Falls back to the textarea gracefully if Monaco's CDN is
unreachable (air-gapped deployments — see monaco-loader.js). *@
<InputTextArea id="script-source" @bind-Value="_form.SourceCode"
class="form-control form-control-sm mono" rows="20"
placeholder="// C# expression body" />
<div class="form-text">SHA-256 hash is computed automatically on save. Monaco editor attaches over the textarea on render.</div>
<MonacoEditor @bind-Value="_form.SourceCode" Height="420px" />
<div class="form-text">SHA-256 hash is computed automatically on save.</div>
</div>
</section>
@@ -110,24 +102,6 @@ else
_loaded = true;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender || !_loaded) return;
// Inject loader once, then attach over the textarea. Failures are silent — the page
// is fully usable via the underlying textarea if Monaco's CDN is unreachable.
try
{
await JS.InvokeVoidAsync("eval", "if (!document.querySelector('script[data-otopcua=monaco-loader]')) { var s=document.createElement('script'); s.src='/_content/ZB.MOM.WW.OtOpcUa.AdminUI/js/monaco-loader.js'; s.dataset.otopcua='monaco-loader'; document.head.appendChild(s); }");
// Wait a tick for the loader IIFE to register window.otOpcUaScriptEditor, then attach.
await Task.Delay(50);
await JS.InvokeVoidAsync("otOpcUaScriptEditor.attach", "script-source");
}
catch
{
// Textarea remains the editor — no-op.
}
}
private async Task SubmitAsync()
{
_busy = true; _error = null;
@@ -1,59 +0,0 @@
// Phase 7 Stream F — Monaco editor loader for ScriptEditor.razor.
// Progressive enhancement: the textarea is authoritative until Monaco attaches;
// after attach, Monaco syncs back into the textarea on every change so Blazor's
// @bind still sees the latest value.
(function () {
if (window.otOpcUaScriptEditor) return;
const MONACO_CDN = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs';
let loaderPromise = null;
function ensureLoader() {
if (loaderPromise) return loaderPromise;
loaderPromise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = `${MONACO_CDN}/loader.js`;
script.onload = () => {
window.require.config({ paths: { vs: MONACO_CDN } });
window.require(['vs/editor/editor.main'], () => resolve(window.monaco));
};
script.onerror = () => reject(new Error('Monaco CDN unreachable'));
document.head.appendChild(script);
});
return loaderPromise;
}
window.otOpcUaScriptEditor = {
attach: async function (textareaId) {
const ta = document.getElementById(textareaId);
if (!ta) return;
const monaco = await ensureLoader();
// Mount Monaco over the textarea. The textarea stays in the DOM as the
// source of truth for Blazor's @bind — Monaco mirrors into it on every
// keystroke so server-side state stays in sync.
const host = document.createElement('div');
host.style.height = '340px';
host.style.border = '1px solid #ced4da';
host.style.borderRadius = '0.25rem';
ta.style.display = 'none';
ta.parentNode.insertBefore(host, ta);
const editor = monaco.editor.create(host, {
value: ta.value,
language: 'csharp',
theme: 'vs',
automaticLayout: true,
fontSize: 13,
minimap: { enabled: false },
scrollBeyondLastLine: false,
});
editor.onDidChangeModelContent(() => {
ta.value = editor.getValue();
ta.dispatchEvent(new Event('input', { bubbles: true }));
});
},
};
})();