@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared @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; /// /// Runtime globals surface the script is analyzed against. Defaults to /// template/shared-script globals; set to InboundApi on the API /// method editor so Route and Parameters type-check. /// [Parameter] public ScriptAnalysis.ScriptKind ScriptKind { get; set; } = ScriptAnalysis.ScriptKind.Template; /// /// Parameter names declared on the form (derived from the SchemaBuilder's /// JSON Schema), surfaced as completions inside Parameters["..."] literals /// and used by the unknown-key diagnostic. /// [Parameter] public IReadOnlyList? DeclaredParameters { get; set; } /// /// Full shapes (name + type + required) for the declared parameters. /// Used by Parameters["name"] hover to show the declared type. If null, /// derived from with type "Object". /// [Parameter] public IReadOnlyList? DeclaredParameterShapes { get; set; } /// /// Shapes (name + parameter list + return type) of other scripts on the /// same template. Surfaced inside CallScript("...") for completion, /// signature help, hover, and argument-count diagnostics. /// [Parameter] public IReadOnlyList? SiblingScripts { get; set; } /// /// Attributes declared on the current template. Surfaced inside /// Attributes["..."] for completion and SCADA006 diagnostics. /// [Parameter] public IReadOnlyList? SelfAttributes { get; set; } /// /// Child compositions on the current template, each with its template's /// attributes and scripts. Surfaced for Children["X"].Attributes, /// Children["X"].CallScript, and SCADA007 diagnostics. /// [Parameter] public IReadOnlyList? Children { get; set; } /// /// Parent template when the current template is composed inside exactly /// one other template. null at the root or when multiple parents /// exist. Surfaced for Parent.Attributes / Parent.CallScript. /// [Parameter] public ScriptAnalysis.CompositionContext? Parent { get; set; } /// /// Fires whenever Monaco's marker set updates (after the 500 ms diagnostic /// debounce). Hosts can render a with the same /// data. /// [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 }, _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 (CentralUI-018) 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) { _lastSentValue = newValue ?? ""; return ValueChanged.InvokeAsync(_lastSentValue); } [JSInvokable] public Task OnMarkersChanged(ScriptAnalysis.DiagnosticMarker[] 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); } /// /// Called from JS at completion-request time so the form's latest state is /// passed through, not whatever was captured when the editor was created. /// [JSInvokable] public ScadaContext GetContext() => new( DeclaredParameters?.ToArray() ?? Array.Empty(), SiblingScripts?.ToArray() ?? Array.Empty(), DeclaredParameterShapes?.ToArray() ?? DeclaredParameters?.Select(n => new ScriptAnalysis.ParameterShape(n, "Object", true)).ToArray() ?? Array.Empty(), SelfAttributes?.ToArray() ?? Array.Empty(), Children?.ToArray() ?? Array.Empty(), Parent, ScriptKind); 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(); } public record ScadaContext( string[] DeclaredParameters, ScriptAnalysis.ScriptShape[] SiblingScripts, ScriptAnalysis.ParameterShape[] DeclaredParameterShapes, ScriptAnalysis.AttributeShape[] SelfAttributes, ScriptAnalysis.CompositionContext[] Children, ScriptAnalysis.CompositionContext? Parent, ScriptAnalysis.ScriptKind ScriptKind); }