@namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared @using Microsoft.Extensions.Logging @implements IAsyncDisposable @inject IJSRuntime JS @inject Microsoft.Extensions.Logging.ILogger Logger @if (ShowToolbar) {
}
@code { [Parameter] public string Value { get; set; } = ""; [Parameter] public EventCallback ValueChanged { get; set; } [Parameter] public string Language { get; set; } = "csharp"; [Parameter] public string Height { get; set; } = "320px"; [Parameter] public bool ReadOnly { get; set; } = false; [Parameter] public bool ShowToolbar { get; set; } = true; /// /// Owning-equipment context for equipment-relative {{equip}}/<RefName> completion + /// diagnostics. Set on the per-equipment VirtualTag / ScriptedAlarm modals; left null on the shared /// ScriptEdit page (where the token cannot resolve to a single equipment). /// [Parameter] public string? EquipmentId { get; set; } /// /// Fires whenever Monaco's marker set updates (after the 500 ms diagnostic /// debounce). The marker DTO is not modelled yet — typed as object[] until a /// later task wires the Roslyn-backed diagnostics. /// [Parameter] public EventCallback MarkersChanged { get; set; } private ElementReference _hostRef; private DotNetObjectReference? _dotNetRef; private readonly string _id = Guid.NewGuid().ToString("N"); private string _lastSentValue = ""; private bool _initialized; private bool _wrap; private bool _minimap; private bool _dark; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _dotNetRef = DotNetObjectReference.Create(this); _lastSentValue = Value ?? ""; try { await JS.InvokeVoidAsync( "MonacoBlazor.createEditor", _id, _hostRef, new { value = Value ?? "", language = Language, readOnly = ReadOnly, equipmentId = EquipmentId }, _dotNetRef); _initialized = true; } catch (InvalidOperationException) { // Prerendering: JS interop is not available yet — the next // (interactive) render retries. Expected, not logged. } catch (JSDisconnectedException) { // Circuit disconnected before init completed — nothing to do. } catch (JSException ex) { // A genuine Monaco init failure — surface it instead of hiding it. Logger.LogError(ex, "Monaco editor {EditorId} failed to initialize.", _id); } } else if (_initialized && (Value ?? "") != _lastSentValue) { _lastSentValue = Value ?? ""; await SafeInvokeAsync("MonacoBlazor.setValue", "set editor value", _id, _lastSentValue); } } /// /// Invokes a Monaco JS function, swallowing the expected disconnect case but /// logging any genuine JS error so failures are not silent. /// private async ValueTask SafeInvokeAsync(string fn, string action, params object?[] args) { try { await JS.InvokeVoidAsync(fn, args); } catch (JSDisconnectedException) { // Circuit gone — the editor no longer exists; nothing to log. } catch (JSException ex) { Logger.LogWarning(ex, "Monaco editor {EditorId}: failed to {Action}.", _id, action); } } [JSInvokable] public Task OnValueChanged(string newValue) { var normalized = newValue ?? ""; if (normalized == _lastSentValue) return Task.CompletedTask; _lastSentValue = normalized; return ValueChanged.InvokeAsync(_lastSentValue); } [JSInvokable] public Task OnMarkersChanged(object[] markers) => MarkersChanged.InvokeAsync(markers ?? Array.Empty()); /// Programmatic scroll-to-line (called by the problems panel). public async Task RevealLineAsync(int line, int column = 1) { if (!_initialized) return; await SafeInvokeAsync("MonacoBlazor.revealLine", "reveal line", _id, line, column); } private async Task FormatAsync() { if (!_initialized) return; await SafeInvokeAsync("MonacoBlazor.format", "format document", _id); } private async Task ToggleWrap() { _wrap = !_wrap; await SafeInvokeAsync("MonacoBlazor.setEditorOption", "toggle word wrap", _id, "wordWrap", _wrap ? "on" : "off"); } private async Task ToggleMinimap() { _minimap = !_minimap; await SafeInvokeAsync("MonacoBlazor.setEditorOption", "toggle minimap", _id, "minimap", new { enabled = _minimap }); } private async Task ToggleTheme() { _dark = !_dark; await SafeInvokeAsync("MonacoBlazor.setEditorOption", "toggle theme", _id, "theme", _dark ? "vs-dark" : "vs"); } public async ValueTask DisposeAsync() { if (_initialized) { // Disposal commonly races a circuit disconnect — JSDisconnectedException // here is expected and silent; a real JSException is still logged. await SafeInvokeAsync("MonacoBlazor.dispose", "dispose editor", _id); } _dotNetRef?.Dispose(); } }