Make navigation+bubbling interactions work like native

This commit is contained in:
Steve Sanderson 2019-09-27 12:02:18 +01:00 committed by Artak
parent 47d1ffce05
commit eab8ba85b9
4 changed files with 30 additions and 33 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

@ -25,10 +25,6 @@ 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).
@ -37,7 +33,7 @@ export class EventDelegator {
private readonly eventsCollectionKey: string; private readonly eventsCollectionKey: string;
private readonly linkClickListeners: OnLinkClickEventCallback[] = []; private readonly afterClickCallbacks: ((event: MouseEvent) => void)[] = [];
private eventInfoStore: EventInfoStore; private eventInfoStore: EventInfoStore;
@ -79,14 +75,12 @@ export class EventDelegator {
} }
} }
public addLinkClickListener(listener: OnLinkClickEventCallback) { public notifyAfterClick(callback: (event: MouseEvent) => void) {
// This is very special-case. We could implement a full-fledged system for // This is extremely special-case. It's needed so that navigation link click interception
// registering delegated event handlers (e.g., listening for all events that // can be sure to run *after* our synthetic bubbling process. If a need arises, we can
// match a certain pattern, not just on a specific element) but there's no // generalise this, but right now it's a purely internal detail.
// use case for that currently. Instead, just specifically let callers this.afterClickCallbacks.push(callback);
// register for notifications about link clicks anywhere this.eventInfoStore.addGlobalListener('click'); // Ensure we always listen for this
this.linkClickListeners.push(listener);
this.eventInfoStore.addGlobalListener('click');
} }
public setStopPropagation(element: Element, eventName: string, value: boolean) { public setStopPropagation(element: Element, eventName: string, value: boolean) {
@ -132,15 +126,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 || stopPropagationWasRequested) ? null : candidateElement.parentElement; 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 { private getEventHandlerInfosForElement(element: Element, createIfNeeded: boolean): EventHandlerInfosForElement | null {

View File

@ -33,34 +33,39 @@ function enableNavigationInterception() {
} }
export function attachToEventDelegator(eventDelegator: EventDelegator) { export function attachToEventDelegator(eventDelegator: EventDelegator) {
// We need to participate in EventDelegator's synthetic event bubbling process // We need to respond to clicks on <a> elements *after* the EventDelegator has finished
// (so we can respect stopPropagation/preventDefault), so register with that instead // running its simulated bubbling process so that we can respect any preventDefault requests.
// of using a native JS event // So instead of registering our own native event, register using the EventDelegator.
eventDelegator.addLinkClickListener((clickEvent, anchorElement) => { eventDelegator.notifyAfterClick(event => {
if (!hasEnabledNavigationInterception) { if (!hasEnabledNavigationInterception) {
return; return;
} }
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;
} }
if (event.defaultPrevented) {
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 | null;
const hrefAttributeName = 'href'; const hrefAttributeName = 'href';
if (anchorElement.hasAttribute(hrefAttributeName)) { if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
const targetAttributeValue = anchorElement.getAttribute('target'); const targetAttributeValue = anchorTarget.getAttribute('target');
const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self'; const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
if (!opensInSameFrame) { if (!opensInSameFrame) {
return; return;
} }
const href = anchorElement.getAttribute(hrefAttributeName)!; const href = anchorTarget.getAttribute(hrefAttributeName)!;
const absoluteHref = toAbsoluteUri(href); const absoluteHref = toAbsoluteUri(href);
if (isWithinBaseUriSpace(absoluteHref)) { if (isWithinBaseUriSpace(absoluteHref)) {
clickEvent.preventDefault(); event.preventDefault();
performInternalNavigation(absoluteHref, true); performInternalNavigation(absoluteHref, true);
} }
} }