Make navigation interception participate in synthetic event bubbling
This commit is contained in:
parent
cdef672310
commit
896feb49e9
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -5,6 +5,7 @@ import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalCh
|
|||
import { applyCaptureIdToElement } from './ElementReferenceCapture';
|
||||
import { EventFieldInfo } from './EventFieldInfo';
|
||||
import { dispatchEvent } from './RendererEventDispatcher';
|
||||
import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager';
|
||||
const selectValuePropname = '_blazorSelectValue';
|
||||
const sharedTemplateElemForParsing = document.createElement('template');
|
||||
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
|
|
@ -26,6 +27,11 @@ export class BrowserRenderer {
|
|||
this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs, eventFieldInfo) => {
|
||||
raiseEvent(event, this.browserRendererId, eventHandlerId, eventArgs, eventFieldInfo);
|
||||
});
|
||||
|
||||
// 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
|
||||
// in the synthetic event bubbling process later
|
||||
attachNavigationManagerToEventDelegator(this.eventDelegator);
|
||||
}
|
||||
|
||||
public attachRootComponentToLogicalElement(componentId: number, element: LogicalElement): void {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ export interface OnEventCallback {
|
|||
(event: Event, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>, eventFieldInfo: EventFieldInfo | null): void;
|
||||
}
|
||||
|
||||
export interface OnLinkClickEventCallback {
|
||||
(event: MouseEvent, element: HTMLAnchorElement): 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).
|
||||
|
|
@ -33,6 +37,8 @@ export class EventDelegator {
|
|||
|
||||
private readonly eventsCollectionKey: string;
|
||||
|
||||
private readonly linkClickListeners: OnLinkClickEventCallback[] = [];
|
||||
|
||||
private eventInfoStore: EventInfoStore;
|
||||
|
||||
constructor(private onEvent: OnEventCallback) {
|
||||
|
|
@ -73,6 +79,16 @@ export class EventDelegator {
|
|||
}
|
||||
}
|
||||
|
||||
public addLinkClickListener(listener: OnLinkClickEventCallback) {
|
||||
// This is very special-case. We could implement a full-fledged system for
|
||||
// registering delegated event handlers (e.g., listening for all events that
|
||||
// match a certain pattern, not just on a specific element) but there's no
|
||||
// use case for that currently. Instead, just specifically let callers
|
||||
// register for notifications about link clicks anywhere
|
||||
this.linkClickListeners.push(listener);
|
||||
this.eventInfoStore.addGlobalListener('click');
|
||||
}
|
||||
|
||||
public setStopBubbling(element: Element, eventName: string, value: boolean) {
|
||||
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
|
||||
infoForElement.stopBubbling(eventName, value);
|
||||
|
|
@ -116,6 +132,13 @@ export class EventDelegator {
|
|||
}
|
||||
}
|
||||
|
||||
// Special case for link clicks for navigation interception
|
||||
if (candidateElement instanceof HTMLAnchorElement && evt instanceof MouseEvent && evt.type === 'click') {
|
||||
if (!evt.defaultPrevented) {
|
||||
this.linkClickListeners.forEach(listener => listener(evt as MouseEvent, candidateElement as HTMLAnchorElement));
|
||||
}
|
||||
}
|
||||
|
||||
candidateElement = (eventIsNonBubbling || stopBubblingWasRequested) ? null : candidateElement.parentElement;
|
||||
}
|
||||
}
|
||||
|
|
@ -149,7 +172,10 @@ class EventInfoStore {
|
|||
|
||||
this.infosByEventHandlerId[info.eventHandlerId] = info;
|
||||
|
||||
const eventName = info.eventName;
|
||||
this.addGlobalListener(info.eventName);
|
||||
}
|
||||
|
||||
public addGlobalListener(eventName: string) {
|
||||
if (this.countByEventName.hasOwnProperty(eventName)) {
|
||||
this.countByEventName[eventName]++;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import '@dotnet/jsinterop';
|
||||
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
|
||||
import { EventDelegator } from '../Rendering/EventDelegator';
|
||||
|
||||
let hasRegisteredNavigationInterception = false;
|
||||
let hasEnabledNavigationInterception = false;
|
||||
let hasRegisteredNavigationEventListeners = false;
|
||||
|
||||
// Will be initialized once someone registers
|
||||
|
|
@ -28,34 +29,38 @@ function listenForNavigationEvents(callback: (uri: string, intercepted: boolean)
|
|||
}
|
||||
|
||||
function enableNavigationInterception() {
|
||||
if (hasRegisteredNavigationInterception) {
|
||||
return;
|
||||
}
|
||||
hasEnabledNavigationInterception = true;
|
||||
}
|
||||
|
||||
hasRegisteredNavigationInterception = true;
|
||||
export function attachToEventDelegator(eventDelegator: EventDelegator) {
|
||||
// We need to participate in EventDelegator's synthetic event bubbling process
|
||||
// (so we can respect stopBubbling/preventDefault), so register with that instead
|
||||
// of using a native JS event
|
||||
eventDelegator.addLinkClickListener((clickEvent, anchorElement) => {
|
||||
if (!hasEnabledNavigationInterception) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('click', event => {
|
||||
if (event.button !== 0 || eventHasSpecialKey(event)) {
|
||||
if (clickEvent.button !== 0 || eventHasSpecialKey(clickEvent)) {
|
||||
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept clicks on all <a> elements where the href is within the <base href> URI space
|
||||
// We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser
|
||||
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement;
|
||||
const hrefAttributeName = 'href';
|
||||
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
|
||||
const targetAttributeValue = anchorTarget.getAttribute('target');
|
||||
if (anchorElement.hasAttribute(hrefAttributeName)) {
|
||||
const targetAttributeValue = anchorElement.getAttribute('target');
|
||||
const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
|
||||
if (!opensInSameFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
const href = anchorTarget.getAttribute(hrefAttributeName)!;
|
||||
const href = anchorElement.getAttribute(hrefAttributeName)!;
|
||||
const absoluteHref = toAbsoluteUri(href);
|
||||
|
||||
if (isWithinBaseUriSpace(absoluteHref)) {
|
||||
event.preventDefault();
|
||||
clickEvent.preventDefault();
|
||||
performInternalNavigation(absoluteHref, true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue