Make navigation+bubbling interactions work like native
This commit is contained in:
parent
47d1ffce05
commit
eab8ba85b9
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue