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/JsEvent/JsEvent.cs

239 lines
6.9 KiB

2 years ago
// Copyright (c) MudBlazor 2021
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using Microsoft.JSInterop;
namespace Connected.Services
{
public interface IJsEvent : IDisposable
{
Task Connect(string elementId, JsEventOptions options);
Task Disconnect();
event Action<int> CaretPositionChanged;
event Action<string> Paste;
event Action<int, int> Select;
}
/// <summary>
/// Subscribe JS events of any element by html id
/// </summary>
public class JsEvent : IJsEvent
{
private bool _isDisposed = false;
private readonly DotNetObjectReference<JsEvent> _dotNetRef;
private readonly IJSRuntime _jsRuntime;
private string _elementId;
private bool _isObserving;
internal HashSet<string> _subscribedEvents = new HashSet<string>();
[DynamicDependency(nameof(OnCaretPositionChanged))]
[DynamicDependency(nameof(OnPaste))]
[DynamicDependency(nameof(OnSelect))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(JsEventOptions))]
public JsEvent(IJSRuntime jsRuntime)
{
_dotNetRef = DotNetObjectReference.Create(this);
_jsRuntime = jsRuntime;
}
/// <summary>
/// Connect to the ancestor element of the element(s) that should be observed
/// </summary>
/// <param name="elementId">Ancestor html element id</param>
/// <param name="options">Define here the descendant(s) by setting TargetClass and the keystrokes to be monitored</param>
public async Task Connect(string elementId, JsEventOptions options)
{
if (_isObserving || _isDisposed)
return;
_elementId = elementId;
_isObserving = await _jsRuntime.InvokeVoidAsyncWithErrorHandling("mudJsEvent.connect", _dotNetRef, elementId, options); ;
}
/// <summary>
/// Disconnect from the previously connected ancestor and its descendants
/// </summary>
public async Task Disconnect()
{
if (_elementId == null)
return;
await UnsubscribeAll();
try
{
await _jsRuntime.InvokeVoidAsync($"mudJsEvent.disconnect", _elementId);
}
catch (Exception) { /*ignore*/ }
_isObserving = false;
}
internal void Subscribe(string eventName)
{
if (_elementId == null)
throw new InvalidOperationException("Call Connect(...) before attaching events!");
if (_subscribedEvents.Contains(eventName) || _isDisposed)
return;
try
{
_jsRuntime.InvokeVoidAsync("mudJsEvent.subscribe", _elementId, eventName).AndForget();
_subscribedEvents.Add(eventName);
}
catch (JSDisconnectedException) { }
catch (TaskCanceledException) { }
}
internal async Task Unsubscribe(string eventName)
{
if (_elementId == null)
return;
try
{
await _jsRuntime.InvokeVoidAsync($"mudJsEvent.unsubscribe", _elementId, eventName);
}
catch (Exception) { /*ignore*/ }
_subscribedEvents.Remove(eventName);
}
internal async Task UnsubscribeAll()
{
if (_elementId == null)
return;
try
{
foreach (var eventName in _subscribedEvents)
await _jsRuntime.InvokeVoidAsync($"mudJsEvent.unsubscribe", _elementId, eventName);
}
catch (Exception) { /*ignore*/ }
_subscribedEvents.Clear();
}
List<Action<int>> _caretPositionChangedHandlers = new List<Action<int>>();
/// <summary>
/// Subscribe this event to get notified about caret changes in an input on click and on keyup
/// </summary>
public event Action<int> CaretPositionChanged
{
add
{
if (_caretPositionChangedHandlers.Count == 0)
{
Subscribe("click");
Subscribe("keyup");
}
_caretPositionChangedHandlers.Add(value);
}
remove
{
if (_caretPositionChangedHandlers.Count == 0)
return;
if (_caretPositionChangedHandlers.Count == 1)
{
Unsubscribe("click").AndForget();
Unsubscribe("keyup").AndForget();
}
_caretPositionChangedHandlers.Remove(value);
}
}
/// <summary>
/// To be invoked only by JS
/// </summary>
[JSInvokable]
public void OnCaretPositionChanged(int caretPosition)
{
foreach (var handler in _caretPositionChangedHandlers)
{
handler.Invoke(caretPosition);
}
}
List<Action<string>> _pasteHandlers = new List<Action<string>>();
/// <summary>
/// Subscribe this event to get notified about paste actions
/// </summary>
public event Action<string> Paste
{
add
{
if (_pasteHandlers.Count == 0)
Subscribe("paste");
_pasteHandlers.Add(value);
}
remove
{
if (_pasteHandlers.Count == 0)
return;
if (_pasteHandlers.Count == 1)
Unsubscribe("paste").AndForget();
_pasteHandlers.Remove(value);
}
}
/// <summary>
/// To be invoked only by JS
/// </summary>
[JSInvokable]
public void OnPaste(string text)
{
foreach (var handler in _pasteHandlers)
{
handler.Invoke(text);
}
}
List<Action<int, int>> _selectHandlers = new List<Action<int, int>>();
/// <summary>
/// Subscribe this event to get notified about paste actions
/// </summary>
public event Action<int, int> Select
{
add
{
if (_selectHandlers.Count == 0)
Subscribe("select");
_selectHandlers.Add(value);
}
remove
{
if (_selectHandlers.Count == 0)
return;
if (_selectHandlers.Count == 1)
Unsubscribe("select").AndForget();
_selectHandlers.Remove(value);
}
}
/// <summary>
/// To be invoked only by JS
/// </summary>
[JSInvokable]
public void OnSelect(int start, int end)
{
foreach (var handler in _selectHandlers)
{
handler.Invoke(start, end);
}
}
protected virtual void Dispose(bool disposing)
{
if (!disposing || _isDisposed)
return;
_isDisposed = true;
Disconnect().AndForget();
_dotNetRef.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}