aspnetcore/src/Components/Browser.JS/src/Services/UriHelper.ts

117 lines
3.7 KiB
TypeScript

import '@dotnet/jsinterop';
let hasRegisteredNavigationInterception = false;
let hasRegisteredNavigationEventListeners = false;
// Will be initialized once someone registers
let notifyLocationChangedCallback: { assemblyName: string; functionName: string } | null = null;
// These are the functions we're making available for invocation from .NET
export const internalFunctions = {
listenForNavigationEvents,
enableNavigationInterception,
navigateTo,
getBaseURI: () => document.baseURI,
getLocationHref: () => location.href,
};
function listenForNavigationEvents(assemblyName: string, functionName: string) {
if (hasRegisteredNavigationEventListeners) {
return;
}
notifyLocationChangedCallback = { assemblyName, functionName };
hasRegisteredNavigationEventListeners = true;
window.addEventListener('popstate', () => notifyLocationChanged(false));
}
function enableNavigationInterception() {
if (hasRegisteredNavigationInterception) {
return;
}
hasRegisteredNavigationInterception = true;
document.addEventListener('click', event => {
if (event.button !== 0 || eventHasSpecialKey(event)) {
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
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;
const hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
const targetAttributeValue = anchorTarget.getAttribute('target');
const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
if (!opensInSameFrame) {
return;
}
const href = anchorTarget.getAttribute(hrefAttributeName)!;
const absoluteHref = toAbsoluteUri(href);
if (isWithinBaseUriSpace(absoluteHref)) {
event.preventDefault();
performInternalNavigation(absoluteHref, true);
}
}
});
}
export function navigateTo(uri: string, forceLoad: boolean) {
const absoluteUri = toAbsoluteUri(uri);
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
performInternalNavigation(absoluteUri, false);
} else {
location.href = uri;
}
}
function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean) {
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
notifyLocationChanged(interceptedLink);
}
async function notifyLocationChanged(interceptedLink: boolean) {
if (notifyLocationChangedCallback) {
await DotNet.invokeMethodAsync(
notifyLocationChangedCallback.assemblyName,
notifyLocationChangedCallback.functionName,
location.href,
interceptedLink
);
}
}
let testAnchor: HTMLAnchorElement;
function toAbsoluteUri(relativeUri: string) {
testAnchor = testAnchor || document.createElement('a');
testAnchor.href = relativeUri;
return testAnchor.href;
}
function findClosestAncestor(element: Element | null, tagName: string) {
return !element
? null
: element.tagName === tagName
? element
: findClosestAncestor(element.parentElement, tagName);
}
function isWithinBaseUriSpace(href: string) {
const baseUriWithTrailingSlash = toBaseUriWithTrailingSlash(document.baseURI!); // TODO: Might baseURI really be null?
return href.startsWith(baseUriWithTrailingSlash);
}
function toBaseUriWithTrailingSlash(baseUri: string) {
return baseUri.substr(0, baseUri.lastIndexOf('/') + 1);
}
function eventHasSpecialKey(event: MouseEvent) {
return event.ctrlKey || event.shiftKey || event.altKey || event.metaKey;
}