using System.Diagnostics.CodeAnalysis; using Connected.Interop; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Options; using Microsoft.JSInterop; namespace Connected.Services; public class ResizeObserver : IResizeObserver, IDisposable, IAsyncDisposable { private Boolean _isDisposed = false; private readonly DotNetObjectReference _dotNetRef; private readonly IJSRuntime _jsRuntime; private readonly Dictionary _cachedValueIds = new(); private readonly Dictionary _cachedValues = new(); private Guid _id = Guid.NewGuid(); private ResizeObserverOptions _options; [DynamicDependency(nameof(OnSizeChanged))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SizeChangeUpdateInfo))] public ResizeObserver(IJSRuntime jsRuntime, ResizeObserverOptions options) { _dotNetRef = DotNetObjectReference.Create(this); _jsRuntime = jsRuntime; _options = options; } public ResizeObserver(IJSRuntime jsRuntime, IOptions options = null) : this(jsRuntime, options?.Value ?? new ResizeObserverOptions()) { } public async Task Observe(ElementReference element) => (await Observe(new[] { element })).FirstOrDefault(); public async Task> Observe(IEnumerable elements) { var filteredElements = elements.Where(x => x.Context != null && _cachedValues.ContainsKey(x) == false).ToList(); if (filteredElements.Any() == false) { return Array.Empty(); } List elementIds = new(); foreach (var item in filteredElements) { var id = Guid.NewGuid(); elementIds.Add(id); _cachedValueIds.Add(id, item); } var result = await _jsRuntime.InvokeAsync>("mudResizeObserver.connect", _id, _dotNetRef, filteredElements, elementIds, _options) ?? Array.Empty(); var counter = 0; foreach (var item in result) { _cachedValues.Add(filteredElements.ElementAt(counter), item); counter++; } return result; } public async Task Unobserve(ElementReference element) { var elementId = _cachedValueIds.FirstOrDefault(x => x.Value.Id == element.Id).Key; if (elementId == default) { return; } //if the unobserve happens during a component teardown, the try-catch is a safe guard to prevent a "pseudo" exception try { await _jsRuntime.InvokeVoidAsync($"mudResizeObserver.disconnect", _id, elementId); } catch (Exception) { } _cachedValueIds.Remove(elementId); _cachedValues.Remove(element); } public bool IsElementObserved(ElementReference reference) => _cachedValues.ContainsKey(reference); public record SizeChangeUpdateInfo(Guid Id, BoundingClientRect Size); [JSInvokable] public void OnSizeChanged(IEnumerable changes) { Dictionary parsedChanges = new(); foreach (var item in changes) { if (_cachedValueIds.ContainsKey(item.Id) == false) { continue; } var elementRef = _cachedValueIds[item.Id]; _cachedValues[elementRef] = item.Size; parsedChanges.Add(elementRef, item.Size); } OnResized?.Invoke(parsedChanges); } public event SizeChanged OnResized; public BoundingClientRect GetSizeInfo(ElementReference reference) { if (_cachedValues.ContainsKey(reference) == false) { return null; } return _cachedValues[reference]; } public double GetHeight(ElementReference reference) => GetSizeInfo(reference)?.Height ?? 0.0; public double GetWidth(ElementReference reference) => GetSizeInfo(reference)?.Width ?? 0.0; protected virtual void Dispose(bool disposing) { if (disposing && _isDisposed == false) { _isDisposed = true; _dotNetRef.Dispose(); _cachedValueIds.Clear(); _cachedValues.Clear(); //in a fire and forget manner, we just "trying" to cancel the listener. So, we are not interested in an potential error try { _ = _jsRuntime.InvokeVoidAsync($"mudResizeObserver.cancelListener", _id); } catch (Exception) { } } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public async ValueTask DisposeAsync() { if (_isDisposed == true) { return; } _isDisposed = true; _dotNetRef.Dispose(); _cachedValueIds.Clear(); _cachedValues.Clear(); //in a fire and forget manner, we just "trying" to cancel the listener. So, we are not interested in an potential error try { await _jsRuntime.InvokeVoidAsync($"mudResizeObserver.cancelListener", _id); } catch (Exception) { } } }