KeyInterceptor
Documentație pentru serviciul KeyInterceptor
1. Descriere Generală
KeyInterceptor
este un serviciu avansat pentru interceptarea și gestionarea evenimentelor de tastatură în componentele Blazor. Oferă control granular asupra comportamentului tastelor, permițând prevenirea acțiunilor implicite și gestionarea combinațiilor complexe de taste.
Caracteristici principale: - Interceptare evenimente keyboard pe elemente specifice - Suport pentru combinații de taste (Ctrl, Alt, Shift, Meta) - Prevenire comportament implicit (preventDefault) - Control propagare evenimente (stopPropagation) - Potrivire taste cu expresii regulate - Monitorizare automată DOM cu MutationObserver - Actualizare dinamică configurație taste
2. Configurare și Înregistrare
// Program.cs sau Startup.cs
// Înregistrare automată prin AddFodComponents
builder.Services.AddFodComponents(configuration);
// Sau înregistrare manuală
builder.Services.AddScoped<IKeyInterceptorFactory, KeyInterceptorFactory>();
3. Interfețe și Clase
IKeyInterceptor
public interface IKeyInterceptor : IDisposable
{
Task Connect(string elementId, KeyInterceptorOptions options);
Task Disconnect();
Task UpdateKey(KeyOptions key);
event Action<KeyboardEventArgs> KeyDown;
event Action<KeyboardEventArgs> KeyUp;
}
IKeyInterceptorFactory
public interface IKeyInterceptorFactory
{
IKeyInterceptor Create();
}
4. Modele de Date
KeyInterceptorOptions
public class KeyInterceptorOptions
{
// Clasa CSS a elementelor de monitorizat
public string TargetClass { get; set; } = "fod-input-control";
// Lista de taste și comportamente
public List<KeyOptions> Keys { get; set; } = new();
}
KeyOptions
public class KeyOptions
{
// Tasta sau pattern regex pentru taste multiple
public string Key { get; set; }
// Previne keydown: "key+none", "key+ctrl", "key+shift", etc.
public string PreventDown { get; set; }
// Previne keyup
public string PreventUp { get; set; }
// Oprește propagarea pentru keydown
public string StopDown { get; set; }
// Oprește propagarea pentru keyup
public string StopUp { get; set; }
}
KeyboardEventArgs
public class KeyboardEventArgs : EventArgs
{
public string Key { get; set; }
public string Code { get; set; }
public float Location { get; set; }
public bool Repeat { get; set; }
public bool CtrlKey { get; set; }
public bool ShiftKey { get; set; }
public bool AltKey { get; set; }
public bool MetaKey { get; set; }
public string Type { get; set; }
}
5. Exemple de Utilizare
Interceptor simplu pentru input
@implements IAsyncDisposable
@inject IKeyInterceptorFactory KeyInterceptorFactory
<div id="@_elementId">
<FodInput @bind-Value="searchText"
Class="fod-input-control"
Placeholder="Caută (Ctrl+K pentru focus)" />
<FodText Typo="Typo.caption" Class="mt-2">
Shortcuts: Ctrl+K (focus), Escape (clear), Enter (search)
</FodText>
</div>
@code {
private string _elementId = $"search-{Guid.NewGuid()}";
private string searchText;
private IKeyInterceptor _keyInterceptor;
private ElementReference inputElement;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await InitializeKeyInterceptor();
}
}
private async Task InitializeKeyInterceptor()
{
_keyInterceptor = KeyInterceptorFactory.Create();
var options = new KeyInterceptorOptions
{
TargetClass = "fod-input-control",
Keys = new List<KeyOptions>
{
// Ctrl+K pentru focus
new KeyOptions
{
Key = "k",
PreventDown = "key+ctrl",
StopDown = "key+ctrl"
},
// Escape pentru clear
new KeyOptions
{
Key = "Escape",
PreventDown = "key+none"
},
// Enter pentru search
new KeyOptions
{
Key = "Enter",
PreventDown = "key+none"
}
}
};
await _keyInterceptor.Connect(_elementId, options);
_keyInterceptor.KeyDown += OnKeyDown;
}
private async void OnKeyDown(KeyboardEventArgs args)
{
await InvokeAsync(async () =>
{
switch (args.Key)
{
case "k" when args.CtrlKey:
await FocusInput();
break;
case "Escape":
searchText = string.Empty;
StateHasChanged();
break;
case "Enter":
await PerformSearch();
break;
}
});
}
private async Task FocusInput()
{
await inputElement.FocusAsync();
}
private async Task PerformSearch()
{
if (!string.IsNullOrWhiteSpace(searchText))
{
// Execută căutarea
await SearchService.Search(searchText);
}
}
public async ValueTask DisposeAsync()
{
if (_keyInterceptor != null)
{
await _keyInterceptor.Disconnect();
_keyInterceptor.Dispose();
}
}
}
Dropdown cu navigare tastatură
@inject IKeyInterceptorFactory KeyInterceptorFactory
<div id="@_dropdownId" class="dropdown-container">
<FodInput @bind-Value="selectedText"
@onfocus="ShowDropdown"
Class="dropdown-input fod-input-control"
Placeholder="Selectați opțiune..." />
@if (isOpen)
{
<FodPaper Class="dropdown-menu" Elevation="4">
<FodList>
@for (int i = 0; i < options.Count; i++)
{
var index = i;
<FodListItem Class="@GetItemClass(index)"
OnClick="() => SelectOption(index)">
@options[index]
</FodListItem>
}
</FodList>
</FodPaper>
}
</div>
@code {
private string _dropdownId = $"dropdown-{Guid.NewGuid()}";
private IKeyInterceptor _keyInterceptor;
private List<string> options = new() { "Opțiune 1", "Opțiune 2", "Opțiune 3", "Opțiune 4" };
private string selectedText;
private bool isOpen;
private int highlightedIndex = -1;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await SetupKeyboardNavigation();
}
}
private async Task SetupKeyboardNavigation()
{
_keyInterceptor = KeyInterceptorFactory.Create();
var options = new KeyInterceptorOptions
{
TargetClass = "dropdown-input",
Keys = new List<KeyOptions>
{
// Navigare cu săgeți
new KeyOptions
{
Key = "ArrowDown",
PreventDown = "key+none",
StopDown = "key+none"
},
new KeyOptions
{
Key = "ArrowUp",
PreventDown = "key+none",
StopDown = "key+none"
},
// Selectare cu Enter
new KeyOptions
{
Key = "Enter",
PreventDown = "key+none"
},
// Închidere cu Escape
new KeyOptions
{
Key = "Escape",
PreventDown = "key+none"
},
// Deschidere cu Space
new KeyOptions
{
Key = " ",
PreventDown = "key+none"
},
// Home/End pentru navigare rapidă
new KeyOptions
{
Key = "Home",
PreventDown = "key+none"
},
new KeyOptions
{
Key = "End",
PreventDown = "key+none"
}
}
};
await _keyInterceptor.Connect(_dropdownId, options);
_keyInterceptor.KeyDown += HandleDropdownKeyboard;
}
private async void HandleDropdownKeyboard(KeyboardEventArgs args)
{
await InvokeAsync(() =>
{
switch (args.Key)
{
case "ArrowDown":
if (!isOpen)
{
ShowDropdown();
}
else
{
highlightedIndex = Math.Min(highlightedIndex + 1, options.Count - 1);
}
break;
case "ArrowUp":
if (isOpen)
{
highlightedIndex = Math.Max(highlightedIndex - 1, 0);
}
break;
case "Enter":
if (isOpen && highlightedIndex >= 0)
{
SelectOption(highlightedIndex);
}
break;
case "Escape":
CloseDropdown();
break;
case " ":
if (!isOpen)
{
ShowDropdown();
}
break;
case "Home":
if (isOpen)
{
highlightedIndex = 0;
}
break;
case "End":
if (isOpen)
{
highlightedIndex = options.Count - 1;
}
break;
}
StateHasChanged();
});
}
private void ShowDropdown()
{
isOpen = true;
highlightedIndex = 0;
}
private void CloseDropdown()
{
isOpen = false;
highlightedIndex = -1;
}
private void SelectOption(int index)
{
selectedText = options[index];
CloseDropdown();
}
private string GetItemClass(int index)
{
return index == highlightedIndex ? "highlighted" : "";
}
}
Editor cu shortcuts personalizate
@inject IKeyInterceptorFactory KeyInterceptorFactory
@inject IJSRuntime JSRuntime
<div id="@_editorId">
<FodToolbar>
<FodIconButton Icon="@FodIcons.Material.Filled.FormatBold"
OnClick="() => ApplyFormat('bold')"
Tooltip="Bold (Ctrl+B)" />
<FodIconButton Icon="@FodIcons.Material.Filled.FormatItalic"
OnClick="() => ApplyFormat('italic')"
Tooltip="Italic (Ctrl+I)" />
<FodIconButton Icon="@FodIcons.Material.Filled.FormatUnderlined"
OnClick="() => ApplyFormat('underline')"
Tooltip="Underline (Ctrl+U)" />
<FodDivider Orientation="Orientation.Vertical" />
<FodIconButton Icon="@FodIcons.Material.Filled.Save"
OnClick="SaveContent"
Tooltip="Save (Ctrl+S)" />
</FodToolbar>
<div @ref="editorElement"
contenteditable="true"
class="editor-content fod-input-control"
@oninput="OnContentChanged">
@((MarkupString)content)
</div>
</div>
@code {
private string _editorId = $"editor-{Guid.NewGuid()}";
private IKeyInterceptor _keyInterceptor;
private ElementReference editorElement;
private string content = "<p>Începeți să scrieți...</p>";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await SetupEditorShortcuts();
}
}
private async Task SetupEditorShortcuts()
{
_keyInterceptor = KeyInterceptorFactory.Create();
var options = new KeyInterceptorOptions
{
TargetClass = "editor-content",
Keys = new List<KeyOptions>
{
// Formatare text
new KeyOptions { Key = "b", PreventDown = "key+ctrl" },
new KeyOptions { Key = "i", PreventDown = "key+ctrl" },
new KeyOptions { Key = "u", PreventDown = "key+ctrl" },
// Salvare
new KeyOptions { Key = "s", PreventDown = "key+ctrl" },
// Undo/Redo custom
new KeyOptions { Key = "z", PreventDown = "key+ctrl+shift" },
// Inserare link
new KeyOptions { Key = "k", PreventDown = "key+ctrl" },
// Clear formatting
new KeyOptions { Key = "\\", PreventDown = "key+ctrl" }
}
};
await _keyInterceptor.Connect(_editorId, options);
_keyInterceptor.KeyDown += HandleEditorShortcuts;
}
private async void HandleEditorShortcuts(KeyboardEventArgs args)
{
await InvokeAsync(async () =>
{
if (args.CtrlKey)
{
switch (args.Key)
{
case "b":
await ApplyFormat("bold");
break;
case "i":
await ApplyFormat("italic");
break;
case "u":
await ApplyFormat("underline");
break;
case "s":
await SaveContent();
break;
case "k":
await InsertLink();
break;
case "\\":
await ClearFormatting();
break;
case "z" when args.ShiftKey:
await Redo();
break;
}
}
});
}
private async Task ApplyFormat(string command)
{
await JSRuntime.InvokeVoidAsync("document.execCommand", command, false, null);
}
private async Task SaveContent()
{
NotificationService.Success("Conținut salvat!");
// Implementare salvare
}
private async Task InsertLink()
{
var url = await JSRuntime.InvokeAsync<string>("prompt", "Introduceți URL:");
if (!string.IsNullOrEmpty(url))
{
await JSRuntime.InvokeVoidAsync("document.execCommand", "createLink", false, url);
}
}
}
6. Actualizare dinamică taste
public class DynamicKeyInterceptor
{
private readonly IKeyInterceptor _keyInterceptor;
private readonly List<KeyOptions> _currentKeys = new();
public async Task AddShortcut(string key, string modifier, Func<Task> action)
{
var keyOption = new KeyOptions
{
Key = key,
PreventDown = $"key+{modifier}",
StopDown = $"key+{modifier}"
};
_currentKeys.Add(keyOption);
await _keyInterceptor.UpdateKey(keyOption);
// Stochează acțiunea asociată
_shortcuts[GetShortcutKey(key, modifier)] = action;
}
public async Task RemoveShortcut(string key, string modifier)
{
var keyToRemove = _currentKeys.FirstOrDefault(k =>
k.Key == key && k.PreventDown.Contains(modifier));
if (keyToRemove != null)
{
_currentKeys.Remove(keyToRemove);
// Actualizare prin reconectare
await RefreshInterceptor();
}
}
public async Task EnableShortcuts()
{
foreach (var key in _currentKeys)
{
await _keyInterceptor.UpdateKey(key);
}
}
public async Task DisableShortcuts()
{
// Dezactivare temporară prin eliminare preventDefault
foreach (var key in _currentKeys)
{
var disabledKey = new KeyOptions { Key = key.Key };
await _keyInterceptor.UpdateKey(disabledKey);
}
}
}
7. Pattern pentru componente complexe
public abstract class KeyboardEnabledComponent : ComponentBase, IAsyncDisposable
{
[Inject] protected IKeyInterceptorFactory KeyInterceptorFactory { get; set; }
protected IKeyInterceptor KeyInterceptor { get; private set; }
protected string ComponentId { get; } = $"component-{Guid.NewGuid()}";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await InitializeKeyboard();
}
await base.OnAfterRenderAsync(firstRender);
}
protected virtual async Task InitializeKeyboard()
{
KeyInterceptor = KeyInterceptorFactory.Create();
var options = new KeyInterceptorOptions
{
TargetClass = GetTargetClass(),
Keys = GetKeyOptions()
};
await KeyInterceptor.Connect(ComponentId, options);
KeyInterceptor.KeyDown += OnKeyDown;
KeyInterceptor.KeyUp += OnKeyUp;
}
protected abstract string GetTargetClass();
protected abstract List<KeyOptions> GetKeyOptions();
protected virtual void OnKeyDown(KeyboardEventArgs args)
{
InvokeAsync(() => HandleKeyDown(args));
}
protected virtual void OnKeyUp(KeyboardEventArgs args)
{
InvokeAsync(() => HandleKeyUp(args));
}
protected abstract Task HandleKeyDown(KeyboardEventArgs args);
protected virtual Task HandleKeyUp(KeyboardEventArgs args) => Task.CompletedTask;
public virtual async ValueTask DisposeAsync()
{
if (KeyInterceptor != null)
{
KeyInterceptor.KeyDown -= OnKeyDown;
KeyInterceptor.KeyUp -= OnKeyUp;
await KeyInterceptor.Disconnect();
KeyInterceptor.Dispose();
}
}
}
8. JavaScript Integration
// fodKeyInterceptor.js - Sistem avansat de interceptare taste
window.fodKeyInterceptor = {
instances: new Map(),
connect: function(elementId, dotNetRef, options) {
const instance = {
elementId: elementId,
dotNetRef: dotNetRef,
options: options,
observer: null,
handlers: new Map()
};
// Configurare observer pentru elemente dinamice
instance.observer = new MutationObserver((mutations) => {
this.updateHandlers(instance);
});
const container = document.getElementById(elementId);
if (container) {
instance.observer.observe(container, {
childList: true,
subtree: true
});
this.updateHandlers(instance);
}
this.instances.set(elementId, instance);
},
updateHandlers: function(instance) {
const container = document.getElementById(instance.elementId);
if (!container) return;
const elements = container.querySelectorAll(`.${instance.options.targetClass}`);
elements.forEach(element => {
if (!instance.handlers.has(element)) {
const handler = (event) => this.handleKeyEvent(event, instance);
element.addEventListener('keydown', handler);
element.addEventListener('keyup', handler);
instance.handlers.set(element, handler);
}
});
// Cleanup pentru elemente eliminate
instance.handlers.forEach((handler, element) => {
if (!container.contains(element)) {
element.removeEventListener('keydown', handler);
element.removeEventListener('keyup', handler);
instance.handlers.delete(element);
}
});
},
handleKeyEvent: function(event, instance) {
const keyOptions = this.findKeyOptions(event, instance.options.keys);
if (!keyOptions) return;
const shouldPrevent = this.shouldPrevent(event, keyOptions);
const shouldStop = this.shouldStop(event, keyOptions);
if (shouldPrevent) {
event.preventDefault();
}
if (shouldStop) {
event.stopPropagation();
}
// Trimite eveniment la .NET
const eventArgs = {
key: event.key,
code: event.code,
location: event.location,
repeat: event.repeat,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
altKey: event.altKey,
metaKey: event.metaKey,
type: event.type
};
instance.dotNetRef.invokeMethodAsync(
event.type === 'keydown' ? 'OnKeyDown' : 'OnKeyUp',
eventArgs
);
},
findKeyOptions: function(event, keys) {
return keys.find(keyOption => {
if (keyOption.key.includes('/') && keyOption.key.includes('/')) {
// Regex pattern
const pattern = keyOption.key.slice(1, -1);
const regex = new RegExp(pattern);
return regex.test(event.key);
}
return keyOption.key === event.key;
});
},
shouldPrevent: function(event, keyOptions) {
const preventKey = event.type === 'keydown'
? keyOptions.preventDown
: keyOptions.preventUp;
return this.matchesModifiers(event, preventKey);
},
shouldStop: function(event, keyOptions) {
const stopKey = event.type === 'keydown'
? keyOptions.stopDown
: keyOptions.stopUp;
return this.matchesModifiers(event, stopKey);
},
matchesModifiers: function(event, modifierString) {
if (!modifierString) return false;
const parts = modifierString.split('+');
const hasKey = parts.includes('key');
const hasCtrl = parts.includes('ctrl');
const hasShift = parts.includes('shift');
const hasAlt = parts.includes('alt');
const hasMeta = parts.includes('meta');
const hasNone = parts.includes('none');
if (hasNone) {
return hasKey && !event.ctrlKey && !event.shiftKey &&
!event.altKey && !event.metaKey;
}
return hasKey &&
(!hasCtrl || event.ctrlKey) &&
(!hasShift || event.shiftKey) &&
(!hasAlt || event.altKey) &&
(!hasMeta || event.metaKey);
},
disconnect: function(elementId) {
const instance = this.instances.get(elementId);
if (instance) {
if (instance.observer) {
instance.observer.disconnect();
}
instance.handlers.forEach((handler, element) => {
element.removeEventListener('keydown', handler);
element.removeEventListener('keyup', handler);
});
this.instances.delete(elementId);
}
},
updateKey: function(elementId, keyOption) {
const instance = this.instances.get(elementId);
if (instance) {
const existingIndex = instance.options.keys.findIndex(k =>
k.key === keyOption.key);
if (existingIndex >= 0) {
instance.options.keys[existingIndex] = keyOption;
} else {
instance.options.keys.push(keyOption);
}
}
}
};
9. Testare
[TestClass]
public class KeyInterceptorTests
{
private Mock<IJSRuntime> _jsRuntime;
private IKeyInterceptor _keyInterceptor;
private List<KeyboardEventArgs> _keyDownEvents;
[TestInitialize]
public void Setup()
{
_jsRuntime = new Mock<IJSRuntime>();
_keyInterceptor = new KeyInterceptor(_jsRuntime.Object);
_keyDownEvents = new List<KeyboardEventArgs>();
_keyInterceptor.KeyDown += args => _keyDownEvents.Add(args);
}
[TestMethod]
public async Task Connect_ValidOptions_CallsJavaScript()
{
// Arrange
var elementId = "test-element";
var options = new KeyInterceptorOptions
{
TargetClass = "test-class",
Keys = new List<KeyOptions>
{
new KeyOptions { Key = "Enter", PreventDown = "key+none" }
}
};
// Act
await _keyInterceptor.Connect(elementId, options);
// Assert
_jsRuntime.Verify(js => js.InvokeVoidAsync(
"fodKeyInterceptor.connect",
It.IsAny<object[]>()), Times.Once);
}
[TestMethod]
public void OnKeyDown_FiresEvent()
{
// Arrange
var keyEvent = new KeyboardEventArgs
{
Key = "Enter",
Type = "keydown"
};
// Act
_keyInterceptor.InvokeKeyDown(keyEvent);
// Assert
Assert.AreEqual(1, _keyDownEvents.Count);
Assert.AreEqual("Enter", _keyDownEvents[0].Key);
}
}
10. Best Practices
- Dispose obligatoriu - Întotdeauna apelați Disconnect și Dispose
- Target specific - Folosiți clase CSS specifice pentru targeting
- Modifiers clare - Definiți clar combinațiile de taste
- Prevent cu grijă - Nu preveniți taste sistem importante
- Documentare shortcuts - Afișați shortcuts disponibile utilizatorilor
- Accessibility - Asigurați alternative pentru toate shortcuts
- Testing - Testați pe diferite browsere și OS-uri
11. Concluzie
KeyInterceptor
oferă un sistem robust și flexibil pentru gestionarea evenimentelor de tastatură în componentele Blazor. Cu suport pentru combinații complexe de taste, monitorizare automată DOM și control granular asupra comportamentului, serviciul facilitează crearea de interfețe cu shortcuts intuitive și experiențe de navigare eficiente cu tastatura.