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;
|
(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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue