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;
}
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).
@ -37,7 +33,7 @@ export class EventDelegator {
private readonly eventsCollectionKey: string;
private readonly linkClickListeners: OnLinkClickEventCallback[] = [];
private readonly afterClickCallbacks: ((event: MouseEvent) => void)[] = [];
private eventInfoStore: EventInfoStore;
@ -79,14 +75,12 @@ 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 notifyAfterClick(callback: (event: MouseEvent) => void) {
// This is extremely special-case. It's needed so that navigation link click interception
// can be sure to run *after* our synthetic bubbling process. If a need arises, we can
// generalise this, but right now it's a purely internal detail.
this.afterClickCallbacks.push(callback);
this.eventInfoStore.addGlobalListener('click'); // Ensure we always listen for this
}
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;
}
// 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 {

View File

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