Make navigation interception participate in synthetic event bubbling

This commit is contained in:
Steve Sanderson 2019-09-26 15:14:00 +01:00 committed by Artak
parent cdef672310
commit 896feb49e9
5 changed files with 54 additions and 17 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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 {

View File

@ -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 {

View File

@ -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);
}
}