From 8e5e7d4ab461257a41f707c6fd486f605d09bc06 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 8 Feb 2021 12:51:39 +0000 Subject: [PATCH 01/25] Mark redundant extension methods as obsolete. --- ...EventCallbackFactoryEventArgsExtensions.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Components/Web/src/Web/WebEventCallbackFactoryEventArgsExtensions.cs b/src/Components/Web/src/Web/WebEventCallbackFactoryEventArgsExtensions.cs index d7f7d01ae205..e13959c43567 100644 --- a/src/Components/Web/src/Web/WebEventCallbackFactoryEventArgsExtensions.cs +++ b/src/Components/Web/src/Web/WebEventCallbackFactoryEventArgsExtensions.cs @@ -19,6 +19,7 @@ public static class WebEventCallbackFactoryEventArgsExtensions /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) { if (factory == null) @@ -37,6 +38,7 @@ public static EventCallback Create(this EventCallbackFactory /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) { if (factory == null) @@ -55,6 +57,7 @@ public static EventCallback Create(this EventCallbackFactory /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) { if (factory == null) @@ -73,6 +76,7 @@ public static EventCallback Create(this EventCallbackFactory fact /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) { if (factory == null) @@ -91,6 +95,7 @@ public static EventCallback Create(this EventCallbackFactory fact /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) { if (factory == null) @@ -109,6 +114,7 @@ public static EventCallback Create(this EventCallbackFactory fac /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) { if (factory == null) @@ -127,6 +133,7 @@ public static EventCallback Create(this EventCallbackFactory fac /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) { if (factory == null) @@ -145,6 +152,7 @@ public static EventCallback Create(this EventCallbackFactory fac /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) { if (factory == null) @@ -163,6 +171,7 @@ public static EventCallback Create(this EventCallbackFactory fac /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) { if (factory == null) @@ -181,6 +190,7 @@ public static EventCallback Create(this EventCallbackFactory /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) { if (factory == null) @@ -199,6 +209,7 @@ public static EventCallback Create(this EventCallbackFactory /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) { if (factory == null) @@ -217,6 +228,7 @@ public static EventCallback Create(this EventCallbackFactory fac /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) { if (factory == null) @@ -234,6 +246,7 @@ public static EventCallback Create(this EventCallbackFactory fac /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) { if (factory == null) @@ -252,6 +265,7 @@ public static EventCallback Create(this EventCallbackFactory f /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) { if (factory == null) @@ -270,6 +284,7 @@ public static EventCallback Create(this EventCallbackFactory f /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) { if (factory == null) @@ -288,6 +303,7 @@ public static EventCallback Create(this EventCallbackFactory /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) { if (factory == null) @@ -306,6 +322,7 @@ public static EventCallback Create(this EventCallbackFactory /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) { if (factory == null) @@ -324,6 +341,7 @@ public static EventCallback Create(this EventCallbackFactory fac /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) { if (factory == null) @@ -342,6 +360,7 @@ public static EventCallback Create(this EventCallbackFactory fac /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) { if (factory == null) @@ -360,6 +379,7 @@ public static EventCallback Create(this EventCallbackFactory fac /// The event receiver. /// The event callback. /// The . + [Obsolete("This extension method is obsolete and will be removed in a future version. Use the generic overload instead.")] public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) { if (factory == null) From ea7c7c2b7b6880f2a8331529f70ba1ddb9e828eb Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 8 Feb 2021 13:39:22 +0000 Subject: [PATCH 02/25] Add E2E test showing polymorphic event handlers work today ... because a subsequent implementation change would break this if it wasn't accounted for --- .../test/E2ETest/Tests/EventTest.cs | 25 +++++++++++++ .../BasicTestApp/MouseEventComponent.razor | 35 ++++++++++++++----- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/Components/test/E2ETest/Tests/EventTest.cs b/src/Components/test/E2ETest/Tests/EventTest.cs index e0d74eeffc04..69e9a558867f 100644 --- a/src/Components/test/E2ETest/Tests/EventTest.cs +++ b/src/Components/test/E2ETest/Tests/EventTest.cs @@ -300,6 +300,31 @@ public void RenderAttributesBeforeConnectedCallBack() Browser.Contains(expectedContent, () => element.Text); } + [Fact] + public void PolymorphicEventHandlersReceiveCorrectArgsSubclass() + { + // This is to show that the type of event argument received corresponds to the declared event + // name, and not to the argument type on the event handler delegate. Note that this is only + // supported (for back-compat) for the built-in standard web event types. For custom events, + // the eventargs deserialization type is determined purely by the delegate's parameters list. + Browser.MountTestComponent(); + + var elem = Browser.Exists(By.Id("polymorphic_event_elem")); + + // Output is initially empty + var output = Browser.Exists(By.Id("output")); + Assert.Equal(string.Empty, output.Text); + + // We can trigger a pointer event and receive a PointerEventArgs + new Actions(Browser).Click(elem).Perform(); + Browser.Equal("Microsoft.AspNetCore.Components.Web.PointerEventArgs:mouse", () => output.Text); + + // We can trigger a drag event and receive a DragEventArgs *on the same handler delegate* + Browser.FindElement(By.Id("clear_event_log")).Click(); + new Actions(Browser).DragAndDrop(elem, Browser.FindElement(By.Id("other"))).Perform(); + Browser.Equal("Microsoft.AspNetCore.Components.Web.DragEventArgs:1", () => output.Text); + } + void SendKeysSequentially(IWebElement target, string text) { // Calling it for each character works around some chars being skipped diff --git a/src/Components/test/testassets/BasicTestApp/MouseEventComponent.razor b/src/Components/test/testassets/BasicTestApp/MouseEventComponent.razor index 9b9d05eedcc3..d529d3094568 100644 --- a/src/Components/test/testassets/BasicTestApp/MouseEventComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/MouseEventComponent.razor @@ -23,12 +23,17 @@
Drop Target

- +

Another input (to distract you)

+ +

+ Polymorphic args handler: +

Click or drag me
+

@code { @@ -39,56 +44,68 @@ { DumpEvent(e); message += "onmouseover,"; - StateHasChanged(); } void OnMouseOut(MouseEventArgs e) { DumpEvent(e); message += "onmouseout,"; - StateHasChanged(); } void OnMouseMove(MouseEventArgs e) { DumpEvent(e); message += "onmousemove,"; - StateHasChanged(); } void OnMouseDown(MouseEventArgs e) { DumpEvent(e); message += "onmousedown,"; - StateHasChanged(); } void OnMouseUp(MouseEventArgs e) { DumpEvent(e); message += "onmouseup,"; - StateHasChanged(); } void OnPointerDown(PointerEventArgs e) { DumpEvent(e); message += "onpointerdown"; - StateHasChanged(); } void OnDragStart(DragEventArgs e) { DumpEvent(e); message += "ondragstart,"; - StateHasChanged(); } void OnDrop(DragEventArgs e) { DumpEvent(e); message += "ondrop,"; - StateHasChanged(); + } + + void OnPolymorphicEvent(EventArgs e) + { + // The purpose of this handler is to show that, even though the declared args type is + // the EventArgs base class, at runtime we actually receive the subclass corresponding + // to the event that occurred. Note that this will only be supported for the built-in + // web event types (for back compatibility), and cannot work for any custom events, + // since we have no way to know which subclass you'd want for a custom event. + message += e.GetType().FullName; + + switch (e) + { + case PointerEventArgs pointerEvent: + message += $":{pointerEvent.PointerType}"; + break; + case DragEventArgs dragEvent: + message += $":{dragEvent.Buttons}"; + break; + } } void DumpEvent(MouseEventArgs e) From 7b582080c80ea1f547a4b7b66f1918dbb9bc34fc Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 8 Feb 2021 15:55:04 +0000 Subject: [PATCH 03/25] Clean up JS-side code to only send raw event args (which does include the raw event type name), but nothing about args deserialization type --- .../Web.JS/src/Rendering/BrowserRenderer.ts | 10 +- .../Web.JS/src/Rendering/EventDelegator.ts | 8 +- .../Web.JS/src/Rendering/EventForDotNet.ts | 206 +++++++++--------- 3 files changed, 109 insertions(+), 115 deletions(-) diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts index 3fab6a2cd0c8..0980e9440689 100644 --- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts @@ -1,6 +1,6 @@ import { RenderBatch, ArrayBuilderSegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch'; import { EventDelegator } from './EventDelegator'; -import { EventForDotNet, UIEventArgs, EventArgsType } from './EventForDotNet'; +import { UIEventArgs } from './EventForDotNet'; import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd, permuteLogicalChildren, getClosestDomElement } from './LogicalElements'; import { applyCaptureIdToElement } from './ElementReferenceCapture'; import { EventFieldInfo } from './EventFieldInfo'; @@ -462,7 +462,7 @@ export interface ComponentDescriptor { export interface EventDescriptor { browserRendererId: number; eventHandlerId: number; - eventArgsType: EventArgsType; + eventName: string; eventFieldInfo: EventFieldInfo | null; } @@ -495,7 +495,7 @@ function raiseEvent( event: Event, browserRendererId: number, eventHandlerId: number, - eventArgs: EventForDotNet, + eventArgs: UIEventArgs, eventFieldInfo: EventFieldInfo | null ): void { if (preventDefaultEvents[event.type]) { @@ -505,11 +505,11 @@ function raiseEvent( const eventDescriptor = { browserRendererId, eventHandlerId, - eventArgsType: eventArgs.type, + eventName: eventArgs.type, eventFieldInfo: eventFieldInfo, }; - dispatchEvent(eventDescriptor, eventArgs.data); + dispatchEvent(eventDescriptor, eventArgs); } function clearElement(element: Element) { diff --git a/src/Components/Web.JS/src/Rendering/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/EventDelegator.ts index 8f63d9406f2f..4595231b29fa 100644 --- a/src/Components/Web.JS/src/Rendering/EventDelegator.ts +++ b/src/Components/Web.JS/src/Rendering/EventDelegator.ts @@ -1,4 +1,4 @@ -import { EventForDotNet, UIEventArgs } from './EventForDotNet'; +import { fromDOMEvent, UIEventArgs } from './EventForDotNet'; import { EventFieldInfo } from './EventFieldInfo'; const nonBubblingEvents = toLookup([ @@ -25,7 +25,7 @@ const nonBubblingEvents = toLookup([ const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']); export interface OnEventCallback { - (event: Event, eventHandlerId: number, eventArgs: EventForDotNet, eventFieldInfo: EventFieldInfo | null): void; + (event: Event, eventHandlerId: number, eventArgs: UIEventArgs, eventFieldInfo: EventFieldInfo | null): void; } // Responsible for adding/removing the eventInfo on an expando property on DOM elements, and @@ -107,7 +107,7 @@ export class EventDelegator { // Scan up the element hierarchy, looking for any matching registered event handlers let candidateElement = evt.target as Element | null; - let eventArgs: EventForDotNet | null = null; // Populate lazily + let eventArgs: UIEventArgs | null = null; // Populate lazily const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type); let stopPropagationWasRequested = false; while (candidateElement) { @@ -117,7 +117,7 @@ export class EventDelegator { if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) { // We are going to raise an event for this element, so prepare info needed by the .NET code if (!eventArgs) { - eventArgs = EventForDotNet.fromDOMEvent(evt); + eventArgs = fromDOMEvent(evt); } const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt); diff --git a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts b/src/Components/Web.JS/src/Rendering/EventForDotNet.ts index 8c6222e775b5..7d4da189ad6b 100644 --- a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts +++ b/src/Components/Web.JS/src/Rendering/EventForDotNet.ts @@ -1,112 +1,108 @@ -export class EventForDotNet { - public constructor(public readonly type: EventArgsType, public readonly data: TData) { +export function fromDOMEvent(event: Event): UIEventArgs { + switch (event.type) { + + case 'input': + case 'change': + return parseChangeEvent(event); + + case 'copy': + case 'cut': + case 'paste': + return { type: event.type }; + + case 'drag': + case 'dragend': + case 'dragenter': + case 'dragleave': + case 'dragover': + case 'dragstart': + case 'drop': + return parseDragEvent(event); + + case 'focus': + case 'blur': + case 'focusin': + case 'focusout': + return { type: event.type }; + + case 'keydown': + case 'keyup': + case 'keypress': + return parseKeyboardEvent(event as KeyboardEvent); + + case 'contextmenu': + case 'click': + case 'mouseover': + case 'mouseout': + case 'mousemove': + case 'mousedown': + case 'mouseup': + case 'dblclick': + return parseMouseEvent(event as MouseEvent); + + case 'error': + return parseErrorEvent(event as ErrorEvent); + + case 'loadstart': + case 'timeout': + case 'abort': + case 'load': + case 'loadend': + case 'progress': + return parseProgressEvent(event as ProgressEvent); + + case 'touchcancel': + case 'touchend': + case 'touchmove': + case 'touchenter': + case 'touchleave': + case 'touchstart': + return parseTouchEvent(event as TouchEvent); + + case 'gotpointercapture': + case 'lostpointercapture': + case 'pointercancel': + case 'pointerdown': + case 'pointerenter': + case 'pointerleave': + case 'pointermove': + case 'pointerout': + case 'pointerover': + case 'pointerup': + return parsePointerEvent(event as PointerEvent); + + case 'wheel': + case 'mousewheel': + return parseWheelEvent(event as WheelEvent); + + case 'toggle': + return { type: event.type }; + + default: + return { type: event.type }; } +} - public static fromDOMEvent(event: Event): EventForDotNet { - const element = event.target as Element; - switch (event.type) { - - case 'input': - case 'change': { - - if (isTimeBasedInput(element)) { - const normalizedValue = normalizeTimeBasedValue(element); - return new EventForDotNet('change', { type: event.type, value: normalizedValue }); - } - - const targetIsCheckbox = isCheckbox(element); - const newValue = targetIsCheckbox ? !!element['checked'] : element['value']; - return new EventForDotNet('change', { type: event.type, value: newValue }); - } - - case 'copy': - case 'cut': - case 'paste': - return new EventForDotNet('clipboard', { type: event.type }); - - case 'drag': - case 'dragend': - case 'dragenter': - case 'dragleave': - case 'dragover': - case 'dragstart': - case 'drop': - return new EventForDotNet('drag', parseDragEvent(event)); - - case 'focus': - case 'blur': - case 'focusin': - case 'focusout': - return new EventForDotNet('focus', { type: event.type }); - - case 'keydown': - case 'keyup': - case 'keypress': - return new EventForDotNet('keyboard', parseKeyboardEvent(event as KeyboardEvent)); - - case 'contextmenu': - case 'click': - case 'mouseover': - case 'mouseout': - case 'mousemove': - case 'mousedown': - case 'mouseup': - case 'dblclick': - return new EventForDotNet('mouse', parseMouseEvent(event as MouseEvent)); - - case 'error': - return new EventForDotNet('error', parseErrorEvent(event as ErrorEvent)); - - case 'loadstart': - case 'timeout': - case 'abort': - case 'load': - case 'loadend': - case 'progress': - return new EventForDotNet('progress', parseProgressEvent(event as ProgressEvent)); - - case 'touchcancel': - case 'touchend': - case 'touchmove': - case 'touchenter': - case 'touchleave': - case 'touchstart': - return new EventForDotNet('touch', parseTouchEvent(event as TouchEvent)); - - case 'gotpointercapture': - case 'lostpointercapture': - case 'pointercancel': - case 'pointerdown': - case 'pointerenter': - case 'pointerleave': - case 'pointermove': - case 'pointerout': - case 'pointerover': - case 'pointerup': - return new EventForDotNet('pointer', parsePointerEvent(event as PointerEvent)); - - case 'wheel': - case 'mousewheel': - return new EventForDotNet('wheel', parseWheelEvent(event as WheelEvent)); - - case 'toggle': - return new EventForDotNet('toggle', { type: event.type }); - - default: - return new EventForDotNet('unknown', { type: event.type }); - } +function parseChangeEvent(event: any): UIChangeEventArgs { + const element = event.target as Element; + if (isTimeBasedInput(element)) { + const normalizedValue = normalizeTimeBasedValue(element); + return { type: event.type, value: normalizedValue }; + } else { + const targetIsCheckbox = isCheckbox(element); + const newValue = targetIsCheckbox ? !!element['checked'] : element['value']; + return { type: event.type, value: newValue }; } } -function parseDragEvent(event: any) { +function parseDragEvent(event: any): UIDragEventArgs { return { ...parseMouseEvent(event), dataTransfer: event.dataTransfer, - }; } -function parseWheelEvent(event: WheelEvent) { +function parseWheelEvent(event: WheelEvent): UIWheelEventArgs { return { ...parseMouseEvent(event), deltaX: event.deltaX, @@ -116,7 +112,7 @@ function parseWheelEvent(event: WheelEvent) { }; } -function parseErrorEvent(event: ErrorEvent) { +function parseErrorEvent(event: ErrorEvent): UIErrorEventArgs { return { type: event.type, message: event.message, @@ -126,7 +122,7 @@ function parseErrorEvent(event: ErrorEvent) { }; } -function parseProgressEvent(event: ProgressEvent) { +function parseProgressEvent(event: ProgressEvent): UIProgressEventArgs { return { type: event.type, lengthComputable: event.lengthComputable, @@ -135,7 +131,7 @@ function parseProgressEvent(event: ProgressEvent) { }; } -function parseTouchEvent(event: TouchEvent) { +function parseTouchEvent(event: TouchEvent): UITouchEventArgs { function parseTouch(touchList: TouchList) { const touches: UITouchPoint[] = []; @@ -168,7 +164,7 @@ function parseTouchEvent(event: TouchEvent) { }; } -function parseKeyboardEvent(event: KeyboardEvent) { +function parseKeyboardEvent(event: KeyboardEvent): UIKeyboardEventArgs { return { type: event.type, key: event.key, @@ -182,7 +178,7 @@ function parseKeyboardEvent(event: KeyboardEvent) { }; } -function parsePointerEvent(event: PointerEvent) { +function parsePointerEvent(event: PointerEvent): UIPointerEventArgs { return { ...parseMouseEvent(event), pointerId: event.pointerId, @@ -196,7 +192,7 @@ function parsePointerEvent(event: PointerEvent) { }; } -function parseMouseEvent(event: MouseEvent) { +function parseMouseEvent(event: MouseEvent): UIMouseEventArgs { return { type: event.type, detail: event.detail, @@ -251,8 +247,6 @@ function normalizeTimeBasedValue(element: HTMLInputElement): string { // The following interfaces must be kept in sync with the UIEventArgs C# classes -export type EventArgsType = 'change' | 'clipboard' | 'drag' | 'error' | 'focus' | 'keyboard' | 'mouse' | 'pointer' | 'progress' | 'touch' | 'unknown' | 'wheel' | 'toggle'; - export interface UIEventArgs { type: string; } From b37c11a80ff0b97d6ef7f2bb6a68f2aa1ed59b3d Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 8 Feb 2021 16:35:09 +0000 Subject: [PATCH 04/25] When necessary, parse the event args JSON using the parameter type from the handler delegate --- .../Components/src/PublicAPI.Unshipped.txt | 1 + .../Components/src/RenderTree/Renderer.cs | 52 +++++- .../Components/test/RendererTest.cs | 101 ++++++++++++ .../Server/src/Circuits/CircuitHost.cs | 2 +- src/Components/Shared/src/WebEventData.cs | 155 +++++++++++++++--- .../Web/src/PublicAPI.Unshipped.txt | 4 + src/Components/Web/src/WebEventDescriptor.cs | 2 +- .../src/Infrastructure/JSInteropMethods.cs | 2 +- .../ComponentHubInvalidEventTest.cs | 4 +- .../InteropReliabilityTests.cs | 4 +- 10 files changed, 292 insertions(+), 35 deletions(-) diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 98ca3f39e896..2f6b57dcfd8a 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -15,6 +15,7 @@ Microsoft.AspNetCore.Components.DynamicComponent.Type.set -> void Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.CascadingTypeParameterAttribute(string! name) -> void Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.Name.get -> string! +Microsoft.AspNetCore.Components.RenderTree.Renderer.GetEventArgsType(ulong eventHandlerId) -> System.Type! static Microsoft.AspNetCore.Components.ParameterView.FromDictionary(System.Collections.Generic.IDictionary! parameters) -> Microsoft.AspNetCore.Components.ParameterView virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs) -> System.Threading.Tasks.Task! *REMOVED*readonly Microsoft.AspNetCore.Components.RenderTree.RenderTreeEdit.RemovedAttributeName -> string diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index bb6f2931c4e5..4b2232dc93d0 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -249,11 +249,7 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie { Dispatcher.AssertAccess(); - if (!_eventBindings.TryGetValue(eventHandlerId, out var callback)) - { - throw new ArgumentException($"There is no event handler associated with this event. EventId: '{eventHandlerId}'.", nameof(eventHandlerId)); - } - + var callback = GetRequiredEventCallback(eventHandlerId); Log.HandlingEvent(_logger, eventHandlerId, eventArgs); if (fieldInfo != null) @@ -291,6 +287,42 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie return result; } + /// + /// Gets the event arguments type for the specified event handler. + /// + /// The value from the original event attribute. + /// The parameter type expected by the event handler. Normally this is a subclass of . + public Type GetEventArgsType(ulong eventHandlerId) + { + var callback = GetRequiredEventCallback(eventHandlerId); + + // The DispatchEventAsync code paths allow for the case where Delegate or its method + // is null, and in this case the event receiver just receives null. This won't happen + // under normal circumstances, but to avoid creating a new failure scenario, allow for + // that edge case here too. + var parameterInfos = callback.Delegate?.Method.GetParameters(); + if (parameterInfos == null || parameterInfos.Length == 0) + { + return typeof(EventArgs); + } + else if (parameterInfos.Length > 1) + { + throw new InvalidOperationException($"The event handler for event {eventHandlerId} declares more than one parameter. Only one is supported."); + } + else + { + var declaredType = parameterInfos[0].ParameterType; + if (typeof(EventArgs).IsAssignableFrom(declaredType)) + { + return declaredType; + } + else + { + throw new InvalidOperationException($"The event handler parameter type {declaredType.FullName} for event must inherit from {typeof(EventArgs).FullName}."); + } + } + } + internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId) { if (frame.FrameTypeField != RenderTreeFrameType.Component) @@ -404,6 +436,16 @@ internal void TrackReplacedEventHandlerId(ulong oldEventHandlerId, ulong newEven _eventHandlerIdReplacements.Add(oldEventHandlerId, newEventHandlerId); } + private EventCallback GetRequiredEventCallback(ulong eventHandlerId) + { + if (!_eventBindings.TryGetValue(eventHandlerId, out var callback)) + { + throw new ArgumentException($"There is no event handler associated with this event. EventId: '{eventHandlerId}'.", nameof(eventHandlerId)); + } + + return callback; + } + private ulong FindLatestEventHandlerIdInChain(ulong eventHandlerId) { while (_eventHandlerIdReplacements.TryGetValue(eventHandlerId, out var replacementEventHandlerId)) diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index 5a5acc6e896d..3c12040f9a0d 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -484,6 +484,98 @@ public void CanDispatchEventsToTopLevelComponents() Assert.Same(eventArgs, receivedArgs); } + [Fact] + public void CanGetEventArgsTypeForHandler() + { + // Arrange: Render a component with an event handler + var renderer = new TestRenderer(); + + var component = new EventComponent + { + OnArbitraryDelegateEvent = (Func)(args => Task.CompletedTask), + }; + var componentId = renderer.AssignRootComponentId(component); + component.TriggerRender(); + + var eventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.AttributeValue != null) + .AttributeEventHandlerId; + + // Assert: Can determine event args type + var eventArgsType = renderer.GetEventArgsType(eventHandlerId); + Assert.Same(typeof(DerivedEventArgs), eventArgsType); + } + + [Fact] + public void CanGetEventArgsTypeForParameterlessHandler() + { + // Arrange: Render a component with an event handler + var renderer = new TestRenderer(); + + var component = new EventComponent + { + OnArbitraryDelegateEvent = (Func)(() => Task.CompletedTask), + }; + var componentId = renderer.AssignRootComponentId(component); + component.TriggerRender(); + + var eventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.AttributeValue != null) + .AttributeEventHandlerId; + + // Assert: Can determine event args type + var eventArgsType = renderer.GetEventArgsType(eventHandlerId); + Assert.Same(typeof(EventArgs), eventArgsType); + } + + [Fact] + public void CannotGetEventArgsTypeForMultiParameterHandler() + { + // Arrange: Render a component with an event handler + var renderer = new TestRenderer(); + + var component = new EventComponent + { + OnArbitraryDelegateEvent = (Action)((x, y) => { }), + }; + var componentId = renderer.AssignRootComponentId(component); + component.TriggerRender(); + + var eventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.AttributeValue != null) + .AttributeEventHandlerId; + + // Assert: Cannot determine event args type + var ex = Assert.Throws(() => renderer.GetEventArgsType(eventHandlerId)); + Assert.Contains("declares more than one parameter", ex.Message); + } + + [Fact] + public void CannotGetEventArgsTypeForHandlerWithNonEventArgsParameter() + { + // Arrange: Render a component with an event handler + var renderer = new TestRenderer(); + + var component = new EventComponent + { + OnArbitraryDelegateEvent = (Action)(arg => { }), + }; + var componentId = renderer.AssignRootComponentId(component); + component.TriggerRender(); + + var eventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.AttributeValue != null) + .AttributeEventHandlerId; + + // Assert: Cannot determine event args type + var ex = Assert.Throws(() => renderer.GetEventArgsType(eventHandlerId)); + Assert.Contains($"must inherit from {typeof(EventArgs).FullName}", ex.Message); + } + [Fact] public void DispatchEventHandlesSynchronousExceptionsFromEventHandlers() { @@ -4224,6 +4316,9 @@ private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent [Parameter] public EventCallback OnClickEventCallbackOfT { get; set; } + [Parameter] + public Delegate OnArbitraryDelegateEvent { get; set; } + public bool SkipElement { get; set; } private int renderCount = 0; @@ -4269,6 +4364,12 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.AddAttribute(5, "onclickaction", OnClickAsyncAction); } + + if (OnArbitraryDelegateEvent != null) + { + builder.AddAttribute(6, "onarbitrarydelegateevent", OnArbitraryDelegateEvent); + } + builder.CloseElement(); builder.CloseElement(); } diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index 91222e9513cd..63c96073f682 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -399,7 +399,7 @@ public async Task DispatchEvent(string eventDescriptorJson, string eventArgsJson WebEventData webEventData; try { - webEventData = WebEventData.Parse(eventDescriptorJson, eventArgsJson); + webEventData = WebEventData.Parse(Renderer, eventDescriptorJson, eventArgsJson); } catch (Exception ex) { diff --git a/src/Components/Shared/src/WebEventData.cs b/src/Components/Shared/src/WebEventData.cs index 2940cc61d830..73830c548b4b 100644 --- a/src/Components/Shared/src/WebEventData.cs +++ b/src/Components/Shared/src/WebEventData.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Microsoft.AspNetCore.Components.RenderTree; @@ -10,8 +11,8 @@ namespace Microsoft.AspNetCore.Components.Web internal class WebEventData { // This class represents the second half of parsing incoming event data, - // once the type of the eventArgs becomes known. - public static WebEventData Parse(string eventDescriptorJson, string eventArgsJson) + // once the event ID (and possibly the type of the eventArgs) becomes known. + public static WebEventData Parse(Renderer renderer, string eventDescriptorJson, string eventArgsJson) { WebEventDescriptor eventDescriptor; try @@ -24,17 +25,19 @@ public static WebEventData Parse(string eventDescriptorJson, string eventArgsJso } return Parse( + renderer, eventDescriptor, eventArgsJson); } - public static WebEventData Parse(WebEventDescriptor eventDescriptor, string eventArgsJson) + public static WebEventData Parse(Renderer renderer, WebEventDescriptor eventDescriptor, string eventArgsJson) { + var parsedEventArgs = ParseEventArgsJson(renderer, eventDescriptor.EventHandlerId, eventDescriptor.EventName, eventArgsJson); return new WebEventData( eventDescriptor.BrowserRendererId, eventDescriptor.EventHandlerId, InterpretEventFieldInfo(eventDescriptor.EventFieldInfo), - ParseEventArgsJson(eventDescriptor.EventHandlerId, eventDescriptor.EventArgsType, eventArgsJson)); + parsedEventArgs); } private WebEventData(int browserRendererId, ulong eventHandlerId, EventFieldInfo? eventFieldInfo, EventArgs eventArgs) @@ -53,36 +56,140 @@ private WebEventData(int browserRendererId, ulong eventHandlerId, EventFieldInfo public EventArgs EventArgs { get; } - private static EventArgs ParseEventArgsJson(ulong eventHandlerId, string eventArgsType, string eventArgsJson) + private static EventArgs ParseEventArgsJson(Renderer renderer, ulong eventHandlerId, string eventName, string eventArgsJson) { try { - return eventArgsType switch + if (TryGetStandardWebEventArgsType(eventName, out var eventArgsType)) { - "change" => DeserializeChangeEventArgs(eventArgsJson), - "clipboard" => Deserialize(eventArgsJson), - "drag" => Deserialize(eventArgsJson), - "error" => Deserialize(eventArgsJson), - "focus" => Deserialize(eventArgsJson), - "keyboard" => Deserialize(eventArgsJson), - "mouse" => Deserialize(eventArgsJson), - "pointer" => Deserialize(eventArgsJson), - "progress" => Deserialize(eventArgsJson), - "touch" => Deserialize(eventArgsJson), - "unknown" => EventArgs.Empty, - "wheel" => Deserialize(eventArgsJson), - "toggle" => Deserialize(eventArgsJson), - _ => throw new InvalidOperationException($"Unsupported event type '{eventArgsType}'. EventId: '{eventHandlerId}'."), - }; + // Special case for ChangeEventArgs because its value type can be one of + // several types, and System.Text.Json doesn't pick types dynamically + if (eventArgsType == typeof(ChangeEventArgs)) + { + return DeserializeChangeEventArgs(eventArgsJson); + } + } + else + { + // For custom events, the args type is determined from the associated delegate + eventArgsType = renderer.GetEventArgsType(eventHandlerId); + } + + return (EventArgs)JsonSerializer.Deserialize(eventArgsJson, eventArgsType, JsonSerializerOptionsProvider.Options)!; } catch (Exception e) { throw new InvalidOperationException($"There was an error parsing the event arguments. EventId: '{eventHandlerId}'.", e); } - } - static T Deserialize(string json) => JsonSerializer.Deserialize(json, JsonSerializerOptionsProvider.Options)!; + private static bool TryGetStandardWebEventArgsType(string eventName, [MaybeNullWhen(false)] out Type type) + { + // For back-compatibility, we recognize the built-in list of web event names and hard-code + // rules about the deserialization type for their eventargs. This makes it possible to declare + // an event handler as receiving EventArgs, and have it actually receive a subclass at runtime + // depending on the event that was raised. + // + // The following list should remain in sync with EventForDotNet.ts. + + switch (eventName) + { + case "input": + case "change": + type = typeof(ChangeEventArgs); + return true; + + case "copy": + case "cut": + case "paste": + type = typeof(EventArgs); + return true; + + case "drag": + case "dragend": + case "dragenter": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + type = typeof(DragEventArgs); + return true; + + case "focus": + case "blur": + case "focusin": + case "focusout": + type = typeof(EventArgs); + return true; + + case "keydown": + case "keyup": + case "keypress": + type = typeof(KeyboardEventArgs); + return true; + + case "contextmenu": + case "click": + case "mouseover": + case "mouseout": + case "mousemove": + case "mousedown": + case "mouseup": + case "dblclick": + type = typeof(MouseEventArgs); + return true; + + case "error": + type = typeof(ErrorEventArgs); + return true; + + case "loadstart": + case "timeout": + case "abort": + case "load": + case "loadend": + case "progress": + type = typeof(ProgressEventArgs); + return true; + + case "touchcancel": + case "touchend": + case "touchmove": + case "touchenter": + case "touchleave": + case "touchstart": + type = typeof(TouchEventArgs); + return true; + + case "gotpointercapture": + case "lostpointercapture": + case "pointercancel": + case "pointerdown": + case "pointerenter": + case "pointerleave": + case "pointermove": + case "pointerout": + case "pointerover": + case "pointerup": + type = typeof(PointerEventArgs); + return true; + + case "wheel": + case "mousewheel": + type = typeof(WheelEventArgs); + return true; + + case "toggle": + type = typeof(EventArgs); + return true; + + default: + // For custom event types, there are no built-in rules, so the deserialization type is + // determined by the parameter declared on the delegate. + type = null; + return false; + } + } private static EventFieldInfo? InterpretEventFieldInfo(EventFieldInfo? fieldInfo) { @@ -111,6 +218,8 @@ private static EventArgs ParseEventArgsJson(ulong eventHandlerId, string eventAr return null; } + static T Deserialize(string json) => JsonSerializer.Deserialize(json, JsonSerializerOptionsProvider.Options)!; + private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson) { var changeArgs = Deserialize(eventArgsJson); diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index f887416cf935..f9125f300d01 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -16,3 +16,7 @@ Microsoft.AspNetCore.Components.Forms.InputTextArea.Element.set -> void *REMOVED*static Microsoft.AspNetCore.Components.Forms.BrowserFileExtensions.RequestImageFileAsync(this Microsoft.AspNetCore.Components.Forms.IBrowserFile! browserFile, string! format, int maxWith, int maxHeight) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Components.ElementReferenceExtensions.FocusAsync(this Microsoft.AspNetCore.Components.ElementReference elementReference, bool preventScroll) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Components.Forms.BrowserFileExtensions.RequestImageFileAsync(this Microsoft.AspNetCore.Components.Forms.IBrowserFile! browserFile, string! format, int maxWidth, int maxHeight) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor.EventName.get -> string! +Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor.EventName.set -> void +*REMOVED*Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor.EventArgsType.get -> string! +*REMOVED*Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor.EventArgsType.set -> void diff --git a/src/Components/Web/src/WebEventDescriptor.cs b/src/Components/Web/src/WebEventDescriptor.cs index 2e063b98f9e4..cc87d8f9d5fa 100644 --- a/src/Components/Web/src/WebEventDescriptor.cs +++ b/src/Components/Web/src/WebEventDescriptor.cs @@ -27,7 +27,7 @@ public sealed class WebEventDescriptor /// /// For framework use only. /// - public string EventArgsType { get; set; } = default!; + public string EventName { get; set; } = default!; /// /// For framework use only. diff --git a/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs index e31f92e8707a..c6eb32b09229 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs @@ -33,8 +33,8 @@ public static void NotifyLocationChanged(string uri, bool isInterceptedLink) [JSInvokable(nameof(DispatchEvent))] public static Task DispatchEvent(WebEventDescriptor eventDescriptor, string eventArgsJson) { - var webEvent = WebEventData.Parse(eventDescriptor, eventArgsJson); var renderer = RendererRegistry.Find(eventDescriptor.BrowserRendererId); + var webEvent = WebEventData.Parse(renderer, eventDescriptor, eventArgsJson); return renderer.DispatchEventAsync( webEvent.EventHandlerId, webEvent.EventFieldInfo, diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs index 5d37553b5264..812259b3b4f8 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs @@ -44,7 +44,7 @@ public async Task DispatchingAnInvalidEventArgument_DoesNotProduceWarnings() { BrowserRendererId = 0, EventHandlerId = 3, - EventArgsType = "mouse", + EventName = "click", }); // Act @@ -71,7 +71,7 @@ public async Task DispatchingAnInvalidEvent_DoesNotTriggerWarnings() { BrowserRendererId = 0, EventHandlerId = 1990, - EventArgsType = "mouse", + EventName = "click", }); var eventArgs = new MouseEventArgs diff --git a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs index 7333672c2a18..87747364bda6 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs @@ -440,7 +440,7 @@ public async Task DispatchingEventsWithInvalidEventArgs() { BrowserRendererId = 0, EventHandlerId = 6, - EventArgsType = "mouse", + EventName = "click", }; await Client.ExpectCircuitError(async () => @@ -478,7 +478,7 @@ public async Task DispatchingEventsWithInvalidEventHandlerId() { BrowserRendererId = 0, EventHandlerId = 1, - EventArgsType = "mouse", + EventName = "click", }; await Client.ExpectCircuitError(async () => From eb391dfdd27277410033212441c583455935611b Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 8 Feb 2021 17:52:51 +0000 Subject: [PATCH 05/25] Fix event mapping --- src/Components/Shared/src/WebEventData.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Components/Shared/src/WebEventData.cs b/src/Components/Shared/src/WebEventData.cs index 73830c548b4b..77b514c18a31 100644 --- a/src/Components/Shared/src/WebEventData.cs +++ b/src/Components/Shared/src/WebEventData.cs @@ -102,7 +102,7 @@ private static bool TryGetStandardWebEventArgsType(string eventName, [MaybeNullW case "copy": case "cut": case "paste": - type = typeof(EventArgs); + type = typeof(ClipboardEventArgs); return true; case "drag": @@ -119,7 +119,7 @@ private static bool TryGetStandardWebEventArgsType(string eventName, [MaybeNullW case "blur": case "focusin": case "focusout": - type = typeof(EventArgs); + type = typeof(FocusEventArgs); return true; case "keydown": From 60d67eb7db1ee51c8c51cc51048e6a6ace63f73e Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 9 Feb 2021 11:14:08 +0000 Subject: [PATCH 06/25] Simplify event dispatching on TypeScript side more. Eliminate unnecessary types. --- .../Web.JS/src/Rendering/BrowserRenderer.ts | 33 ++------ .../Web.JS/src/Rendering/EventDelegator.ts | 12 +-- .../Web.JS/src/Rendering/EventForDotNet.ts | 83 ++++++++----------- .../src/Rendering/RendererEventDispatcher.ts | 8 +- 4 files changed, 50 insertions(+), 86 deletions(-) diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts index 0980e9440689..9abee79b79d2 100644 --- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts @@ -1,6 +1,5 @@ import { RenderBatch, ArrayBuilderSegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch'; import { EventDelegator } from './EventDelegator'; -import { UIEventArgs } from './EventForDotNet'; import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd, permuteLogicalChildren, getClosestDomElement } from './LogicalElements'; import { applyCaptureIdToElement } from './ElementReferenceCapture'; import { EventFieldInfo } from './EventFieldInfo'; @@ -20,12 +19,13 @@ export class BrowserRenderer { private childComponentLocations: { [componentId: number]: LogicalElement } = {}; - private browserRendererId: number; - public constructor(browserRendererId: number) { - this.browserRendererId = browserRendererId; - this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs, eventFieldInfo) => { - raiseEvent(event, this.browserRendererId, eventHandlerId, eventArgs, eventFieldInfo); + this.eventDelegator = new EventDelegator((event, eventHandlerId, eventName, eventArgs, eventFieldInfo) => { + if (preventDefaultEvents[event.type]) { + event.preventDefault(); + } + + dispatchEvent({ browserRendererId, eventHandlerId, eventName, eventFieldInfo }, eventArgs); }); // We don't yet know whether or not navigation interception will be enabled, but in case it will be, @@ -491,27 +491,6 @@ function countDescendantFrames(batch: RenderBatch, frame: RenderTreeFrame): numb } } -function raiseEvent( - event: Event, - browserRendererId: number, - eventHandlerId: number, - eventArgs: UIEventArgs, - eventFieldInfo: EventFieldInfo | null -): void { - if (preventDefaultEvents[event.type]) { - event.preventDefault(); - } - - const eventDescriptor = { - browserRendererId, - eventHandlerId, - eventName: eventArgs.type, - eventFieldInfo: eventFieldInfo, - }; - - dispatchEvent(eventDescriptor, eventArgs); -} - function clearElement(element: Element) { let childNode: Node | null; while (childNode = element.firstChild) { diff --git a/src/Components/Web.JS/src/Rendering/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/EventDelegator.ts index 4595231b29fa..c0478e4d51ae 100644 --- a/src/Components/Web.JS/src/Rendering/EventDelegator.ts +++ b/src/Components/Web.JS/src/Rendering/EventDelegator.ts @@ -1,4 +1,4 @@ -import { fromDOMEvent, UIEventArgs } from './EventForDotNet'; +import { fromDOMEvent } from './EventForDotNet'; import { EventFieldInfo } from './EventFieldInfo'; const nonBubblingEvents = toLookup([ @@ -25,7 +25,7 @@ const nonBubblingEvents = toLookup([ const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']); export interface OnEventCallback { - (event: Event, eventHandlerId: number, eventArgs: UIEventArgs, eventFieldInfo: EventFieldInfo | null): void; + (event: Event, eventHandlerId: number, eventName: string, eventArgs: any, eventFieldInfo: EventFieldInfo | null): void; } // Responsible for adding/removing the eventInfo on an expando property on DOM elements, and @@ -107,7 +107,8 @@ export class EventDelegator { // Scan up the element hierarchy, looking for any matching registered event handlers let candidateElement = evt.target as Element | null; - let eventArgs: UIEventArgs | null = null; // Populate lazily + let eventArgs: any = null; // Populate lazily + let eventArgsIsPopulated = false; const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type); let stopPropagationWasRequested = false; while (candidateElement) { @@ -116,12 +117,13 @@ export class EventDelegator { const handlerInfo = handlerInfos.getHandler(evt.type); if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) { // We are going to raise an event for this element, so prepare info needed by the .NET code - if (!eventArgs) { + if (!eventArgsIsPopulated) { eventArgs = fromDOMEvent(evt); + eventArgsIsPopulated = true; } const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt); - this.onEvent(evt, handlerInfo.eventHandlerId, eventArgs, eventFieldInfo); + this.onEvent(evt, handlerInfo.eventHandlerId, evt.type, eventArgs, eventFieldInfo); } if (handlerInfos.stopPropagation(evt.type)) { diff --git a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts b/src/Components/Web.JS/src/Rendering/EventForDotNet.ts index 7d4da189ad6b..da8fb6c83f87 100644 --- a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts +++ b/src/Components/Web.JS/src/Rendering/EventForDotNet.ts @@ -1,4 +1,4 @@ -export function fromDOMEvent(event: Event): UIEventArgs { +export function fromDOMEvent(event: Event): any { switch (event.type) { case 'input': @@ -8,7 +8,7 @@ export function fromDOMEvent(event: Event): UIEventArgs { case 'copy': case 'cut': case 'paste': - return { type: event.type }; + return {}; case 'drag': case 'dragend': @@ -23,7 +23,7 @@ export function fromDOMEvent(event: Event): UIEventArgs { case 'blur': case 'focusin': case 'focusout': - return { type: event.type }; + return {}; case 'keydown': case 'keyup': @@ -76,33 +76,33 @@ export function fromDOMEvent(event: Event): UIEventArgs { return parseWheelEvent(event as WheelEvent); case 'toggle': - return { type: event.type }; + return {}; default: - return { type: event.type }; + return {}; } } -function parseChangeEvent(event: any): UIChangeEventArgs { +function parseChangeEvent(event: any): ChangeEventArgs { const element = event.target as Element; if (isTimeBasedInput(element)) { const normalizedValue = normalizeTimeBasedValue(element); - return { type: event.type, value: normalizedValue }; + return { value: normalizedValue }; } else { const targetIsCheckbox = isCheckbox(element); const newValue = targetIsCheckbox ? !!element['checked'] : element['value']; - return { type: event.type, value: newValue }; + return { value: newValue }; } } -function parseDragEvent(event: any): UIDragEventArgs { +function parseDragEvent(event: any): DragEventArgs { return { ...parseMouseEvent(event), dataTransfer: event.dataTransfer, }; } -function parseWheelEvent(event: WheelEvent): UIWheelEventArgs { +function parseWheelEvent(event: WheelEvent): WheelEventArgs { return { ...parseMouseEvent(event), deltaX: event.deltaX, @@ -112,9 +112,8 @@ function parseWheelEvent(event: WheelEvent): UIWheelEventArgs { }; } -function parseErrorEvent(event: ErrorEvent): UIErrorEventArgs { +function parseErrorEvent(event: ErrorEvent): ErrorEventArgs { return { - type: event.type, message: event.message, filename: event.filename, lineno: event.lineno, @@ -122,19 +121,18 @@ function parseErrorEvent(event: ErrorEvent): UIErrorEventArgs { }; } -function parseProgressEvent(event: ProgressEvent): UIProgressEventArgs { +function parseProgressEvent(event: ProgressEvent): ProgressEventArgs { return { - type: event.type, lengthComputable: event.lengthComputable, loaded: event.loaded, total: event.total, }; } -function parseTouchEvent(event: TouchEvent): UITouchEventArgs { +function parseTouchEvent(event: TouchEvent): TouchEventArgs { function parseTouch(touchList: TouchList) { - const touches: UITouchPoint[] = []; + const touches: TouchPoint[] = []; for (let i = 0; i < touchList.length; i++) { const touch = touchList[i]; @@ -152,7 +150,6 @@ function parseTouchEvent(event: TouchEvent): UITouchEventArgs { } return { - type: event.type, detail: event.detail, touches: parseTouch(event.touches), targetTouches: parseTouch(event.targetTouches), @@ -164,9 +161,8 @@ function parseTouchEvent(event: TouchEvent): UITouchEventArgs { }; } -function parseKeyboardEvent(event: KeyboardEvent): UIKeyboardEventArgs { +function parseKeyboardEvent(event: KeyboardEvent): KeyboardEventArgs { return { - type: event.type, key: event.key, code: event.code, location: event.location, @@ -178,7 +174,7 @@ function parseKeyboardEvent(event: KeyboardEvent): UIKeyboardEventArgs { }; } -function parsePointerEvent(event: PointerEvent): UIPointerEventArgs { +function parsePointerEvent(event: PointerEvent): PointerEventArgs { return { ...parseMouseEvent(event), pointerId: event.pointerId, @@ -192,9 +188,8 @@ function parsePointerEvent(event: PointerEvent): UIPointerEventArgs { }; } -function parseMouseEvent(event: MouseEvent): UIMouseEventArgs { +function parseMouseEvent(event: MouseEvent): MouseEventArgs { return { - type: event.type, detail: event.detail, screenX: event.screenX, screenY: event.screenY, @@ -247,20 +242,13 @@ function normalizeTimeBasedValue(element: HTMLInputElement): string { // The following interfaces must be kept in sync with the UIEventArgs C# classes -export interface UIEventArgs { - type: string; -} - -interface UIChangeEventArgs extends UIEventArgs { +interface ChangeEventArgs { value: string | boolean; } -interface UIClipboardEventArgs extends UIEventArgs { -} - -interface UIDragEventArgs extends UIEventArgs { +interface DragEventArgs { detail: number; - dataTransfer: UIDataTransfer; + dataTransfer: DataTransfer; screenX: number; screenY: number; clientX: number; @@ -273,20 +261,20 @@ interface UIDragEventArgs extends UIEventArgs { metaKey: boolean; } -interface UIDataTransfer { +interface DataTransfer { dropEffect: string; effectAllowed: string; files: string[]; - items: UIDataTransferItem[]; + items: DataTransferItem[]; types: string[]; } -interface UIDataTransferItem { +interface DataTransferItem { kind: string; type: string; } -interface UIErrorEventArgs extends UIEventArgs { +interface ErrorEventArgs { message: string; filename: string; lineno: number; @@ -296,10 +284,7 @@ interface UIErrorEventArgs extends UIEventArgs { // do that. https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent } -interface UIFocusEventArgs extends UIEventArgs { -} - -interface UIKeyboardEventArgs extends UIEventArgs { +interface KeyboardEventArgs { key: string; code: string; location: number; @@ -310,7 +295,7 @@ interface UIKeyboardEventArgs extends UIEventArgs { metaKey: boolean; } -interface UIMouseEventArgs extends UIEventArgs { +interface MouseEventArgs { detail: number; screenX: number; screenY: number; @@ -326,7 +311,7 @@ interface UIMouseEventArgs extends UIEventArgs { metaKey: boolean; } -interface UIPointerEventArgs extends UIMouseEventArgs { +interface PointerEventArgs extends MouseEventArgs { pointerId: number; width: number; height: number; @@ -337,24 +322,24 @@ interface UIPointerEventArgs extends UIMouseEventArgs { isPrimary: boolean; } -interface UIProgressEventArgs extends UIEventArgs { +interface ProgressEventArgs { lengthComputable: boolean; loaded: number; total: number; } -interface UITouchEventArgs extends UIEventArgs { +interface TouchEventArgs { detail: number; - touches: UITouchPoint[]; - targetTouches: UITouchPoint[]; - changedTouches: UITouchPoint[]; + touches: TouchPoint[]; + targetTouches: TouchPoint[]; + changedTouches: TouchPoint[]; ctrlKey: boolean; shiftKey: boolean; altKey: boolean; metaKey: boolean; } -interface UITouchPoint { +interface TouchPoint { identifier: number; screenX: number; screenY: number; @@ -364,7 +349,7 @@ interface UITouchPoint { pageY: number; } -interface UIWheelEventArgs extends UIMouseEventArgs { +interface WheelEventArgs extends MouseEventArgs { deltaX: number; deltaY: number; deltaZ: number; diff --git a/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts b/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts index 1b4870dac2bb..a18d9e17e8d4 100644 --- a/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts +++ b/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts @@ -1,11 +1,9 @@ import { EventDescriptor } from './BrowserRenderer'; -import { UIEventArgs } from './EventForDotNet'; - -type EventDispatcher = (eventDescriptor: EventDescriptor, eventArgs: UIEventArgs) => void; +type EventDispatcher = (eventDescriptor: EventDescriptor, eventArgs: any) => void; let eventDispatcherInstance: EventDispatcher; -export function dispatchEvent(eventDescriptor: EventDescriptor, eventArgs: UIEventArgs): void { +export function dispatchEvent(eventDescriptor: EventDescriptor, eventArgs: any): void { if (!eventDispatcherInstance) { throw new Error('eventDispatcher not initialized. Call \'setEventDispatcher\' to configure it.'); } @@ -13,6 +11,6 @@ export function dispatchEvent(eventDescriptor: EventDescriptor, eventArgs: UIEve eventDispatcherInstance(eventDescriptor, eventArgs); } -export function setEventDispatcher(newDispatcher: (eventDescriptor: EventDescriptor, eventArgs: UIEventArgs) => void): void { +export function setEventDispatcher(newDispatcher: (eventDescriptor: EventDescriptor, eventArgs: any) => void): void { eventDispatcherInstance = newDispatcher; } From f0a78bbdeb8e2043219b97c5ff393e17e68a4934 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 9 Feb 2021 11:16:12 +0000 Subject: [PATCH 07/25] Rename file for clarity --- .../src/Rendering/{EventForDotNet.ts => EventArgsFactory.ts} | 2 +- src/Components/Web.JS/src/Rendering/EventDelegator.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Components/Web.JS/src/Rendering/{EventForDotNet.ts => EventArgsFactory.ts} (99%) diff --git a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts b/src/Components/Web.JS/src/Rendering/EventArgsFactory.ts similarity index 99% rename from src/Components/Web.JS/src/Rendering/EventForDotNet.ts rename to src/Components/Web.JS/src/Rendering/EventArgsFactory.ts index da8fb6c83f87..1f690a4e8ba9 100644 --- a/src/Components/Web.JS/src/Rendering/EventForDotNet.ts +++ b/src/Components/Web.JS/src/Rendering/EventArgsFactory.ts @@ -1,4 +1,4 @@ -export function fromDOMEvent(event: Event): any { +export function createEventArgsFromDOMEvent(event: Event): any { switch (event.type) { case 'input': diff --git a/src/Components/Web.JS/src/Rendering/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/EventDelegator.ts index c0478e4d51ae..8b32b38049e3 100644 --- a/src/Components/Web.JS/src/Rendering/EventDelegator.ts +++ b/src/Components/Web.JS/src/Rendering/EventDelegator.ts @@ -1,4 +1,4 @@ -import { fromDOMEvent } from './EventForDotNet'; +import { createEventArgsFromDOMEvent } from './EventArgsFactory'; import { EventFieldInfo } from './EventFieldInfo'; const nonBubblingEvents = toLookup([ @@ -118,7 +118,7 @@ export class EventDelegator { if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) { // We are going to raise an event for this element, so prepare info needed by the .NET code if (!eventArgsIsPopulated) { - eventArgs = fromDOMEvent(evt); + eventArgs = createEventArgsFromDOMEvent(evt); eventArgsIsPopulated = true; } From 6d2694d28623f5ca9510e7eee29ece93c2a7a4a1 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 9 Feb 2021 11:21:42 +0000 Subject: [PATCH 08/25] Migrate "preventDefault for submit" behavior into EventDelegator because that's where other similar responsibilities are --- .../Web.JS/src/Rendering/BrowserRenderer.ts | 7 +------ .../Web.JS/src/Rendering/EventDelegator.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts index 9abee79b79d2..41c5c1eeeef5 100644 --- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts @@ -8,7 +8,6 @@ import { attachToEventDelegator as attachNavigationManagerToEventDelegator } fro const selectValuePropname = '_blazorSelectValue'; const sharedTemplateElemForParsing = document.createElement('template'); const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g'); -const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true }; const rootComponentsPendingFirstRender: { [componentId: number]: LogicalElement } = {}; const internalAttributeNamePrefix = '__internal_'; const eventPreventDefaultAttributeNamePrefix = 'preventDefault_'; @@ -20,11 +19,7 @@ export class BrowserRenderer { private childComponentLocations: { [componentId: number]: LogicalElement } = {}; public constructor(browserRendererId: number) { - this.eventDelegator = new EventDelegator((event, eventHandlerId, eventName, eventArgs, eventFieldInfo) => { - if (preventDefaultEvents[event.type]) { - event.preventDefault(); - } - + this.eventDelegator = new EventDelegator((eventHandlerId, eventName, eventArgs, eventFieldInfo) => { dispatchEvent({ browserRendererId, eventHandlerId, eventName, eventFieldInfo }, eventArgs); }); diff --git a/src/Components/Web.JS/src/Rendering/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/EventDelegator.ts index 8b32b38049e3..2a1f3d6dea6d 100644 --- a/src/Components/Web.JS/src/Rendering/EventDelegator.ts +++ b/src/Components/Web.JS/src/Rendering/EventDelegator.ts @@ -22,10 +22,12 @@ const nonBubblingEvents = toLookup([ 'DOMNodeRemovedFromDocument', ]); +const alwaysPreventDefaultEvents: { [eventType: string]: boolean } = { submit: true }; + const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']); export interface OnEventCallback { - (event: Event, eventHandlerId: number, eventName: string, eventArgs: any, eventFieldInfo: EventFieldInfo | null): void; + (eventHandlerId: number, eventName: string, eventArgs: any, eventFieldInfo: EventFieldInfo | null): void; } // Responsible for adding/removing the eventInfo on an expando property on DOM elements, and @@ -122,8 +124,14 @@ export class EventDelegator { eventArgsIsPopulated = true; } + // For certain built-in events, having any .NET handler implicitly means we will prevent + // the browser's default behavior + if (alwaysPreventDefaultEvents.hasOwnProperty(evt.type)) { + evt.preventDefault(); + } + const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt); - this.onEvent(evt, handlerInfo.eventHandlerId, evt.type, eventArgs, eventFieldInfo); + this.onEvent(handlerInfo.eventHandlerId, evt.type, eventArgs, eventFieldInfo); } if (handlerInfos.stopPropagation(evt.type)) { From 1792351788042d12774c5f7122bd96be5124b9c9 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 9 Feb 2021 11:28:05 +0000 Subject: [PATCH 09/25] Put event-centric files into an Events directory for more clarity --- src/Components/Web.JS/src/Boot.Server.ts | 2 +- src/Components/Web.JS/src/Boot.WebAssembly.ts | 2 +- .../Web.JS/src/Rendering/BrowserRenderer.ts | 12 ++---------- .../src/Rendering/{ => Events}/EventArgsFactory.ts | 0 .../src/Rendering/{ => Events}/EventDelegator.ts | 0 .../EventDispatcher.ts} | 10 +++++++++- .../src/Rendering/{ => Events}/EventFieldInfo.ts | 0 .../Web.JS/src/Services/NavigationManager.ts | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) rename src/Components/Web.JS/src/Rendering/{ => Events}/EventArgsFactory.ts (100%) rename src/Components/Web.JS/src/Rendering/{ => Events}/EventDelegator.ts (100%) rename src/Components/Web.JS/src/Rendering/{RendererEventDispatcher.ts => Events/EventDispatcher.ts} (73%) rename src/Components/Web.JS/src/Rendering/{ => Events}/EventFieldInfo.ts (100%) diff --git a/src/Components/Web.JS/src/Boot.Server.ts b/src/Components/Web.JS/src/Boot.Server.ts index ca6ddf28606d..8fd6135e0be7 100644 --- a/src/Components/Web.JS/src/Boot.Server.ts +++ b/src/Components/Web.JS/src/Boot.Server.ts @@ -8,7 +8,7 @@ import { RenderQueue } from './Platform/Circuits/RenderQueue'; import { ConsoleLogger } from './Platform/Logging/Loggers'; import { LogLevel, Logger } from './Platform/Logging/Logger'; import { CircuitDescriptor } from './Platform/Circuits/CircuitManager'; -import { setEventDispatcher } from './Rendering/RendererEventDispatcher'; +import { setEventDispatcher } from './Rendering/Events/EventDispatcher'; import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions'; import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler'; import { attachRootComponentToLogicalElement } from './Rendering/Renderer'; diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.ts b/src/Components/Web.JS/src/Boot.WebAssembly.ts index d5acaae49b4a..235a5b9ecd10 100644 --- a/src/Components/Web.JS/src/Boot.WebAssembly.ts +++ b/src/Components/Web.JS/src/Boot.WebAssembly.ts @@ -5,7 +5,7 @@ import { monoPlatform } from './Platform/Mono/MonoPlatform'; import { renderBatch, getRendererer, attachRootComponentToElement, attachRootComponentToLogicalElement } from './Rendering/Renderer'; import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch'; import { shouldAutoStart } from './BootCommon'; -import { setEventDispatcher } from './Rendering/RendererEventDispatcher'; +import { setEventDispatcher } from './Rendering/Events/EventDispatcher'; import { WebAssemblyResourceLoader } from './Platform/WebAssemblyResourceLoader'; import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader'; import { BootConfigResult } from './Platform/BootConfig'; diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts index 41c5c1eeeef5..5db19618b7e5 100644 --- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts @@ -1,9 +1,8 @@ import { RenderBatch, ArrayBuilderSegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch'; -import { EventDelegator } from './EventDelegator'; +import { EventDelegator } from './Events/EventDelegator'; import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd, permuteLogicalChildren, getClosestDomElement } from './LogicalElements'; import { applyCaptureIdToElement } from './ElementReferenceCapture'; -import { EventFieldInfo } from './EventFieldInfo'; -import { dispatchEvent } from './RendererEventDispatcher'; +import { dispatchEvent } from './Events/EventDispatcher'; import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager'; const selectValuePropname = '_blazorSelectValue'; const sharedTemplateElemForParsing = document.createElement('template'); @@ -454,13 +453,6 @@ export interface ComponentDescriptor { end: Node; } -export interface EventDescriptor { - browserRendererId: number; - eventHandlerId: number; - eventName: string; - eventFieldInfo: EventFieldInfo | null; -} - function parseMarkup(markup: string, isSvg: boolean) { if (isSvg) { sharedSvgElemForParsing.innerHTML = markup || ' '; diff --git a/src/Components/Web.JS/src/Rendering/EventArgsFactory.ts b/src/Components/Web.JS/src/Rendering/Events/EventArgsFactory.ts similarity index 100% rename from src/Components/Web.JS/src/Rendering/EventArgsFactory.ts rename to src/Components/Web.JS/src/Rendering/Events/EventArgsFactory.ts diff --git a/src/Components/Web.JS/src/Rendering/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts similarity index 100% rename from src/Components/Web.JS/src/Rendering/EventDelegator.ts rename to src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts diff --git a/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts b/src/Components/Web.JS/src/Rendering/Events/EventDispatcher.ts similarity index 73% rename from src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts rename to src/Components/Web.JS/src/Rendering/Events/EventDispatcher.ts index a18d9e17e8d4..244052eabbf3 100644 --- a/src/Components/Web.JS/src/Rendering/RendererEventDispatcher.ts +++ b/src/Components/Web.JS/src/Rendering/Events/EventDispatcher.ts @@ -1,4 +1,12 @@ -import { EventDescriptor } from './BrowserRenderer'; +import { EventFieldInfo } from './EventFieldInfo'; + +export interface EventDescriptor { + browserRendererId: number; + eventHandlerId: number; + eventName: string; + eventFieldInfo: EventFieldInfo | null; +} + type EventDispatcher = (eventDescriptor: EventDescriptor, eventArgs: any) => void; let eventDispatcherInstance: EventDispatcher; diff --git a/src/Components/Web.JS/src/Rendering/EventFieldInfo.ts b/src/Components/Web.JS/src/Rendering/Events/EventFieldInfo.ts similarity index 100% rename from src/Components/Web.JS/src/Rendering/EventFieldInfo.ts rename to src/Components/Web.JS/src/Rendering/Events/EventFieldInfo.ts diff --git a/src/Components/Web.JS/src/Services/NavigationManager.ts b/src/Components/Web.JS/src/Services/NavigationManager.ts index 091481be13e3..66e4aad32c90 100644 --- a/src/Components/Web.JS/src/Services/NavigationManager.ts +++ b/src/Components/Web.JS/src/Services/NavigationManager.ts @@ -1,6 +1,6 @@ import '@microsoft/dotnet-js-interop'; import { resetScrollAfterNextBatch } from '../Rendering/Renderer'; -import { EventDelegator } from '../Rendering/EventDelegator'; +import { EventDelegator } from '../Rendering/Events/EventDelegator'; let hasEnabledNavigationInterception = false; let hasRegisteredNavigationEventListeners = false; From badc38f41c25e3036bcc6e488f506a1f6a6550c6 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 9 Feb 2021 11:33:24 +0000 Subject: [PATCH 10/25] Disentangle event dispatch from BrowserRenderer --- .../Web.JS/src/Rendering/BrowserRenderer.ts | 5 +---- .../Web.JS/src/Rendering/Events/EventDelegator.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts index 5db19618b7e5..d541724dc547 100644 --- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts @@ -2,7 +2,6 @@ import { RenderBatch, ArrayBuilderSegment, RenderTreeEdit, RenderTreeFrame, Edit import { EventDelegator } from './Events/EventDelegator'; import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd, permuteLogicalChildren, getClosestDomElement } from './LogicalElements'; import { applyCaptureIdToElement } from './ElementReferenceCapture'; -import { dispatchEvent } from './Events/EventDispatcher'; import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager'; const selectValuePropname = '_blazorSelectValue'; const sharedTemplateElemForParsing = document.createElement('template'); @@ -18,9 +17,7 @@ export class BrowserRenderer { private childComponentLocations: { [componentId: number]: LogicalElement } = {}; public constructor(browserRendererId: number) { - this.eventDelegator = new EventDelegator((eventHandlerId, eventName, eventArgs, eventFieldInfo) => { - dispatchEvent({ browserRendererId, eventHandlerId, eventName, eventFieldInfo }, eventArgs); - }); + this.eventDelegator = new EventDelegator(browserRendererId); // We don't yet know whether or not navigation interception will be enabled, but in case it will be, // we wire up the navigation manager to the event delegator so it has the option to participate diff --git a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts index 2a1f3d6dea6d..403d2d667f8e 100644 --- a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts +++ b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts @@ -1,5 +1,6 @@ import { createEventArgsFromDOMEvent } from './EventArgsFactory'; import { EventFieldInfo } from './EventFieldInfo'; +import { dispatchEvent } from './EventDispatcher'; const nonBubblingEvents = toLookup([ 'abort', @@ -26,10 +27,6 @@ const alwaysPreventDefaultEvents: { [eventType: string]: boolean } = { submit: t const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']); -export interface OnEventCallback { - (eventHandlerId: number, eventName: string, eventArgs: any, eventFieldInfo: EventFieldInfo | null): void; -} - // Responsible for adding/removing the eventInfo on an expando property on DOM elements, and // calling an EventInfoStore that deals with registering/unregistering the underlying delegated // event listeners as required (and also maps actual events back to the given callback). @@ -42,7 +39,7 @@ export class EventDelegator { private eventInfoStore: EventInfoStore; - constructor(private onEvent: OnEventCallback) { + constructor(private browserRendererId: number) { const eventDelegatorId = ++EventDelegator.nextEventDelegatorId; this.eventsCollectionKey = `_blazorEvents_${eventDelegatorId}`; this.eventInfoStore = new EventInfoStore(this.onGlobalEvent.bind(this)); @@ -130,8 +127,12 @@ export class EventDelegator { evt.preventDefault(); } - const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt); - this.onEvent(handlerInfo.eventHandlerId, evt.type, eventArgs, eventFieldInfo); + dispatchEvent({ + browserRendererId: this.browserRendererId, + eventHandlerId: handlerInfo.eventHandlerId, + eventName: evt.type, + eventFieldInfo: EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt) + }, eventArgs); } if (handlerInfos.stopPropagation(evt.type)) { From c832baad08ca5e321deb04852c34d47370cfc2b1 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 9 Feb 2021 11:55:26 +0000 Subject: [PATCH 11/25] Update comment --- src/Components/Shared/src/WebEventData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Shared/src/WebEventData.cs b/src/Components/Shared/src/WebEventData.cs index 77b514c18a31..ca3cc6ac4332 100644 --- a/src/Components/Shared/src/WebEventData.cs +++ b/src/Components/Shared/src/WebEventData.cs @@ -90,7 +90,7 @@ private static bool TryGetStandardWebEventArgsType(string eventName, [MaybeNullW // an event handler as receiving EventArgs, and have it actually receive a subclass at runtime // depending on the event that was raised. // - // The following list should remain in sync with EventForDotNet.ts. + // The following list should remain in sync with EventArgsFactory.ts. switch (eventName) { From 08967e813ca5104d4aa7de17a56a2e12cf5d54c3 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 9 Feb 2021 13:00:14 +0000 Subject: [PATCH 12/25] Add a cache for the handler->argstype lookup --- .../src/RenderTree/EventArgsTypeCache.cs | 42 +++++++++++++++++++ .../Components/src/RenderTree/Renderer.cs | 26 ++---------- 2 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 src/Components/Components/src/RenderTree/EventArgsTypeCache.cs diff --git a/src/Components/Components/src/RenderTree/EventArgsTypeCache.cs b/src/Components/Components/src/RenderTree/EventArgsTypeCache.cs new file mode 100644 index 000000000000..314545941589 --- /dev/null +++ b/src/Components/Components/src/RenderTree/EventArgsTypeCache.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Reflection; + +namespace Microsoft.AspNetCore.Components.RenderTree +{ + internal static class EventArgsTypeCache + { + private static ConcurrentDictionary Cache = new ConcurrentDictionary(); + + public static Type GetEventArgsType(MethodInfo methodInfo) + { + return Cache.GetOrAdd(methodInfo, methodInfo => + { + var parameterInfos = methodInfo.GetParameters(); + if (parameterInfos.Length == 0) + { + return typeof(EventArgs); + } + else if (parameterInfos.Length > 1) + { + throw new InvalidOperationException($"The method {methodInfo} cannot be used as an event handler because it declares more than one parameter."); + } + else + { + var declaredType = parameterInfos[0].ParameterType; + if (typeof(EventArgs).IsAssignableFrom(declaredType)) + { + return declaredType; + } + else + { + throw new InvalidOperationException($"The event handler parameter type {declaredType.FullName} for event must inherit from {typeof(EventArgs).FullName}."); + } + } + }); + } + } +} diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 4b2232dc93d0..8e058f1da9a3 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -294,33 +294,15 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie /// The parameter type expected by the event handler. Normally this is a subclass of . public Type GetEventArgsType(ulong eventHandlerId) { - var callback = GetRequiredEventCallback(eventHandlerId); + var methodInfo = GetRequiredEventCallback(eventHandlerId).Delegate?.Method; // The DispatchEventAsync code paths allow for the case where Delegate or its method // is null, and in this case the event receiver just receives null. This won't happen // under normal circumstances, but to avoid creating a new failure scenario, allow for // that edge case here too. - var parameterInfos = callback.Delegate?.Method.GetParameters(); - if (parameterInfos == null || parameterInfos.Length == 0) - { - return typeof(EventArgs); - } - else if (parameterInfos.Length > 1) - { - throw new InvalidOperationException($"The event handler for event {eventHandlerId} declares more than one parameter. Only one is supported."); - } - else - { - var declaredType = parameterInfos[0].ParameterType; - if (typeof(EventArgs).IsAssignableFrom(declaredType)) - { - return declaredType; - } - else - { - throw new InvalidOperationException($"The event handler parameter type {declaredType.FullName} for event must inherit from {typeof(EventArgs).FullName}."); - } - } + return methodInfo == null + ? typeof(EventArgs) + : EventArgsTypeCache.GetEventArgsType(methodInfo); } internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId) From 66f7dba0067b72a8606a0ed3408c8865508287d9 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 9 Feb 2021 14:51:17 +0000 Subject: [PATCH 13/25] Create a registry of event types so we'll be able to add custom ones later --- .../src/Rendering/Events/EventDelegator.ts | 5 +- .../{EventArgsFactory.ts => EventTypes.ts} | 256 ++++++++---------- 2 files changed, 121 insertions(+), 140 deletions(-) rename src/Components/Web.JS/src/Rendering/Events/{EventArgsFactory.ts => EventTypes.ts} (60%) diff --git a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts index 403d2d667f8e..3ea64147ebd2 100644 --- a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts +++ b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts @@ -1,6 +1,6 @@ -import { createEventArgsFromDOMEvent } from './EventArgsFactory'; import { EventFieldInfo } from './EventFieldInfo'; import { dispatchEvent } from './EventDispatcher'; +import { getEventTypeOptions } from './EventTypes'; const nonBubblingEvents = toLookup([ 'abort', @@ -117,7 +117,8 @@ export class EventDelegator { if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) { // We are going to raise an event for this element, so prepare info needed by the .NET code if (!eventArgsIsPopulated) { - eventArgs = createEventArgsFromDOMEvent(evt); + const eventOptions = getEventTypeOptions(evt.type); + eventArgs = eventOptions.createEventArgs(evt); eventArgsIsPopulated = true; } diff --git a/src/Components/Web.JS/src/Rendering/Events/EventArgsFactory.ts b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts similarity index 60% rename from src/Components/Web.JS/src/Rendering/Events/EventArgsFactory.ts rename to src/Components/Web.JS/src/Rendering/Events/EventTypes.ts index 1f690a4e8ba9..938441cbb772 100644 --- a/src/Components/Web.JS/src/Rendering/Events/EventArgsFactory.ts +++ b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts @@ -1,89 +1,64 @@ -export function createEventArgsFromDOMEvent(event: Event): any { - switch (event.type) { - - case 'input': - case 'change': - return parseChangeEvent(event); - - case 'copy': - case 'cut': - case 'paste': - return {}; - - case 'drag': - case 'dragend': - case 'dragenter': - case 'dragleave': - case 'dragover': - case 'dragstart': - case 'drop': - return parseDragEvent(event); - - case 'focus': - case 'blur': - case 'focusin': - case 'focusout': - return {}; - - case 'keydown': - case 'keyup': - case 'keypress': - return parseKeyboardEvent(event as KeyboardEvent); - - case 'contextmenu': - case 'click': - case 'mouseover': - case 'mouseout': - case 'mousemove': - case 'mousedown': - case 'mouseup': - case 'dblclick': - return parseMouseEvent(event as MouseEvent); - - case 'error': - return parseErrorEvent(event as ErrorEvent); - - case 'loadstart': - case 'timeout': - case 'abort': - case 'load': - case 'loadend': - case 'progress': - return parseProgressEvent(event as ProgressEvent); - - case 'touchcancel': - case 'touchend': - case 'touchmove': - case 'touchenter': - case 'touchleave': - case 'touchstart': - return parseTouchEvent(event as TouchEvent); - - case 'gotpointercapture': - case 'lostpointercapture': - case 'pointercancel': - case 'pointerdown': - case 'pointerenter': - case 'pointerleave': - case 'pointermove': - case 'pointerout': - case 'pointerover': - case 'pointerup': - return parsePointerEvent(event as PointerEvent); - - case 'wheel': - case 'mousewheel': - return parseWheelEvent(event as WheelEvent); - - case 'toggle': - return {}; - - default: - return {}; - } +interface EventTypeOptions { + browserEventName?: string; + createEventArgs: (event: Event) => any; +} + +const eventTypeRegistry: Map = new Map(); +const emptyEventArgsOptions: EventTypeOptions = { + createEventArgs: () => ({}) +}; + +export function getEventTypeOptions(eventTypeName: string): EventTypeOptions { + return eventTypeRegistry.get(eventTypeName) || emptyEventArgsOptions; +} + +function registerBuiltInEventType(eventNames: string[], options: EventTypeOptions) { + eventNames.forEach(eventName => eventTypeRegistry.set(eventName, options)); } -function parseChangeEvent(event: any): ChangeEventArgs { +registerBuiltInEventType(['input', 'change'], { + createEventArgs: parseChangeEvent +}); + +registerBuiltInEventType(['copy', 'cut', 'paste'], emptyEventArgsOptions); + +registerBuiltInEventType(['drag', 'dragend', 'dragenter', 'dragleave', 'dragover', 'dragstart', 'drop'], { + createEventArgs: e => parseDragEvent(e as DragEvent) +}); + +registerBuiltInEventType(['focus', 'blur', 'focusin', 'focusout'], emptyEventArgsOptions); + +registerBuiltInEventType(['keydown', 'keyup', 'keypress'], { + createEventArgs: e => parseKeyboardEvent(e as KeyboardEvent) +}); + +registerBuiltInEventType(['contextmenu', 'click', 'mouseover', 'mouseout', 'mousemove', 'mousedown', 'mouseup', 'dblclick'], { + createEventArgs: e => parseMouseEvent(e as MouseEvent) +}); + +registerBuiltInEventType(['error'], { + createEventArgs: e => parseErrorEvent(e as ErrorEvent) +}); + +registerBuiltInEventType(['loadstart', 'timeout', 'abort', 'load', 'loadend', 'progress'], { + createEventArgs: e => parseProgressEvent(e as ProgressEvent) +}); + +registerBuiltInEventType(['touchcancel', 'touchend', 'touchmove', 'touchenter', 'touchleave', 'touchstart'], { + createEventArgs: e => parseTouchEvent(e as TouchEvent) +}); + +registerBuiltInEventType(['gotpointercapture', 'lostpointercapture', 'pointercancel', 'pointerdown', 'pointerenter', 'pointerleave', 'pointermove', 'pointerout', 'pointerover', 'pointerup'], { + createEventArgs: e => parsePointerEvent(e as PointerEvent) +}); + +registerBuiltInEventType(['wheel', 'mousewheel'], { + createEventArgs: e => parseWheelEvent(e as WheelEvent) +}); + +registerBuiltInEventType(['toggle'], emptyEventArgsOptions); + +function parseChangeEvent(event: Event): ChangeEventArgs { const element = event.target as Element; if (isTimeBasedInput(element)) { const normalizedValue = normalizeTimeBasedValue(element); @@ -95,13 +70,6 @@ function parseChangeEvent(event: any): ChangeEventArgs { } } -function parseDragEvent(event: any): DragEventArgs { - return { - ...parseMouseEvent(event), - dataTransfer: event.dataTransfer, - }; -} - function parseWheelEvent(event: WheelEvent): WheelEventArgs { return { ...parseMouseEvent(event), @@ -112,43 +80,21 @@ function parseWheelEvent(event: WheelEvent): WheelEventArgs { }; } -function parseErrorEvent(event: ErrorEvent): ErrorEventArgs { - return { - message: event.message, - filename: event.filename, - lineno: event.lineno, - colno: event.colno, - }; -} - -function parseProgressEvent(event: ProgressEvent): ProgressEventArgs { +function parsePointerEvent(event: PointerEvent): PointerEventArgs { return { - lengthComputable: event.lengthComputable, - loaded: event.loaded, - total: event.total, + ...parseMouseEvent(event), + pointerId: event.pointerId, + width: event.width, + height: event.height, + pressure: event.pressure, + tiltX: event.tiltX, + tiltY: event.tiltY, + pointerType: event.pointerType, + isPrimary: event.isPrimary, }; } function parseTouchEvent(event: TouchEvent): TouchEventArgs { - - function parseTouch(touchList: TouchList) { - const touches: TouchPoint[] = []; - - for (let i = 0; i < touchList.length; i++) { - const touch = touchList[i]; - touches.push({ - identifier: touch.identifier, - clientX: touch.clientX, - clientY: touch.clientY, - screenX: touch.screenX, - screenY: touch.screenY, - pageX: touch.pageX, - pageY: touch.pageY, - }); - } - return touches; - } - return { detail: event.detail, touches: parseTouch(event.touches), @@ -161,6 +107,23 @@ function parseTouchEvent(event: TouchEvent): TouchEventArgs { }; } +function parseProgressEvent(event: ProgressEvent): ProgressEventArgs { + return { + lengthComputable: event.lengthComputable, + loaded: event.loaded, + total: event.total, + }; +} + +function parseErrorEvent(event: ErrorEvent): ErrorEventArgs { + return { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + }; +} + function parseKeyboardEvent(event: KeyboardEvent): KeyboardEventArgs { return { key: event.key, @@ -174,20 +137,37 @@ function parseKeyboardEvent(event: KeyboardEvent): KeyboardEventArgs { }; } -function parsePointerEvent(event: PointerEvent): PointerEventArgs { +function parseDragEvent(event: DragEvent): DragEventArgs { return { ...parseMouseEvent(event), - pointerId: event.pointerId, - width: event.width, - height: event.height, - pressure: event.pressure, - tiltX: event.tiltX, - tiltY: event.tiltY, - pointerType: event.pointerType, - isPrimary: event.isPrimary, + dataTransfer: event.dataTransfer ? { + dropEffect: event.dataTransfer.dropEffect, + effectAllowed: event.dataTransfer.effectAllowed, + files: Array.from(event.dataTransfer.files).map(f => f.name), + items: Array.from(event.dataTransfer.items).map(i => ({ kind: i.kind, type: i.type })), + types: event.dataTransfer.types, + } : null, }; } +function parseTouch(touchList: TouchList): TouchPoint[] { + const touches: TouchPoint[] = []; + + for (let i = 0; i < touchList.length; i++) { + const touch = touchList[i]; + touches.push({ + identifier: touch.identifier, + clientX: touch.clientX, + clientY: touch.clientY, + screenX: touch.screenX, + screenY: touch.screenY, + pageX: touch.pageX, + pageY: touch.pageY, + }); + } + return touches; +} + function parseMouseEvent(event: MouseEvent): MouseEventArgs { return { detail: event.detail, @@ -240,7 +220,7 @@ function normalizeTimeBasedValue(element: HTMLInputElement): string { throw new Error(`Invalid element type '${type}'.`); } -// The following interfaces must be kept in sync with the UIEventArgs C# classes +// The following interfaces must be kept in sync with the EventArgs C# classes interface ChangeEventArgs { value: string | boolean; @@ -248,7 +228,7 @@ interface ChangeEventArgs { interface DragEventArgs { detail: number; - dataTransfer: DataTransfer; + dataTransfer: DataTransferEventArgs | null; screenX: number; screenY: number; clientX: number; @@ -261,12 +241,12 @@ interface DragEventArgs { metaKey: boolean; } -interface DataTransfer { +interface DataTransferEventArgs { dropEffect: string; effectAllowed: string; - files: string[]; - items: DataTransferItem[]; - types: string[]; + files: readonly string[]; + items: readonly DataTransferItem[]; + types: readonly string[]; } interface DataTransferItem { From c9ca4b8cced7c1528395d6474be2fb463e2b7e6e Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 9 Feb 2021 15:02:30 +0000 Subject: [PATCH 14/25] Public API for registering custom event types --- src/Components/Web.JS/src/GlobalExports.ts | 2 ++ .../src/Rendering/Events/EventDelegator.ts | 2 +- .../Web.JS/src/Rendering/Events/EventTypes.ts | 24 ++++++++++++++----- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Components/Web.JS/src/GlobalExports.ts b/src/Components/Web.JS/src/GlobalExports.ts index facb2eea445d..1443192e477a 100644 --- a/src/Components/Web.JS/src/GlobalExports.ts +++ b/src/Components/Web.JS/src/GlobalExports.ts @@ -1,10 +1,12 @@ import { navigateTo, internalFunctions as navigationManagerInternalFunctions } from './Services/NavigationManager'; import { domFunctions } from './DomWrapper'; import { Virtualize } from './Virtualize'; +import { registerCustomEventType } from './Rendering/Events/EventTypes'; // Make the following APIs available in global scope for invocation from JS window['Blazor'] = { navigateTo, + registerCustomEventType, _internal: { navigationManager: navigationManagerInternalFunctions, diff --git a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts index 3ea64147ebd2..30e9f8af96c7 100644 --- a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts +++ b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts @@ -118,7 +118,7 @@ export class EventDelegator { // We are going to raise an event for this element, so prepare info needed by the .NET code if (!eventArgsIsPopulated) { const eventOptions = getEventTypeOptions(evt.type); - eventArgs = eventOptions.createEventArgs(evt); + eventArgs = eventOptions.createEventArgs ? eventOptions.createEventArgs(evt) : {}; eventArgsIsPopulated = true; } diff --git a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts index 938441cbb772..f641b814d5cc 100644 --- a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts +++ b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts @@ -1,15 +1,27 @@ interface EventTypeOptions { browserEventName?: string; - createEventArgs: (event: Event) => any; + createEventArgs?: (event: Event) => any; } const eventTypeRegistry: Map = new Map(); -const emptyEventArgsOptions: EventTypeOptions = { - createEventArgs: () => ({}) -}; +const emptyEventArgsOptions: EventTypeOptions = {}; -export function getEventTypeOptions(eventTypeName: string): EventTypeOptions { - return eventTypeRegistry.get(eventTypeName) || emptyEventArgsOptions; +export function registerCustomEventType(eventName: string, options: EventTypeOptions): void { + if (!options) { + throw new Error('The options parameter is required.'); + } + + // There can't be more than one registration for the same event name because then we wouldn't + // know which eventargs data to supply. + if (eventTypeRegistry.has(eventName)) { + throw new Error(`The event '${eventName}' is already registered.`); + } + + eventTypeRegistry.set(eventName, options); +} + +export function getEventTypeOptions(eventName: string): EventTypeOptions { + return eventTypeRegistry.get(eventName) || emptyEventArgsOptions; } function registerBuiltInEventType(eventNames: string[], options: EventTypeOptions) { From 11cbd2035004ef1511fd954a73e17069b106f349 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 10 Feb 2021 11:23:32 +0000 Subject: [PATCH 15/25] Dispatch events to any registered event type aliases too --- .../src/Rendering/Events/EventDelegator.ts | 58 ++++++++++++------- .../Web.JS/src/Rendering/Events/EventTypes.ts | 28 +++++++-- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts index 30e9f8af96c7..1683f1f74547 100644 --- a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts +++ b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts @@ -1,6 +1,6 @@ import { EventFieldInfo } from './EventFieldInfo'; import { dispatchEvent } from './EventDispatcher'; -import { getEventTypeOptions } from './EventTypes'; +import { getEventNameAliases, getEventTypeOptions } from './EventTypes'; const nonBubblingEvents = toLookup([ 'abort', @@ -104,54 +104,68 @@ export class EventDelegator { return; } + // Always dispatch to any listeners for the original underlying browser event name + this.dispatchGlobalEventToAllElements(evt.type, evt); + + // If this event name has aliases, dispatch for those listeners too + const eventNameAliases = getEventNameAliases(evt.type); + eventNameAliases && eventNameAliases.forEach(alias => + this.dispatchGlobalEventToAllElements(alias, evt)); + + // Special case for navigation interception + if (evt.type === 'click') { + this.afterClickCallbacks.forEach(callback => callback(evt as MouseEvent)); + } + } + + private dispatchGlobalEventToAllElements(eventName: string, browserEvent: Event) { + // Note that 'eventName' can be an alias. For example, eventName may be 'click.special' + // while browserEvent.type may be 'click'. + // Scan up the element hierarchy, looking for any matching registered event handlers - let candidateElement = evt.target as Element | null; + let candidateElement = browserEvent.target as Element | null; let eventArgs: any = null; // Populate lazily let eventArgsIsPopulated = false; - const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type); + const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(eventName); let stopPropagationWasRequested = false; while (candidateElement) { const handlerInfos = this.getEventHandlerInfosForElement(candidateElement, false); if (handlerInfos) { - const handlerInfo = handlerInfos.getHandler(evt.type); - if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) { + const handlerInfo = handlerInfos.getHandler(eventName); + if (handlerInfo && !eventIsDisabledOnElement(candidateElement, browserEvent.type)) { // We are going to raise an event for this element, so prepare info needed by the .NET code if (!eventArgsIsPopulated) { - const eventOptions = getEventTypeOptions(evt.type); - eventArgs = eventOptions.createEventArgs ? eventOptions.createEventArgs(evt) : {}; + const eventOptions = getEventTypeOptions(eventName); + eventArgs = eventOptions.createEventArgs ? eventOptions.createEventArgs(browserEvent) : null; eventArgsIsPopulated = true; } // For certain built-in events, having any .NET handler implicitly means we will prevent - // the browser's default behavior - if (alwaysPreventDefaultEvents.hasOwnProperty(evt.type)) { - evt.preventDefault(); + // the browser's default behavior. This has to be based on the original browser event type name, + // not any alias (e.g., if you create a custom 'submit' variant, it should still preventDefault). + if (alwaysPreventDefaultEvents.hasOwnProperty(browserEvent.type)) { + browserEvent.preventDefault(); } dispatchEvent({ browserRendererId: this.browserRendererId, eventHandlerId: handlerInfo.eventHandlerId, - eventName: evt.type, - eventFieldInfo: EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt) + eventName: eventName, + eventFieldInfo: EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, browserEvent) }, eventArgs); } - if (handlerInfos.stopPropagation(evt.type)) { + if (handlerInfos.stopPropagation(eventName)) { stopPropagationWasRequested = true; } - if (handlerInfos.preventDefault(evt.type)) { - evt.preventDefault(); + if (handlerInfos.preventDefault(eventName)) { + browserEvent.preventDefault(); } } candidateElement = (eventIsNonBubbling || stopPropagationWasRequested) ? null : candidateElement.parentElement; } - - // Special case for navigation interception - if (evt.type === 'click') { - this.afterClickCallbacks.forEach(callback => callback(evt as MouseEvent)); - } } private getEventHandlerInfosForElement(element: Element, createIfNeeded: boolean): EventHandlerInfosForElement | null { @@ -293,10 +307,10 @@ function toLookup(items: string[]): { [key: string]: boolean } { return result; } -function eventIsDisabledOnElement(element: Element, eventName: string): boolean { +function eventIsDisabledOnElement(element: Element, rawBrowserEventName: string): boolean { // We want to replicate the normal DOM event behavior that, for 'interactive' elements // with a 'disabled' attribute, certain mouse events are suppressed return (element instanceof HTMLButtonElement || element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) - && disableableEventNames.hasOwnProperty(eventName) + && disableableEventNames.hasOwnProperty(rawBrowserEventName) && element.disabled; } diff --git a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts index f641b814d5cc..a99b31dc5045 100644 --- a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts +++ b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts @@ -4,7 +4,9 @@ interface EventTypeOptions { } const eventTypeRegistry: Map = new Map(); -const emptyEventArgsOptions: EventTypeOptions = {}; +const browserEventNamesToAliases: Map = new Map(); +const unknownEventTypeOptions: EventTypeOptions = { }; +const createBlankEventArgsOptions: EventTypeOptions = { createEventArgs: () => ({}) }; export function registerCustomEventType(eventName: string, options: EventTypeOptions): void { if (!options) { @@ -17,11 +19,27 @@ export function registerCustomEventType(eventName: string, options: EventTypeOpt throw new Error(`The event '${eventName}' is already registered.`); } + // If applicable, register this as an alias of the given browserEventName + if (options.browserEventName) { + const aliasGroup = browserEventNamesToAliases.get(options.browserEventName); + if (aliasGroup) { + aliasGroup.push(eventName); + } else { + browserEventNamesToAliases.set(options.browserEventName, [eventName]); + } + + // Make sure there's a global delegating handler for that browserEventName + } + eventTypeRegistry.set(eventName, options); } export function getEventTypeOptions(eventName: string): EventTypeOptions { - return eventTypeRegistry.get(eventName) || emptyEventArgsOptions; + return eventTypeRegistry.get(eventName) || unknownEventTypeOptions; +} + +export function getEventNameAliases(eventName: string): string[] | undefined { + return browserEventNamesToAliases.get(eventName); } function registerBuiltInEventType(eventNames: string[], options: EventTypeOptions) { @@ -32,13 +50,13 @@ registerBuiltInEventType(['input', 'change'], { createEventArgs: parseChangeEvent }); -registerBuiltInEventType(['copy', 'cut', 'paste'], emptyEventArgsOptions); +registerBuiltInEventType(['copy', 'cut', 'paste'], createBlankEventArgsOptions); registerBuiltInEventType(['drag', 'dragend', 'dragenter', 'dragleave', 'dragover', 'dragstart', 'drop'], { createEventArgs: e => parseDragEvent(e as DragEvent) }); -registerBuiltInEventType(['focus', 'blur', 'focusin', 'focusout'], emptyEventArgsOptions); +registerBuiltInEventType(['focus', 'blur', 'focusin', 'focusout'], createBlankEventArgsOptions); registerBuiltInEventType(['keydown', 'keyup', 'keypress'], { createEventArgs: e => parseKeyboardEvent(e as KeyboardEvent) @@ -68,7 +86,7 @@ registerBuiltInEventType(['wheel', 'mousewheel'], { createEventArgs: e => parseWheelEvent(e as WheelEvent) }); -registerBuiltInEventType(['toggle'], emptyEventArgsOptions); +registerBuiltInEventType(['toggle'], createBlankEventArgsOptions); function parseChangeEvent(event: Event): ChangeEventArgs { const element = event.target as Element; From 6ca5e3a888832a44555c896bf896721ededfed50 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 10 Feb 2021 11:39:36 +0000 Subject: [PATCH 16/25] Back-compat for unregistered event types --- .../Web.JS/src/Rendering/Events/EventDelegator.ts | 8 ++++++-- src/Components/Web.JS/src/Rendering/Events/EventTypes.ts | 5 ++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts index 1683f1f74547..ed546ae8ca56 100644 --- a/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts +++ b/src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts @@ -135,8 +135,12 @@ export class EventDelegator { if (handlerInfo && !eventIsDisabledOnElement(candidateElement, browserEvent.type)) { // We are going to raise an event for this element, so prepare info needed by the .NET code if (!eventArgsIsPopulated) { - const eventOptions = getEventTypeOptions(eventName); - eventArgs = eventOptions.createEventArgs ? eventOptions.createEventArgs(browserEvent) : null; + const eventOptionsIfRegistered = getEventTypeOptions(eventName); + // For back-compat, if there's no registered createEventArgs, we supply empty event args (not null). + // But if there is a registered createEventArgs, it can supply anything (including null). + eventArgs = eventOptionsIfRegistered?.createEventArgs + ? eventOptionsIfRegistered.createEventArgs(browserEvent) + : {}; eventArgsIsPopulated = true; } diff --git a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts index a99b31dc5045..71abd941363c 100644 --- a/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts +++ b/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts @@ -5,7 +5,6 @@ interface EventTypeOptions { const eventTypeRegistry: Map = new Map(); const browserEventNamesToAliases: Map = new Map(); -const unknownEventTypeOptions: EventTypeOptions = { }; const createBlankEventArgsOptions: EventTypeOptions = { createEventArgs: () => ({}) }; export function registerCustomEventType(eventName: string, options: EventTypeOptions): void { @@ -34,8 +33,8 @@ export function registerCustomEventType(eventName: string, options: EventTypeOpt eventTypeRegistry.set(eventName, options); } -export function getEventTypeOptions(eventName: string): EventTypeOptions { - return eventTypeRegistry.get(eventName) || unknownEventTypeOptions; +export function getEventTypeOptions(eventName: string): EventTypeOptions | undefined { + return eventTypeRegistry.get(eventName); } export function getEventNameAliases(eventName: string): string[] | undefined { From c1523ff16e71358fa7cfae90dd1d9884dd38ff83 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 10 Feb 2021 12:05:38 +0000 Subject: [PATCH 17/25] Update some older E2E tests --- .../BasicTestApp/EventBubblingComponent.razor | 13 ++++++------- .../BasicTestApp/EventPreventDefaultComponent.razor | 4 ---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Components/test/testassets/BasicTestApp/EventBubblingComponent.razor b/src/Components/test/testassets/BasicTestApp/EventBubblingComponent.razor index 66c28ac823fa..87343158bad1 100644 --- a/src/Components/test/testassets/BasicTestApp/EventBubblingComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/EventBubblingComponent.razor @@ -1,10 +1,9 @@ 

Bubbling standard event

-@* Temporarily hard-coding the internal names - this will be replaced once the Razor compiler supports @onevent:stopPropagation and @onevent:preventDefault *@
@* This element shows you can stop propagation even without necessarily also handling the event *@ -
- + + + + + + + +

Event log

+ + + + +@code { + string logValue = string.Empty; + + void LogMessage(string message) + { + logValue += message + Environment.NewLine; + } + + void HandleTestEvent(TestEventArgs eventArgs) + { + var args = eventArgs == null ? "null" : $"{{ MyProp={eventArgs.MyProp ?? "null"} }}"; + LogMessage($"Received testevent with args '{args}'"); + } +} diff --git a/src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs b/src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs new file mode 100644 index 000000000000..f7487b1916a1 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.AspNetCore.Components; + +namespace BasicTestApp.CustomEventTypesNamespace +{ + [EventHandler("ontestevent", typeof(TestEventArgs), true, true)] + public static class EventHandlers + { + } + + class TestEventArgs : EventArgs + { + public string MyProp { get; set; } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index e58c7ffc757d..909d4780716c 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -27,7 +27,7 @@ - + From 34c4338f7bbfd27cd050912afde163d2cab1ff1a Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 10 Feb 2021 13:08:17 +0000 Subject: [PATCH 19/25] More E2E scenarios --- .../EventCustomArgsComponent.razor | 40 +++++++++++++++++-- .../BasicTestApp/EventCustomArgsTypes.cs | 6 +++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor b/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor index f6410fdd2cf0..19d2b4578790 100644 --- a/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor @@ -4,10 +4,18 @@

This component exercises various scenarios around custom event types and arguments.

-
+
Event target -
Child
+
+ Child +

+ +

+
+ +

+ +

+ +

+ +

+

Event log

@@ -37,6 +64,8 @@ @code { string logValue = string.Empty; + bool customPastePreventDefault; + bool customPasteStopPropagation; void LogMessage(string message) { @@ -48,4 +77,9 @@ var args = eventArgs == null ? "null" : $"{{ MyProp={eventArgs.MyProp ?? "null"} }}"; LogMessage($"Received testevent with args '{args}'"); } + + void HandleCustomPaste(TestPasteEventArgs eventArgs) + { + LogMessage($"You pasted: {eventArgs.PastedText}"); + } } diff --git a/src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs b/src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs index f7487b1916a1..aee76023f127 100644 --- a/src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs +++ b/src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs @@ -4,6 +4,7 @@ namespace BasicTestApp.CustomEventTypesNamespace { [EventHandler("ontestevent", typeof(TestEventArgs), true, true)] + [EventHandler("onpaste.testvariant", typeof(TestPasteEventArgs), true, true)] public static class EventHandlers { } @@ -12,4 +13,9 @@ class TestEventArgs : EventArgs { public string MyProp { get; set; } } + + class TestPasteEventArgs : EventArgs + { + public string PastedText { get; set; } + } } From c09a2f191384b380feb3a97174395419bda339cc Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 10 Feb 2021 13:17:53 +0000 Subject: [PATCH 20/25] Change E2E scenario to use keydown, not paste, to avoid isolation complications during automated runs --- .../EventCustomArgsComponent.razor | 30 +++++++++---------- .../BasicTestApp/EventCustomArgsTypes.cs | 6 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor b/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor index 19d2b4578790..82870ded6759 100644 --- a/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor @@ -5,15 +5,15 @@

This component exercises various scenarios around custom event types and arguments.

+ @onkeydown.testvariant="@HandleCustomKeyDown"> Event target
Child -

- +

+

@@ -39,21 +39,21 @@

@@ -64,8 +64,8 @@ @code { string logValue = string.Empty; - bool customPastePreventDefault; - bool customPasteStopPropagation; + bool customKeyDownPreventDefault; + bool customKeyDownStopPropagation; void LogMessage(string message) { @@ -78,8 +78,8 @@ LogMessage($"Received testevent with args '{args}'"); } - void HandleCustomPaste(TestPasteEventArgs eventArgs) + void HandleCustomKeyDown(TestKeyDownEventArgs eventArgs) { - LogMessage($"You pasted: {eventArgs.PastedText}"); + LogMessage($"You pressed: {eventArgs.CustomKeyInfo}"); } } diff --git a/src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs b/src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs index aee76023f127..2bbebfcca10a 100644 --- a/src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs +++ b/src/Components/test/testassets/BasicTestApp/EventCustomArgsTypes.cs @@ -4,7 +4,7 @@ namespace BasicTestApp.CustomEventTypesNamespace { [EventHandler("ontestevent", typeof(TestEventArgs), true, true)] - [EventHandler("onpaste.testvariant", typeof(TestPasteEventArgs), true, true)] + [EventHandler("onkeydown.testvariant", typeof(TestKeyDownEventArgs), true, true)] public static class EventHandlers { } @@ -14,8 +14,8 @@ class TestEventArgs : EventArgs public string MyProp { get; set; } } - class TestPasteEventArgs : EventArgs + class TestKeyDownEventArgs : EventArgs { - public string PastedText { get; set; } + public string CustomKeyInfo { get; set; } } } From 1ebdf84072f03a7fb8a40c90eaaf2e072191c74c Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 10 Feb 2021 13:38:08 +0000 Subject: [PATCH 21/25] Prepare E2E case for when an aliased event has no native global listener --- .../BasicTestApp/EventCustomArgsComponent.razor | 10 ++++++++-- .../testassets/BasicTestApp/EventCustomArgsTypes.cs | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor b/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor index 82870ded6759..6029d5bb9333 100644 --- a/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor @@ -7,7 +7,8 @@
+ @onkeydown.testvariant="@HandleCustomKeyDown" + @oncustommouseover="@(e => { LogMessage("Received custom mouseover event"); })"> Event target
Child @@ -38,11 +39,16 @@ Register testevent with createventargs that supplies args - + +

Event log

- + @code { From 8c411117455b9c751b58490f69cf37ea100f55c5 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 10 Feb 2021 15:45:57 +0000 Subject: [PATCH 24/25] Another test case showing multiple aliases work --- .../test/E2ETest/Tests/EventCustomArgsTest.cs | 29 +++++++++++++++++-- .../EventCustomArgsComponent.razor | 11 +++++++ .../BasicTestApp/EventCustomArgsTypes.cs | 6 ++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/Components/test/E2ETest/Tests/EventCustomArgsTest.cs b/src/Components/test/E2ETest/Tests/EventCustomArgsTest.cs index ff954374a3f9..329af1353344 100644 --- a/src/Components/test/E2ETest/Tests/EventCustomArgsTest.cs +++ b/src/Components/test/E2ETest/Tests/EventCustomArgsTest.cs @@ -108,15 +108,40 @@ public void CanAliasBrowserEvent_StopPropagationIndependentOfNativeEvent() { var input = Browser.Exists(By.CssSelector("#test-event-target-child input")); Browser.FindElement(By.Id("register-custom-keydown")).Click(); + Browser.FindElement(By.Id("register-yet-another-keydown")).Click(); Browser.FindElement(By.Id("custom-keydown-stop-propagation")).Click(); SendKeysSequentially(input, "ab"); Browser.Equal(new[] { - // The native event still bubbles up to its listener on an ancestor, but the - // custom variant does not bubble up past the stopPropagation point + // The native event still bubbles up to its listener on an ancestor, and + // other aliased events still receive it, but the stopPropagation-ed + // variant does not "Received native keydown event", + "Yet another aliased event received: a", "Received native keydown event", + "Yet another aliased event received: b", + }, GetLogLines); + + Assert.Equal("ab", input.GetAttribute("value")); + } + + [Fact] + public void CanHaveMultipleAliasesForASingleBrowserEvent() + { + var input = Browser.Exists(By.CssSelector("#test-event-target-child input")); + Browser.FindElement(By.Id("register-custom-keydown")).Click(); + Browser.FindElement(By.Id("register-yet-another-keydown")).Click(); + SendKeysSequentially(input, "ab"); + + Browser.Equal(new[] + { + "Received native keydown event", + "You pressed: a", + "Yet another aliased event received: a", + "Received native keydown event", + "You pressed: b", + "Yet another aliased event received: b", }, GetLogLines); Assert.Equal("ab", input.GetAttribute("value")); diff --git a/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor b/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor index c558621d2e18..d79814c7205d 100644 --- a/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/EventCustomArgsComponent.razor @@ -8,6 +8,7 @@ @onkeydown="@(e => { LogMessage("Received native keydown event"); })" @ontestevent="@HandleTestEvent" @onkeydown.testvariant="@HandleCustomKeyDown" + @onkeydown.yetanother="@HandleYetAnotherKeyboardEvent" @oncustommouseover="@(e => { LogMessage("Received custom mouseover event"); })"> Event target
@@ -44,6 +45,11 @@ Register custom keydown event + +

Alternatively, reload

',this.message=this.modal.querySelector("h5"),this.button=this.modal.querySelector("button"),this.reloadParagraph=this.modal.querySelector("p"),this.loader=this.getLoader(),this.message.after(this.loader),this.button.addEventListener("click",async()=>{this.show();try{await window.Blazor.reconnect()||this.rejected()}catch(e){this.logger.log(Te.Error,e),this.failed()}}),this.reloadParagraph.querySelector("a").addEventListener("click",()=>location.reload())}show(){this.addedToDom||(this.addedToDom=!0,this.document.body.appendChild(this.modal)),this.modal.style.display="block",this.loader.style.display="inline-block",this.button.style.display="none",this.reloadParagraph.style.display="none",this.message.textContent="Attempting to reconnect to the server...",this.modal.style.visibility="hidden",setTimeout(()=>{this.modal.style.visibility="visible"},0)}update(e){this.message.textContent=`Attempting to reconnect to the server: ${e} of ${this.maxRetries}`}hide(){this.modal.style.display="none"}failed(){this.button.style.display="block",this.reloadParagraph.style.display="none",this.loader.style.display="none",this.message.innerHTML="Reconnection failed. Try reloading the page if you're unable to reconnect.",this.message.querySelector("a").addEventListener("click",()=>location.reload())}rejected(){this.button.style.display="none",this.reloadParagraph.style.display="none",this.loader.style.display="none",this.message.innerHTML="Could not reconnect to the server. Reload the page to restore functionality.",this.message.querySelector("a").addEventListener("click",()=>location.reload())}getLoader(){const e=this.document.createElement("div");return e.style.cssText=["border: 0.3em solid #f3f3f3","border-top: 0.3em solid #3498db","border-radius: 50%","width: 2em","height: 2em","display: inline-block"].join(";"),e.animate([{transform:"rotate(0deg)"},{transform:"rotate(360deg)"}],{duration:2e3,iterations:1/0}),e}}class Ue{constructor(e,t,n){this.dialog=e,this.maxRetries=t,this.document=n,this.document=n;const r=this.document.getElementById(Ue.MaxRetriesId);r&&(r.innerText=this.maxRetries.toString())}show(){this.removeClasses(),this.dialog.classList.add(Ue.ShowClassName)}update(e){const t=this.document.getElementById(Ue.CurrentAttemptId);t&&(t.innerText=e.toString())}hide(){this.removeClasses(),this.dialog.classList.add(Ue.HideClassName)}failed(){this.removeClasses(),this.dialog.classList.add(Ue.FailedClassName)}rejected(){this.removeClasses(),this.dialog.classList.add(Ue.RejectedClassName)}removeClasses(){this.dialog.classList.remove(Ue.ShowClassName,Ue.HideClassName,Ue.FailedClassName,Ue.RejectedClassName)}}Ue.ShowClassName="components-reconnect-show",Ue.HideClassName="components-reconnect-hide",Ue.FailedClassName="components-reconnect-failed",Ue.RejectedClassName="components-reconnect-rejected",Ue.MaxRetriesId="components-reconnect-max-retries",Ue.CurrentAttemptId="components-reconnect-current-attempt";class Le{constructor(e,t,n){this._currentReconnectionProcess=null,this._logger=e,this._reconnectionDisplay=t,this._reconnectCallback=n||(()=>window.Blazor.reconnect())}onConnectionDown(e,t){if(!this._reconnectionDisplay){const t=document.getElementById(e.dialogId);this._reconnectionDisplay=t?new Ue(t,e.maxRetries,document):new Be(e.dialogId,e.maxRetries,document,this._logger)}this._currentReconnectionProcess||(this._currentReconnectionProcess=new Ne(e,this._logger,this._reconnectCallback,this._reconnectionDisplay))}onConnectionUp(){this._currentReconnectionProcess&&(this._currentReconnectionProcess.dispose(),this._currentReconnectionProcess=null)}}class Ne{constructor(e,t,n,r){this.logger=t,this.reconnectCallback=n,this.isDisposed=!1,this.reconnectDisplay=r,this.reconnectDisplay.show(),this.attemptPeriodicReconnection(e)}dispose(){this.isDisposed=!0,this.reconnectDisplay.hide()}async attemptPeriodicReconnection(e){for(let t=0;tNe.MaximumFirstRetryInterval?Ne.MaximumFirstRetryInterval:e.retryIntervalMilliseconds;if(await this.delay(n),this.isDisposed)break;try{return await this.reconnectCallback()?void 0:void this.reconnectDisplay.rejected()}catch(e){this.logger.log(Te.Error,e)}}this.reconnectDisplay.failed()}delay(e){return new Promise(t=>setTimeout(t,e))}}Ne.MaximumFirstRetryInterval=3e3;var Fe=n(21),He=n(18);let qe=!1,We=!1;async function ze(e){if(We)throw new Error("Blazor has already started.");We=!0;const t=function(e){const t={...Ae,...e};return e&&e.reconnectionOptions&&(t.reconnectionOptions={...Ae.reconnectionOptions,...e.reconnectionOptions}),t}(e),n=new Oe(t.logLevel);window.Blazor.defaultReconnectionHandler=new Le(n),window.Blazor._internal.InputFile=He.a,t.reconnectionHandler=t.reconnectionHandler||window.Blazor.defaultReconnectionHandler,n.log(Te.Information,"Starting up blazor server-side application.");const r=Object(Fe.a)(document,"server"),o=new De(r),i=await Je(t,n,o);if(!await o.startCircuit(i))return void n.log(Te.Error,"Failed to start the circuit.");let s=!1;const a=()=>{if(!s){const e=new FormData,t=o.circuitId;e.append("circuitId",t),s=navigator.sendBeacon("_blazor/disconnect",e)}};window.Blazor.disconnect=a,window.addEventListener("unload",a,{capture:!1,once:!0}),window.Blazor.reconnect=async e=>{if(qe)return!1;const r=e||await Je(t,n,o);return await o.reconnect(r)?(t.reconnectionHandler.onConnectionUp(),!0):(n.log(Te.Information,"Reconnection attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server."),!1)},n.log(Te.Information,"Blazor server-side application started.")}async function Je(e,t,n){const r=new ue;r.name="blazorpack";const i=(new oe).withUrl("_blazor").withHubProtocol(r);e.configureSignalR(i);const s=i.build();Object(Me.b)((e,t)=>{s.send("DispatchBrowserEvent",JSON.stringify(e),JSON.stringify(t))}),window.Blazor._internal.navigationManager.listenForNavigationEvents((e,t)=>s.send("OnLocationChanged",e,t)),s.on("JS.AttachComponent",(e,t)=>Object(fe.b)(0,n.resolveElement(t),e)),s.on("JS.BeginInvokeJS",o.a.jsCallDispatcher.beginInvokeJSFromDotNet),s.on("JS.EndInvokeDotNet",e=>o.a.jsCallDispatcher.endInvokeDotNetFromJS(...o.a.parseJsonWithRevivers(e)));const a=xe.getOrCreate(t);s.on("JS.RenderBatch",(e,n)=>{t.log(Te.Debug,`Received render batch with id ${e} and ${n.byteLength} bytes.`),a.processBatch(e,n,s)}),s.onclose(t=>!qe&&e.reconnectionHandler.onConnectionDown(e.reconnectionOptions,t)),s.on("JS.Error",e=>{qe=!0,Ye(s,e,t),Object(le.a)()}),window.Blazor._internal.forceCloseConnection=()=>s.stop();try{await s.start()}catch(e){Ye(s,e,t)}return o.a.attachDispatcher({beginInvokeDotNetFromJS:(e,t,n,r,o)=>{s.send("BeginInvokeDotNetFromJS",e?e.toString():null,t,n,r||0,o)},endInvokeJSFromDotNet:(e,t,n)=>{s.send("EndInvokeJSFromDotNet",e,t,n)}}),s}function Ye(e,t,n){n.log(Te.Error,t),e&&e.stop()}window.Blazor.start=ze,Object(he.a)()&&ze()}]); \ No newline at end of file +var r=n(16),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function s(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=s),s.prototype=Object.create(o.prototype),i(o,s),s.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},s.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},s.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},s.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){"use strict";e.exports=i;var r=n(34),o=Object.create(n(17));function i(e){if(!(this instanceof i))return new i(e);r.call(this,e)}o.inherits=n(13),o.inherits(i,r),i.prototype._transform=function(e,t,n){n(null,e)}},function(e,t,n){ +/*! safe-buffer. MIT License. Feross Aboukhadijeh */ +var r=n(16),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function s(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=s),s.prototype=Object.create(o.prototype),i(o,s),s.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},s.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},s.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},s.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){"use strict";var r=n(27).Transform,o=n(13),i=n(21);function s(e){(e=e||{}).objectMode=!0,e.highWaterMark=16,r.call(this,e),this._msgpack=e.msgpack}function a(e){if(!(this instanceof a))return(e=e||{}).msgpack=this,new a(e);s.call(this,e),this._wrap="wrap"in e&&e.wrap}function c(e){if(!(this instanceof c))return(e=e||{}).msgpack=this,new c(e);s.call(this,e),this._chunks=i(),this._wrap="wrap"in e&&e.wrap}o(s,r),o(a,s),a.prototype._transform=function(e,t,n){var r=null;try{r=this._msgpack.encode(this._wrap?e.value:e).slice(0)}catch(e){return this.emit("error",e),n()}this.push(r),n()},o(c,s),c.prototype._transform=function(e,t,n){e&&this._chunks.append(e);try{var r=this._msgpack.decode(this._chunks);this._wrap&&(r={value:r}),this.push(r)}catch(e){return void(e instanceof this._msgpack.IncompleteBufferError?n():this.emit("error",e))}this._chunks.length>0?this._transform(null,t,n):n()},e.exports.decoder=c,e.exports.encoder=a},function(e,t,n){"use strict";var r=n(21);function o(e){Error.call(this),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),this.name=this.constructor.name,this.message=e||"unable to decode"}n(23).inherits(o,Error),e.exports=function(e){return function(e){e instanceof r||(e=r().append(e));var t=i(e);if(t)return e.consume(t.bytesConsumed),t.value;throw new o};function t(e,t,n){return t>=n+e}function n(e,t){return{value:e,bytesConsumed:t}}function i(e,r){r=void 0===r?0:r;var o=e.length-r;if(o<=0)return null;var i,l,f,h=e.readUInt8(r),d=0;if(!function(e,t){var n=function(e){switch(e){case 196:return 2;case 197:return 3;case 198:return 5;case 199:return 3;case 200:return 4;case 201:return 6;case 202:return 5;case 203:return 9;case 204:return 2;case 205:return 3;case 206:return 5;case 207:return 9;case 208:return 2;case 209:return 3;case 210:return 5;case 211:return 9;case 212:return 3;case 213:return 4;case 214:return 6;case 215:return 10;case 216:return 18;case 217:return 2;case 218:return 3;case 219:return 5;case 222:return 3;default:return-1}}(e);return!(-1!==n&&t=0;f--)d+=e.readUInt8(r+f+1)*Math.pow(2,8*(7-f));return n(d,9);case 208:return n(d=e.readInt8(r+1),2);case 209:return n(d=e.readInt16BE(r+1),3);case 210:return n(d=e.readInt32BE(r+1),5);case 211:return n(d=function(e,t){var n=128==(128&e[t]);if(n)for(var r=1,o=t+7;o>=t;o--){var i=(255^e[o])+r;e[o]=255&i,r=i>>8}var s=e.readUInt32BE(t+0),a=e.readUInt32BE(t+4);return(4294967296*s+a)*(n?-1:1)}(e.slice(r+1,r+9),0),9);case 202:return n(d=e.readFloatBE(r+1),5);case 203:return n(d=e.readDoubleBE(r+1),9);case 217:return t(i=e.readUInt8(r+1),o,2)?n(d=e.toString("utf8",r+2,r+2+i),2+i):null;case 218:return t(i=e.readUInt16BE(r+1),o,3)?n(d=e.toString("utf8",r+3,r+3+i),3+i):null;case 219:return t(i=e.readUInt32BE(r+1),o,5)?n(d=e.toString("utf8",r+5,r+5+i),5+i):null;case 196:return t(i=e.readUInt8(r+1),o,2)?n(d=e.slice(r+2,r+2+i),2+i):null;case 197:return t(i=e.readUInt16BE(r+1),o,3)?n(d=e.slice(r+3,r+3+i),3+i):null;case 198:return t(i=e.readUInt32BE(r+1),o,5)?n(d=e.slice(r+5,r+5+i),5+i):null;case 220:return o<3?null:(i=e.readUInt16BE(r+1),s(e,r,i,3));case 221:return o<5?null:(i=e.readUInt32BE(r+1),s(e,r,i,5));case 222:return i=e.readUInt16BE(r+1),a(e,r,i,3);case 223:return i=e.readUInt32BE(r+1),a(e,r,i,5);case 212:return c(e,r,1);case 213:return c(e,r,2);case 214:return c(e,r,4);case 215:return c(e,r,8);case 216:return c(e,r,16);case 199:return i=e.readUInt8(r+1),l=e.readUInt8(r+2),t(i,o,3)?u(e,r,l,i,3):null;case 200:return i=e.readUInt16BE(r+1),l=e.readUInt8(r+3),t(i,o,4)?u(e,r,l,i,4):null;case 201:return i=e.readUInt32BE(r+1),l=e.readUInt8(r+5),t(i,o,6)?u(e,r,l,i,6):null}if(144==(240&h))return s(e,r,i=15&h,1);if(128==(240&h))return a(e,r,i=15&h,1);if(160==(224&h))return t(i=31&h,o,1)?n(d=e.toString("utf8",r+1,r+i+1),i+1):null;if(h>=224)return n(d=h-256,1);if(h<128)return n(h,1);throw new Error("not implemented yet")}function s(e,t,r,o){var s,a=[],c=0;for(t+=o,s=0;s0&&l.write(c,1)):f<=255&&!n?((l=r.allocUnsafe(2+f))[0]=217,l[1]=f,l.write(c,2)):f<=65535?((l=r.allocUnsafe(3+f))[0]=218,l.writeUInt16BE(f,1),l.write(c,3)):((l=r.allocUnsafe(5+f))[0]=219,l.writeUInt32BE(f,1),l.write(c,5));else if(c&&(c.readUInt32LE||c instanceof Uint8Array))c instanceof Uint8Array&&(c=r.from(c)),c.length<=255?((l=r.allocUnsafe(2))[0]=196,l[1]=c.length):c.length<=65535?((l=r.allocUnsafe(3))[0]=197,l.writeUInt16BE(c.length,1)):((l=r.allocUnsafe(5))[0]=198,l.writeUInt32BE(c.length,1)),l=o([l,c]);else if(Array.isArray(c))c.length<16?(l=r.allocUnsafe(1))[0]=144|c.length:c.length<65536?((l=r.allocUnsafe(3))[0]=220,l.writeUInt16BE(c.length,1)):((l=r.allocUnsafe(5))[0]=221,l.writeUInt32BE(c.length,1)),l=c.reduce((function(e,t){return e.append(a(t,!0)),e}),o().append(l));else{if(!s&&"function"==typeof c.getDate)return function(e){var t,n=1*e,i=Math.floor(n/1e3),s=1e6*(n-1e3*i);if(s||i>4294967295){(t=r.allocUnsafe(10))[0]=215,t[1]=-1;var a=4*s,c=i/Math.pow(2,32),u=a+c&4294967295,l=4294967295&i;t.writeInt32BE(u,2),t.writeInt32BE(l,6)}else(t=r.allocUnsafe(6))[0]=214,t[1]=-1,t.writeUInt32BE(Math.floor(n/1e3),2);return o().append(t)}(c);if("object"==typeof c)l=function(t){var n,i,s,a=[];for(n=0;n>8),a.push(255&s)):(a.push(201),a.push(s>>24),a.push(s>>16&255),a.push(s>>8&255),a.push(255&s));return o().append(r.from(a)).append(i)}(c)||function(e){var t,n,i=[],s=0;for(t in e)e.hasOwnProperty(t)&&void 0!==e[t]&&"function"!=typeof e[t]&&(++s,i.push(a(t,!0)),i.push(a(e[t],!0)));s<16?(n=r.allocUnsafe(1))[0]=128|s:s<65535?((n=r.allocUnsafe(3))[0]=222,n.writeUInt16BE(s,1)):((n=r.allocUnsafe(5))[0]=223,n.writeUInt32BE(s,1));return i.unshift(n),i.reduce((function(e,t){return e.append(t)}),o())}(c);else if("number"==typeof c){if(function(e){return e%1!=0}(c))return i(c,t);if(c>=0)if(c<128)(l=r.allocUnsafe(1))[0]=c;else if(c<256)(l=r.allocUnsafe(2))[0]=204,l[1]=c;else if(c<65536)(l=r.allocUnsafe(3))[0]=205,l.writeUInt16BE(c,1);else if(c<=4294967295)(l=r.allocUnsafe(5))[0]=206,l.writeUInt32BE(c,1);else{if(!(c<=9007199254740991))return i(c,!0);(l=r.allocUnsafe(9))[0]=207,function(e,t){for(var n=7;n>=0;n--)e[n+1]=255&t,t/=256}(l,c)}else if(c>=-32)(l=r.allocUnsafe(1))[0]=256+c;else if(c>=-128)(l=r.allocUnsafe(2))[0]=208,l.writeInt8(c,1);else if(c>=-32768)(l=r.allocUnsafe(3))[0]=209,l.writeInt16BE(c,1);else if(c>-214748365)(l=r.allocUnsafe(5))[0]=210,l.writeInt32BE(c,1);else{if(!(c>=-9007199254740991))return i(c,!0);(l=r.allocUnsafe(9))[0]=211,function(e,t,n){var r=n<0;r&&(n=Math.abs(n));var o=n%4294967296,i=n/4294967296;if(e.writeUInt32BE(Math.floor(i),t+0),e.writeUInt32BE(o,t+4),r)for(var s=1,a=t+7;a>=t;a--){var c=(255^e[a])+s;e[a]=255&c,s=c>>8}}(l,1,c)}}}if(!l)throw new Error("not implemented yet");return u?l:l.slice()}return a}},function(e,t,n){"use strict";n.r(t);var r,o=n(4),i=(n(25),r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),s=function(e){function t(t,n){var r=this,o=this.constructor.prototype;return(r=e.call(this,t)||this).statusCode=n,r.__proto__=o,r}return i(t,e),t}(Error),a=function(e){function t(t){void 0===t&&(t="A timeout occurred.");var n=this,r=this.constructor.prototype;return(n=e.call(this,t)||this).__proto__=r,n}return i(t,e),t}(Error),c=function(e){function t(t){void 0===t&&(t="An abort occurred.");var n=this,r=this.constructor.prototype;return(n=e.call(this,t)||this).__proto__=r,n}return i(t,e),t}(Error),u=function(){return(u=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]=200&&o.status<300?n(new l(o.status,o.statusText,o.response||o.responseText)):r(new s(o.statusText,o.status))},o.onerror=function(){t.logger.log(h.a.Warning,"Error from HTTP request. "+o.status+": "+o.statusText+"."),r(new s(o.statusText,o.status))},o.ontimeout=function(){t.logger.log(h.a.Warning,"Timeout from HTTP request."),r(new a)},o.send(e.content||"")})):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t}(f),S=function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(t,n)};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),I=function(e){function t(t){var n=e.call(this)||this;if("undefined"!=typeof fetch||d.c.isNode)n.httpClient=new b(t);else{if("undefined"==typeof XMLHttpRequest)throw new Error("No usable HttpClient found.");n.httpClient=new E(t)}return n}return S(t,e),t.prototype.send=function(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new c):e.method?e.url?this.httpClient.send(e):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t.prototype.getCookieString=function(e){return this.httpClient.getCookieString(e)},t}(f),C=function(){function e(){}return e.write=function(t){return""+t+e.RecordSeparator},e.parse=function(t){if(t[t.length-1]!==e.RecordSeparator)throw new Error("Message is incomplete.");var n=t.split(e.RecordSeparator);return n.pop(),n},e.RecordSeparatorCode=30,e.RecordSeparator=String.fromCharCode(e.RecordSeparatorCode),e}(),k=function(){function e(){}return e.prototype.writeHandshakeRequest=function(e){return C.write(JSON.stringify(e))},e.prototype.parseHandshakeResponse=function(e){var t,n;if(Object(d.h)(e)){var r=new Uint8Array(e);if(-1===(i=r.indexOf(C.RecordSeparatorCode)))throw new Error("Message is incomplete.");var o=i+1;t=String.fromCharCode.apply(null,Array.prototype.slice.call(r.slice(0,o))),n=r.byteLength>o?r.slice(o).buffer:null}else{var i,s=e;if(-1===(i=s.indexOf(C.RecordSeparator)))throw new Error("Message is incomplete.");o=i+1;t=s.substring(0,o),n=s.length>o?s.substring(o):null}var a=C.parse(t),c=JSON.parse(a[0]);if(c.type)throw new Error("Expected a handshake response from the server.");return[n,c]},e}();!function(e){e[e.Invocation=1]="Invocation",e[e.StreamItem=2]="StreamItem",e[e.Completion=3]="Completion",e[e.StreamInvocation=4]="StreamInvocation",e[e.CancelInvocation=5]="CancelInvocation",e[e.Ping=6]="Ping",e[e.Close=7]="Close"}(v||(v={}));var _,T=function(){function e(){this.observers=[]}return e.prototype.next=function(e){for(var t=0,n=this.observers;t0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0?[2,Promise.reject(new Error("Unable to connect to the server with any of the available transports. "+i.join(" ")))]:[2,Promise.reject(new Error("None of the transports supported by the client are supported by the server."))]}}))}))},e.prototype.constructTransport=function(e){switch(e){case R.WebSockets:if(!this.options.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new V(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.WebSocket,this.options.headers||{});case R.ServerSentEvents:if(!this.options.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new z(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.EventSource,this.options.withCredentials,this.options.headers||{});case R.LongPolling:return new F(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.withCredentials,this.options.headers||{});default:throw new Error("Unknown transport: "+e+".")}},e.prototype.startTransport=function(e,t){var n=this;return this.transport.onreceive=this.onreceive,this.transport.onclose=function(e){return n.stopConnection(e)},this.transport.connect(e,t)},e.prototype.resolveTransportOrError=function(e,t,n){var r=R[e.transport];if(null==r)return this.logger.log(h.a.Debug,"Skipping transport '"+e.transport+"' because it is not supported by this client."),new Error("Skipping transport '"+e.transport+"' because it is not supported by this client.");if(!function(e,t){return!e||0!=(t&e)}(t,r))return this.logger.log(h.a.Debug,"Skipping transport '"+R[r]+"' because it was disabled by the client."),new Error("'"+R[r]+"' is disabled by the client.");if(!(e.transferFormats.map((function(e){return P[e]})).indexOf(n)>=0))return this.logger.log(h.a.Debug,"Skipping transport '"+R[r]+"' because it does not support the requested transfer format '"+P[n]+"'."),new Error("'"+R[r]+"' does not support "+P[n]+".");if(r===R.WebSockets&&!this.options.WebSocket||r===R.ServerSentEvents&&!this.options.EventSource)return this.logger.log(h.a.Debug,"Skipping transport '"+R[r]+"' because it is not supported in your environment.'"),new Error("'"+R[r]+"' is not supported in your environment.");this.logger.log(h.a.Debug,"Selecting transport '"+R[r]+"'.");try{return this.constructTransport(r)}catch(e){return e}},e.prototype.isITransport=function(e){return e&&"object"==typeof e&&"connect"in e},e.prototype.stopConnection=function(e){var t=this;if(this.logger.log(h.a.Debug,"HttpConnection.stopConnection("+e+") called while in state "+this.connectionState+"."),this.transport=void 0,e=this.stopError||e,this.stopError=void 0,"Disconnected"!==this.connectionState){if("Connecting"===this.connectionState)throw this.logger.log(h.a.Warning,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection is still in the connecting state."),new Error("HttpConnection.stopConnection("+e+") was called while the connection is still in the connecting state.");if("Disconnecting"===this.connectionState&&this.stopPromiseResolver(),e?this.logger.log(h.a.Error,"Connection disconnected with error '"+e+"'."):this.logger.log(h.a.Information,"Connection disconnected."),this.sendQueue&&(this.sendQueue.stop().catch((function(e){t.logger.log(h.a.Error,"TransportSendQueue.stop() threw error '"+e+"'.")})),this.sendQueue=void 0),this.connectionId=void 0,this.connectionState="Disconnected",this.connectionStarted){this.connectionStarted=!1;try{this.onclose&&this.onclose(e)}catch(t){this.logger.log(h.a.Error,"HttpConnection.onclose("+e+") threw error '"+t+"'.")}}}else this.logger.log(h.a.Debug,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection is already in the disconnected state.")},e.prototype.resolveUrl=function(e){if(0===e.lastIndexOf("https://",0)||0===e.lastIndexOf("http://",0))return e;if(!d.c.isBrowser||!window.document)throw new Error("Cannot resolve '"+e+"'.");var t=window.document.createElement("a");return t.href=e,this.logger.log(h.a.Information,"Normalizing '"+e+"' to '"+t.href+"'."),t.href},e.prototype.resolveNegotiateUrl=function(e){var t=e.indexOf("?"),n=e.substring(0,-1===t?e.length:t);return"/"!==n[n.length-1]&&(n+="/"),n+="negotiate",-1===(n+=-1===t?"":e.substring(t)).indexOf("negotiateVersion")&&(n+=-1===t?"?":"&",n+="negotiateVersion="+this.negotiateVersion),n},e}();var Z=function(){function e(e){this.transport=e,this.buffer=[],this.executing=!0,this.sendBufferedData=new ee,this.transportResult=new ee,this.sendLoopPromise=this.sendLoop()}return e.prototype.send=function(e){return this.bufferData(e),this.transportResult||(this.transportResult=new ee),this.transportResult.promise},e.prototype.stop=function(){return this.executing=!1,this.sendBufferedData.resolve(),this.sendLoopPromise},e.prototype.bufferData=function(e){if(this.buffer.length&&typeof this.buffer[0]!=typeof e)throw new Error("Expected data to be of type "+typeof this.buffer+" but was of type "+typeof e);this.buffer.push(e),this.sendBufferedData.resolve()},e.prototype.sendLoop=function(){return X(this,void 0,void 0,(function(){var t,n,r;return G(this,(function(o){switch(o.label){case 0:return[4,this.sendBufferedData.promise];case 1:if(o.sent(),!this.executing)return this.transportResult&&this.transportResult.reject("Connection stopped."),[3,6];this.sendBufferedData=new ee,t=this.transportResult,this.transportResult=void 0,n="string"==typeof this.buffer[0]?this.buffer.join(""):e.concatBuffers(this.buffer),this.buffer.length=0,o.label=2;case 2:return o.trys.push([2,4,,5]),[4,this.transport.send(n)];case 3:return o.sent(),t.resolve(),[3,5];case 4:return r=o.sent(),t.reject(r),[3,5];case 5:return[3,0];case 6:return[2]}}))}))},e.concatBuffers=function(e){for(var t=e.map((function(e){return e.byteLength})).reduce((function(e,t){return e+t})),n=new Uint8Array(t),r=0,o=0,i=e;o>=7)>0&&(r|=128),n.push(r)}while(t>0);t=e.byteLength||e.length;var o=new Uint8Array(n.length+t);return o.set(n,0),o.set(e,n.length),o.buffer},e.parse=function(e){for(var t=[],n=new Uint8Array(e),r=[0,7,14,21,28],o=0;o7)throw new Error("Messages bigger than 2GB are not supported.");if(!(n.byteLength>=o+i+s))throw new Error("Incomplete message.");t.push(n.slice?n.slice(o+i,o+i+s):n.subarray(o+i,o+i+s)),o=o+i+s}return t},e}();var ce=function(){return(ce=Object.assign||function(e){for(var t,n=1,r=arguments.length;n=3?e[2]:void 0,error:e[1],type:v.Close}},e.prototype.createPingMessage=function(e){if(e.length<1)throw new Error("Invalid payload for Ping message.");return{type:v.Ping}},e.prototype.createInvocationMessage=function(e,t){if(t.length<5)throw new Error("Invalid payload for Invocation message.");var n=t[2];return n?{arguments:t[4],headers:e,invocationId:n,streamIds:[],target:t[3],type:v.Invocation}:{arguments:t[4],headers:e,streamIds:[],target:t[3],type:v.Invocation}},e.prototype.createStreamItemMessage=function(e,t){if(t.length<4)throw new Error("Invalid payload for StreamItem message.");return{headers:e,invocationId:t[2],item:t[3],type:v.StreamItem}},e.prototype.createCompletionMessage=function(e,t){if(t.length<4)throw new Error("Invalid payload for Completion message.");var n,r,o=t[3];if(o!==this.voidResult&&t.length<5)throw new Error("Invalid payload for Completion message.");switch(o){case this.errorResult:n=t[4];break;case this.nonVoidResult:r=t[4]}return{error:n,headers:e,invocationId:t[2],result:r,type:v.Completion}},e.prototype.writeInvocation=function(e){var t,n=se(this.messagePackOptions);return t=e.streamIds?n.encode([v.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments,e.streamIds]):n.encode([v.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments]),ae.write(t.slice())},e.prototype.writeStreamInvocation=function(e){var t,n=se(this.messagePackOptions);return t=e.streamIds?n.encode([v.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments,e.streamIds]):n.encode([v.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments]),ae.write(t.slice())},e.prototype.writeStreamItem=function(e){var t=se(this.messagePackOptions).encode([v.StreamItem,e.headers||{},e.invocationId,e.item]);return ae.write(t.slice())},e.prototype.writeCompletion=function(e){var t,n=se(this.messagePackOptions),r=e.error?this.errorResult:e.result?this.nonVoidResult:this.voidResult;switch(r){case this.errorResult:t=n.encode([v.Completion,e.headers||{},e.invocationId,r,e.error]);break;case this.voidResult:t=n.encode([v.Completion,e.headers||{},e.invocationId,r]);break;case this.nonVoidResult:t=n.encode([v.Completion,e.headers||{},e.invocationId,r,e.result])}return ae.write(t.slice())},e.prototype.writeCancelInvocation=function(e){var t=se(this.messagePackOptions).encode([v.CancelInvocation,e.headers||{},e.invocationId]);return ae.write(t.slice())},e.prototype.readHeaders=function(e){var t=e[1];if("object"!=typeof t)throw new Error("Invalid headers.");return t},e}(),fe=n(18),he=n(19),de=n(7);const pe="function"==typeof TextDecoder?new TextDecoder("utf-8"):null,ge=pe?pe.decode.bind(pe):function(e){let t=0;const n=e.length,r=[],o=[];for(;t65535&&(a-=65536,r.push(a>>>10&1023|55296),a=56320|1023&a),r.push(a)}r.length>1024&&(o.push(String.fromCharCode.apply(null,r)),r.length=0)}return o.push(String.fromCharCode.apply(null,r)),o.join("")};const ye=Math.pow(2,32),me=Math.pow(2,21)-1;function be(e,t){return e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24}function ve(e,t){return e[t]+(e[t+1]<<8)+(e[t+2]<<16)+(e[t+3]<<24>>>0)}function we(e,t){const n=ve(e,t+4);if(n>me)throw new Error(`Cannot read uint64 with high order part ${n}, because the result would exceed Number.MAX_SAFE_INTEGER.`);return n*ye+ve(e,t)}class Ee{constructor(e){this.batchData=e;const t=new ke(e);this.arrayRangeReader=new _e(e),this.arrayBuilderSegmentReader=new Te(e),this.diffReader=new Se(e),this.editReader=new Ie(e,t),this.frameReader=new Ce(e,t)}updatedComponents(){return be(this.batchData,this.batchData.length-20)}referenceFrames(){return be(this.batchData,this.batchData.length-16)}disposedComponentIds(){return be(this.batchData,this.batchData.length-12)}disposedEventHandlerIds(){return be(this.batchData,this.batchData.length-8)}updatedComponentsEntry(e,t){const n=e+4*t;return be(this.batchData,n)}referenceFramesEntry(e,t){return e+20*t}disposedComponentIdsEntry(e,t){const n=e+4*t;return be(this.batchData,n)}disposedEventHandlerIdsEntry(e,t){const n=e+8*t;return we(this.batchData,n)}}class Se{constructor(e){this.batchDataUint8=e}componentId(e){return be(this.batchDataUint8,e)}edits(e){return e+4}editsEntry(e,t){return e+16*t}}class Ie{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}editType(e){return be(this.batchDataUint8,e)}siblingIndex(e){return be(this.batchDataUint8,e+4)}newTreeIndex(e){return be(this.batchDataUint8,e+8)}moveToSiblingIndex(e){return be(this.batchDataUint8,e+8)}removedAttributeName(e){const t=be(this.batchDataUint8,e+12);return this.stringReader.readString(t)}}class Ce{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}frameType(e){return be(this.batchDataUint8,e)}subtreeLength(e){return be(this.batchDataUint8,e+4)}elementReferenceCaptureId(e){const t=be(this.batchDataUint8,e+4);return this.stringReader.readString(t)}componentId(e){return be(this.batchDataUint8,e+8)}elementName(e){const t=be(this.batchDataUint8,e+8);return this.stringReader.readString(t)}textContent(e){const t=be(this.batchDataUint8,e+4);return this.stringReader.readString(t)}markupContent(e){const t=be(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeName(e){const t=be(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeValue(e){const t=be(this.batchDataUint8,e+8);return this.stringReader.readString(t)}attributeEventHandlerId(e){return we(this.batchDataUint8,e+12)}}class ke{constructor(e){this.batchDataUint8=e,this.stringTableStartIndex=be(e,e.length-4)}readString(e){if(-1===e)return null;{const n=be(this.batchDataUint8,this.stringTableStartIndex+4*e),r=function(e,t){let n=0,r=0;for(let o=0;o<4;o++){const i=e[t+o];if(n|=(127&i)<this.nextBatchId)return this.fatalError?(this.logger.log(Oe.Debug,`Received a new batch ${e} but errored out on a previous batch ${this.nextBatchId-1}`),void await n.send("OnRenderCompleted",this.nextBatchId-1,this.fatalError.toString())):void this.logger.log(Oe.Debug,`Waiting for batch ${this.nextBatchId}. Batch ${e} not processed.`);try{this.nextBatchId++,this.logger.log(Oe.Debug,`Applying batch ${e}.`),Object(de.d)(this.browserRendererId,new Ee(t)),await this.completeBatch(n,e)}catch(t){throw this.fatalError=t.toString(),this.logger.log(Oe.Error,`There was an error applying batch ${e}.`),n.send("OnRenderCompleted",e,t.toString()),t}}getLastBatchid(){return this.nextBatchId-1}async completeBatch(e,t){try{await e.send("OnRenderCompleted",t,null)}catch{this.logger.log(Oe.Warning,`Failed to deliver completion notification for render '${t}'.`)}}}class Re{constructor(){}log(e,t){}}Re.instance=new Re;class Pe{constructor(e){this.minimumLogLevel=e}log(e,t){if(e>=this.minimumLogLevel)switch(e){case Oe.Critical:case Oe.Error:console.error(`[${(new Date).toISOString()}] ${Oe[e]}: ${t}`);break;case Oe.Warning:console.warn(`[${(new Date).toISOString()}] ${Oe[e]}: ${t}`);break;case Oe.Information:console.info(`[${(new Date).toISOString()}] ${Oe[e]}: ${t}`);break;default:console.log(`[${(new Date).toISOString()}] ${Oe[e]}: ${t}`)}}}var je=n(6),De=n(2);class Ae{constructor(e){this.circuitId=void 0,this.components=e}reconnect(e){if(!this.circuitId)throw new Error("Circuit host not initialized.");return e.invoke("ConnectCircuit",this.circuitId)}initialize(e){if(this.circuitId)throw new Error(`Circuit host '${this.circuitId}' already initialized.`);this.circuitId=e}async startCircuit(e){const t=await e.invoke("StartCircuit",je.b.getBaseURI(),je.b.getLocationHref(),JSON.stringify(this.components.map(e=>e.toRecord())));return!!t&&(this.initialize(t),!0)}resolveElement(e){const t=Number.parseInt(e);if(Number.isNaN(t))throw new Error(`Invalid sequence number '${e}'.`);return Object(De.l)(this.components[t].start,this.components[t].end)}}var Be=n(8);const Me={configureSignalR:e=>{},logLevel:Oe.Warning,reconnectionOptions:{maxRetries:8,retryIntervalMilliseconds:2e4,dialogId:"components-reconnect-modal"}};class Ne{constructor(e,t,n,r){this.maxRetries=t,this.document=n,this.logger=r,this.addedToDom=!1,this.modal=this.document.createElement("div"),this.modal.id=e,this.maxRetries=t;this.modal.style.cssText=["position: fixed","top: 0","right: 0","bottom: 0","left: 0","z-index: 1050","display: none","overflow: hidden","background-color: #fff","opacity: 0.8","text-align: center","font-weight: bold","transition: visibility 0s linear 500ms"].join(";"),this.modal.innerHTML='

Alternatively, reload

',this.message=this.modal.querySelector("h5"),this.button=this.modal.querySelector("button"),this.reloadParagraph=this.modal.querySelector("p"),this.loader=this.getLoader(),this.message.after(this.loader),this.button.addEventListener("click",async()=>{this.show();try{await window.Blazor.reconnect()||this.rejected()}catch(e){this.logger.log(Oe.Error,e),this.failed()}}),this.reloadParagraph.querySelector("a").addEventListener("click",()=>location.reload())}show(){this.addedToDom||(this.addedToDom=!0,this.document.body.appendChild(this.modal)),this.modal.style.display="block",this.loader.style.display="inline-block",this.button.style.display="none",this.reloadParagraph.style.display="none",this.message.textContent="Attempting to reconnect to the server...",this.modal.style.visibility="hidden",setTimeout(()=>{this.modal.style.visibility="visible"},0)}update(e){this.message.textContent=`Attempting to reconnect to the server: ${e} of ${this.maxRetries}`}hide(){this.modal.style.display="none"}failed(){this.button.style.display="block",this.reloadParagraph.style.display="none",this.loader.style.display="none",this.message.innerHTML="Reconnection failed. Try reloading the page if you're unable to reconnect.",this.message.querySelector("a").addEventListener("click",()=>location.reload())}rejected(){this.button.style.display="none",this.reloadParagraph.style.display="none",this.loader.style.display="none",this.message.innerHTML="Could not reconnect to the server. Reload the page to restore functionality.",this.message.querySelector("a").addEventListener("click",()=>location.reload())}getLoader(){const e=this.document.createElement("div");return e.style.cssText=["border: 0.3em solid #f3f3f3","border-top: 0.3em solid #3498db","border-radius: 50%","width: 2em","height: 2em","display: inline-block"].join(";"),e.animate([{transform:"rotate(0deg)"},{transform:"rotate(360deg)"}],{duration:2e3,iterations:1/0}),e}}class Ue{constructor(e,t,n){this.dialog=e,this.maxRetries=t,this.document=n,this.document=n;const r=this.document.getElementById(Ue.MaxRetriesId);r&&(r.innerText=this.maxRetries.toString())}show(){this.removeClasses(),this.dialog.classList.add(Ue.ShowClassName)}update(e){const t=this.document.getElementById(Ue.CurrentAttemptId);t&&(t.innerText=e.toString())}hide(){this.removeClasses(),this.dialog.classList.add(Ue.HideClassName)}failed(){this.removeClasses(),this.dialog.classList.add(Ue.FailedClassName)}rejected(){this.removeClasses(),this.dialog.classList.add(Ue.RejectedClassName)}removeClasses(){this.dialog.classList.remove(Ue.ShowClassName,Ue.HideClassName,Ue.FailedClassName,Ue.RejectedClassName)}}Ue.ShowClassName="components-reconnect-show",Ue.HideClassName="components-reconnect-hide",Ue.FailedClassName="components-reconnect-failed",Ue.RejectedClassName="components-reconnect-rejected",Ue.MaxRetriesId="components-reconnect-max-retries",Ue.CurrentAttemptId="components-reconnect-current-attempt";class Le{constructor(e,t,n){this._currentReconnectionProcess=null,this._logger=e,this._reconnectionDisplay=t,this._reconnectCallback=n||(()=>window.Blazor.reconnect())}onConnectionDown(e,t){if(!this._reconnectionDisplay){const t=document.getElementById(e.dialogId);this._reconnectionDisplay=t?new Ue(t,e.maxRetries,document):new Ne(e.dialogId,e.maxRetries,document,this._logger)}this._currentReconnectionProcess||(this._currentReconnectionProcess=new Fe(e,this._logger,this._reconnectCallback,this._reconnectionDisplay))}onConnectionUp(){this._currentReconnectionProcess&&(this._currentReconnectionProcess.dispose(),this._currentReconnectionProcess=null)}}class Fe{constructor(e,t,n,r){this.logger=t,this.reconnectCallback=n,this.isDisposed=!1,this.reconnectDisplay=r,this.reconnectDisplay.show(),this.attemptPeriodicReconnection(e)}dispose(){this.isDisposed=!0,this.reconnectDisplay.hide()}async attemptPeriodicReconnection(e){for(let t=0;tFe.MaximumFirstRetryInterval?Fe.MaximumFirstRetryInterval:e.retryIntervalMilliseconds;if(await this.delay(n),this.isDisposed)break;try{return await this.reconnectCallback()?void 0:void this.reconnectDisplay.rejected()}catch(e){this.logger.log(Oe.Error,e)}}this.reconnectDisplay.failed()}delay(e){return new Promise(t=>setTimeout(t,e))}}Fe.MaximumFirstRetryInterval=3e3;var He=n(20),qe=n(15);let We=!1,ze=!1;async function Je(e){if(ze)throw new Error("Blazor has already started.");ze=!0;const t=function(e){const t={...Me,...e};return e&&e.reconnectionOptions&&(t.reconnectionOptions={...Me.reconnectionOptions,...e.reconnectionOptions}),t}(e),n=new Pe(t.logLevel);window.Blazor.defaultReconnectionHandler=new Le(n),window.Blazor._internal.InputFile=qe.a,t.reconnectionHandler=t.reconnectionHandler||window.Blazor.defaultReconnectionHandler,n.log(Oe.Information,"Starting up blazor server-side application.");const r=Object(He.a)(document,"server"),o=new Ae(r),i=await Ye(t,n,o);if(!await o.startCircuit(i))return void n.log(Oe.Error,"Failed to start the circuit.");let s=!1;const a=()=>{if(!s){const e=new FormData,t=o.circuitId;e.append("circuitId",t),s=navigator.sendBeacon("_blazor/disconnect",e)}};window.Blazor.disconnect=a,window.addEventListener("unload",a,{capture:!1,once:!0}),window.Blazor.reconnect=async e=>{if(We)return!1;const r=e||await Ye(t,n,o);return await o.reconnect(r)?(t.reconnectionHandler.onConnectionUp(),!0):(n.log(Oe.Information,"Reconnection attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server."),!1)},n.log(Oe.Information,"Blazor server-side application started.")}async function Ye(e,t,n){const r=new le;r.name="blazorpack";const i=(new ie).withUrl("_blazor").withHubProtocol(r);e.configureSignalR(i);const s=i.build();Object(Be.b)((e,t)=>{s.send("DispatchBrowserEvent",JSON.stringify(e),JSON.stringify(t))}),window.Blazor._internal.navigationManager.listenForNavigationEvents((e,t)=>s.send("OnLocationChanged",e,t)),s.on("JS.AttachComponent",(e,t)=>Object(de.b)(0,n.resolveElement(t),e)),s.on("JS.BeginInvokeJS",o.a.jsCallDispatcher.beginInvokeJSFromDotNet),s.on("JS.EndInvokeDotNet",e=>o.a.jsCallDispatcher.endInvokeDotNetFromJS(...o.a.parseJsonWithRevivers(e)));const a=xe.getOrCreate(t);s.on("JS.RenderBatch",(e,n)=>{t.log(Oe.Debug,`Received render batch with id ${e} and ${n.byteLength} bytes.`),a.processBatch(e,n,s)}),s.onclose(t=>!We&&e.reconnectionHandler.onConnectionDown(e.reconnectionOptions,t)),s.on("JS.Error",e=>{We=!0,$e(s,e,t),Object(fe.a)()}),window.Blazor._internal.forceCloseConnection=()=>s.stop();try{await s.start()}catch(e){$e(s,e,t)}return o.a.attachDispatcher({beginInvokeDotNetFromJS:(e,t,n,r,o)=>{s.send("BeginInvokeDotNetFromJS",e?e.toString():null,t,n,r||0,o)},endInvokeJSFromDotNet:(e,t,n)=>{s.send("EndInvokeJSFromDotNet",e,t,n)}}),s}function $e(e,t,n){n.log(Oe.Error,t),e&&e.stop()}window.Blazor.start=Je,Object(he.a)()&&Je()}]); \ No newline at end of file