You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Components/Services/ReseizeObserver/ResizeObserver.cs

146 lines
4.8 KiB

2 years ago
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<ResizeObserver> _dotNetRef;
private readonly IJSRuntime _jsRuntime;
private readonly Dictionary<Guid, ElementReference> _cachedValueIds = new();
private readonly Dictionary<ElementReference, BoundingClientRect> _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<ResizeObserverOptions> options = null) : this(jsRuntime, options?.Value ?? new ResizeObserverOptions())
{
}
public async Task<BoundingClientRect> Observe(ElementReference element) => (await Observe(new[] { element })).FirstOrDefault();
public async Task<IEnumerable<BoundingClientRect>> Observe(IEnumerable<ElementReference> elements)
{
var filteredElements = elements.Where(x => x.Context != null && _cachedValues.ContainsKey(x) == false).ToList();
if (filteredElements.Any() == false)
{
return Array.Empty<BoundingClientRect>();
}
List<Guid> elementIds = new();
foreach (var item in filteredElements)
{
var id = Guid.NewGuid();
elementIds.Add(id);
_cachedValueIds.Add(id, item);
}
var result = await _jsRuntime.InvokeAsync<IEnumerable<BoundingClientRect>>("mudResizeObserver.connect", _id, _dotNetRef, filteredElements, elementIds, _options) ?? Array.Empty<BoundingClientRect>();
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<SizeChangeUpdateInfo> changes)
{
Dictionary<ElementReference, BoundingClientRect> 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) { }
}
}