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 { applyCaptureIdToElement } from './ElementReferenceCapture';
|
||||||
import { EventFieldInfo } from './EventFieldInfo';
|
import { EventFieldInfo } from './EventFieldInfo';
|
||||||
import { dispatchEvent } from './RendererEventDispatcher';
|
import { dispatchEvent } from './RendererEventDispatcher';
|
||||||
|
import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager';
|
||||||
const selectValuePropname = '_blazorSelectValue';
|
const selectValuePropname = '_blazorSelectValue';
|
||||||
const sharedTemplateElemForParsing = document.createElement('template');
|
const sharedTemplateElemForParsing = document.createElement('template');
|
||||||
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
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) => {
|
this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs, eventFieldInfo) => {
|
||||||
raiseEvent(event, this.browserRendererId, 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 {
|
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;
|
(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
|
// 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
|
// 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).
|
// 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 eventsCollectionKey: string;
|
||||||
|
|
||||||
|
private readonly linkClickListeners: OnLinkClickEventCallback[] = [];
|
||||||
|
|
||||||
private eventInfoStore: EventInfoStore;
|
private eventInfoStore: EventInfoStore;
|
||||||
|
|
||||||
constructor(private onEvent: OnEventCallback) {
|
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) {
|
public setStopBubbling(element: Element, eventName: string, value: boolean) {
|
||||||
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
|
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
|
||||||
infoForElement.stopBubbling(eventName, value);
|
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;
|
candidateElement = (eventIsNonBubbling || stopBubblingWasRequested) ? null : candidateElement.parentElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +172,10 @@ class EventInfoStore {
|
||||||
|
|
||||||
this.infosByEventHandlerId[info.eventHandlerId] = info;
|
this.infosByEventHandlerId[info.eventHandlerId] = info;
|
||||||
|
|
||||||
const eventName = info.eventName;
|
this.addGlobalListener(info.eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addGlobalListener(eventName: string) {
|
||||||
if (this.countByEventName.hasOwnProperty(eventName)) {
|
if (this.countByEventName.hasOwnProperty(eventName)) {
|
||||||
this.countByEventName[eventName]++;
|
this.countByEventName[eventName]++;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import '@dotnet/jsinterop';
|
import '@dotnet/jsinterop';
|
||||||
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
|
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
|
||||||
|
import { EventDelegator } from '../Rendering/EventDelegator';
|
||||||
|
|
||||||
let hasRegisteredNavigationInterception = false;
|
let hasEnabledNavigationInterception = false;
|
||||||
let hasRegisteredNavigationEventListeners = false;
|
let hasRegisteredNavigationEventListeners = false;
|
||||||
|
|
||||||
// Will be initialized once someone registers
|
// Will be initialized once someone registers
|
||||||
|
|
@ -28,34 +29,38 @@ function listenForNavigationEvents(callback: (uri: string, intercepted: boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableNavigationInterception() {
|
function enableNavigationInterception() {
|
||||||
if (hasRegisteredNavigationInterception) {
|
hasEnabledNavigationInterception = true;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 (clickEvent.button !== 0 || eventHasSpecialKey(clickEvent)) {
|
||||||
if (event.button !== 0 || eventHasSpecialKey(event)) {
|
|
||||||
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
|
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intercept clicks on all <a> elements where the href is within the <base href> URI space
|
// 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
|
// 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';
|
const hrefAttributeName = 'href';
|
||||||
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
|
if (anchorElement.hasAttribute(hrefAttributeName)) {
|
||||||
const targetAttributeValue = anchorTarget.getAttribute('target');
|
const targetAttributeValue = anchorElement.getAttribute('target');
|
||||||
const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
|
const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
|
||||||
if (!opensInSameFrame) {
|
if (!opensInSameFrame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const href = anchorTarget.getAttribute(hrefAttributeName)!;
|
const href = anchorElement.getAttribute(hrefAttributeName)!;
|
||||||
const absoluteHref = toAbsoluteUri(href);
|
const absoluteHref = toAbsoluteUri(href);
|
||||||
|
|
||||||
if (isWithinBaseUriSpace(absoluteHref)) {
|
if (isWithinBaseUriSpace(absoluteHref)) {
|
||||||
event.preventDefault();
|
clickEvent.preventDefault();
|
||||||
performInternalNavigation(absoluteHref, true);
|
performInternalNavigation(absoluteHref, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue