I am trying to have all external links on my website raise a confirm saying "You are being redirected to an external site." I know how to write the JS to check for if a link is external and how to raise the confirm, but is there a way I can apply this to every link in my site without going through individually? The links will always have the format <a href=URL> Link </a>. An angular script would check if the URL subdomain is the same as my site, and if not it will add onclick=return confirm('You will now be redirected to an exteral site') and target="_blank" to the link HTML.
As you already said, this can be achieved by using confirm onclick, you can easily add an EventListener to all a Elements that are external (l.hostname !== location.hostname) in your page and only redirect after the user accepts the message, just like so:
Array.from(document.querySelectorAll("a")).filter(l => l.hostname !== location.hostname).forEach(el => el.addEventListener("click", evtL));
function evtL(e) {
if (confirm("Are you sure you want to leave the page?")) {
return; //redirect
} else {
e.preventDefault();
return false; //don't redirect
}
}
internal
external
also internal
It can be achieved by the regex matching.
Instead of adding multiple event listeners, single event listener can be added on the document object and detect the external links as follows. It will give more efficiency.
For reference, Handling events for multiple elements in a single listener
var regexp = /https?:\/\/(www.){0,1}((?:[\w\d-]+\.)+[\w\d]{2,})/i;
function isExternal(url)
{
return regexp.exec(location.href)[2] !== regexp.exec(url)[2];
}
document.addEventListener("click",function(){
if(event.target.tagName.toLowerCase()==="a")
{
if(isExternal(event.target.href))
{
if(confirm("Redirecting to an external site...."))
{
console.log("Going to external site...");
return;
}
else
{
event.preventDefault();
return;
}
}
else
{
console.log("Internal URL Clicked.")
}
}
});
External Site 1 - https://www.google.com<br>
External Site 2 - https://www.stackoverflow.com<br>
External Site 3 - http://www.facebook.com (Using HTTP)<br>
Internal Site (Using HTTP) - http://www.stacksnippets.net<br>
Internal Site (without www.) - http://stacksnippets.net<br>
Internal reference - /path/to/something (Internal reference)<br>
Thanks #pseudosavant for the regex.
Though #Luca's answer works, the hostname is not supported in IE, Safari and Opera. Browser Compatibility for reference.
Since you are using Angular.js, I would recommend taking a look at how they work with <a> since they already have a directive applied to all <a> to make ngHref work. The source would be here: <a> directive.
The basic idea is to put the logic that you use to change the href to the warning or display a modal or whatever in the element.on('click', function(event) {...}).
Because Angular.js already defined an <a> directive, you may need to fiddle with the priority of your directive so that you don't accidentally break the way Angular fiddles with <a>.
The directive would look something like this:
// Logic that dictactes whether the URL should show a warning
function isSuspectUrl(l) {
return false;
}
const app = angular
.module("aWarn", [])
.directive("a", function() {
return {
restrict: "E",
compile: function(el, attr) {
element.on("click", function(e) {
if (isSuspectUrl(attr.href)) {
// Logic that would display a warning
}
});
}
}
});
2021 version
es2015+
Works with SPA
Handles local links that look external
Subdomains support
const baseUrl = window.location.origin;
const absoluteUrlRegex = new RegExp('^(?:[a-z]+:)?//', 'i');
const isAbsoluteUrl = (url) => absoluteUrlRegex.test(url);
const isLocalUrl = (url) => url.startsWith(baseUrl) || !isAbsoluteUrl(url);
// https://gist.github.com/ -> github.com
// https://stackoverflow.com/ -> stackoverflow.com
const getDomain = (url) => {
const urlInstance = new URL(url);
const dividedUrl = urlInstance.hostname.split('.');
const urlDomainsCount = dividedUrl.length;
return urlDomainsCount === 2
? urlInstance.hostname
: `${dividedUrl[urlDomainsCount - 2]}.${dividedUrl[urlDomainsCount - 1]}`;
};
// example whitelist
const whitelist = [
'twitter.com',
'github.com',
];
// url has same domain or whitelisted
const isWhitelistedUrl = (url) => {
const domain = getDomain(url);
return domain === window.location.hostname || whitelist.includes(domain);
};
// bind listener
const confirmExternalLinks = (confirmationFn) => {
document.addEventListener('click', async (e) => {
const linkElement = e.target.closest('a');
if (!linkElement) return;
const link = linkElement.getAttribute('href');
if (isLocalUrl(link) || isWhitelistedUrl(link)) return;
e.preventDefault();
const confirmation = await confirmationFn(link);
if (confirmation) window.open(link);
});
};
// tip: replace confirmationFn with your custom handler which returns Promise
confirmExternalLinks((link) => confirm(`Proceed to ${link}?`));
<a target="_blank" href="https://stacksnippets.net/">https://stacksnippets.net/</a> is local for this snippet iframe
<br>
<a target="_blank" href="https://twitter.com/">https://twitter.com/</a> is whitelisted
<br>
<a target="_blank" href="https://developer.mozilla.org/">https://developer.mozilla.org/</a> is external
Related
I can disable opening new tab or window in case of <a> tag by removing target attribute e.g. target='_blank'
In some cases instead of <a> website put <button> and add onClick() or submit() etc which programmatically generates a link and sets the target attribute.
<button id="653cde4c-1620-11ec-85b5-5ae26c154b46">
<div>click here</div>
</button>
In some webpack generated javascript file there would be something like which would not be easy to find and read minified code.
var button = document.querySelctor('653cde4c-1620-11ec-85b5-5ae26c154b46')
// Add whatever you want to add to this button element, e.g. add onClick() or submit() or something else which I am not sure of
We can see image at https://i.stack.imgur.com/8oG5w.png
In such case where <button> is providing link click functionality. I can not view and edit onClick() or submit() etc as its webpack generated javascript. I can only run my javascript code after loading of that webapge in devtools console.
How can I disable opening new tab or new window of browser in such case? Or What javascript code should I run to override <button> link behaviour?
Depending on how the webpack based code does manage the tab handling of resources one possible solution was to wrap own code around window.open or even entirely replace it with an own implementation.
As for the wrapping, based on the intercepted data, one could decide of whether one does suppress the url-handling entirely or, instead of opening a new tab/window, does a forwarding to location.href or even proceed with invoking the original window.open.
// untouchable 3rd party code
//
function alienLocationHandler({ currentTarget }) {
let { url, target } = currentTarget.dataset;
url = (url ?? '').trim();
if (url) {
target = (target ?? '').trim();
if (target) {
// due to SO's permission handling ...
//
// ... Blocked opening 'https://stackoverflow.com/' in a
// new window because the request was made in a sandboxed
// frame whose 'allow-popups' permission is not set.
//
window.open(url, target);
} else {
window.location.href = url;
}
}
}
document
.querySelectorAll('[data-url]')
.forEach(elmNode =>
elmNode.addEventListener('click', alienLocationHandler)
);
// one possible approach :: wrap around `window.open`
//
window.open = (function createAroundHandler(proceed, thisArg) {
return function aroundWindowOpen(url, target, ...rest) {
console.log('aroundWindowOpen ...', {
url,
target,
rest,
});
// - of cause all the code is just for demonstration purpose.
//
// - the OP has to come up with own handler logic which does
// fit the OP's needs best.
if (url !== 'https://stackoverflow.com') {
// invocation of the original `window.open`
// will be blocked by SO's permission handling.
proceed.call(thisArg, url, target, ...rest);
} else {
// delayed fowarding to `location.href`.
setTimeout(() => { window.location.href = url }, 5000);
}
};
}(window.open, window));
body { margin: 0; }
.as-console-wrapper { min-height: 87%; top: auto; }
<button data-url="https://stackoverflow.com">
no interception
</button>
<button
data-url="https://google.com"
data-target='google'
>
intercepted & open attempt
</button>
<button
id="_653cde4c-1620-11ec-85b5-5ae26c154b46"
data-url="https://stackoverflow.com"
data-target='_blank'
>
intercepted and forwarded with 5sec delay
</button>
Another approach was to make use of event delegation by listening to and handling click events at e.g. document.body level.
// untouchable 3rd party code
//
function alienLocationHandler({ currentTarget }) {
let { url, target } = currentTarget.dataset;
url = (url ?? '').trim();
if (url) {
target = (target ?? '').trim();
if (target) {
// due to SO's permission handling ...
//
// ... Blocked opening 'https://stackoverflow.com/' in a
// new window because the request was made in a sandboxed
// frame whose 'allow-popups' permission is not set.
//
window.open(url, target);
} else {
window.location.href = url;
}
}
}
document
.querySelectorAll('[data-url]')
.forEach(elmNode =>
elmNode.addEventListener('click', alienLocationHandler)
);
// another possible approach :: event delegation
//
// - [https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_delegation]
// - [https://davidwalsh.name/event-delegate]
//
// - [https://javascript.info/event-delegation]
// - [https://learn.jquery.com/events/event-delegation/]
//
function isButtonEvent(activeElement, targetNode) {
return (activeElement.tagName.toLowerCase() === 'button') && (
activeElement.isSameNode(targetNode) ||
activeElement.contains(targetNode)
);
}
function preventSpecificButtonClickBehavior(evt) {
const elmButton = document.activeElement;
if (isButtonEvent(elmButton, evt.target)) {
const url = (elmButton.dataset.url ?? '').trim();
const isUrlBasedPrevented = (url !== 'https://stackoverflow.com');
const isIdBasedPrevented = ((elmButton.id ?? '').trim() !== '');
if (isUrlBasedPrevented || isIdBasedPrevented) {
evt.stopImmediatePropagation();
evt.stopPropagation();
if (isUrlBasedPrevented) {
console.log('prevented button click behavior ... URL based')
}
if (isIdBasedPrevented) {
console.log('prevented button click behavior ... ID based')
}
}
}
}
document
.body
.addEventListener(
'click',
preventSpecificButtonClickBehavior,
// - [https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters]
// - The boolean `useCapture` parameter/flag does the trick if set as `true` value.
true
);
body { margin: 0; }
.as-console-wrapper { min-height: 87%; top: auto; }
<button data-url="https://stackoverflow.com">
click behavior not prevented
</button>
<button
id="_653cde4c-1620-11ec-85b5-5ae26c154b46"
data-url="https://stackoverflow.com"
>
prevented behavior (id based)
</button>
<button
data-url="https://google.com"
data-target='google'
>
<span>
<span>
prevented behavior (url based)
</span>
</span>
</button>
Probably the logic in the onClick() does something like window.open(url, [params]). You can change this to setting window.location.href = url; and the url will be loaded in the same window.
I have a page that includes a third party script (Xsolla login). This script modifies elements on the page, one of the particular elements being iframe.
First a placeholder element is inserted
Then the iframe is deleted and new iframe is inserted with different dynamically loading content
Both iframes have the same id
How one can detect when the second, replaced, iframe is correctly loaded as Cypress cy.get() grabs the first iframe and then never detects newly changed content within the replaced iframe?
You can use cypress-wait-until plugin and then write a custom check function that inspects deep into the iframe.
/**
* Because Xsolla does dynamic replacement of iframe element, we need to use this hacky wait.
*/
function checkLoadedXsollaIframe(doc: Document): bool {
try {
const iframe = doc.getElementById('XsollaLoginWidgetIframe') as any;
if(!iframe) {
return false;
}
// This element becomes available only when Xsolla has done its magic JS loading
const usernameInput = iframe.contentDocument.querySelector('input[name="email"]');
return usernameInput !== null;
} catch(e) {
return false;
}
}
context('Login', () => {
beforeEach(() => {
cy.visit(frontendURL);
});
it('Should login with valid credentials', () => {
// This injects Xsolla <script> tag and then
// this third party script takes its own course of actions
cy.get('.login-btn').first().click();
cy.waitUntil(() => cy.document().then(doc => checkLoadedXsollaIframe(doc)), {timeout: 5000 });
Below is the snippet that waits for the content inside the iframe to be loaded and HTMLElements be available & no timeouts required.
const iframeElement = (selector) => {
const iframe = cy.get(selector);
return iframe
.should(($iframe) => // Make sure its not blank
expect($iframe.attr('src')).not.to.include('about:blank')
)
.should(($iframe) =>
expect($iframe.attr('src')).not.to.be.empty) // Make sure its not empty
.then(($inner) => {
const iWindow = $inner[0].contentWindow;
return new Promise((resolve) => {
resolve(iWindow);
});
})
.then((iWindow) => {
return new Promise((resolve) => {
iWindow.onload = () => { // Listen to onLoad event
resolve(iWindow.document);
};
});
})
.then((iDoc) => {
return cy.wrap(iDoc.body); // Wrap the element to access Cypress API
});
};
Now access the element inside the iframeDocument
iframeElement('#my-iframe') // Grab the iframe
.find('h2')
.should('have.text', 'My header text'); //Assert iframe header
Note: Don't attempt to access CORS websites. It might fail due to
security reasons
I am observing very peculiar behaviour, when testing system on Samsung tablet some of links stopped working. To make it easier to debug I am using Chrome Developer tools emulator, when page is loaded I can call kncoukout.js global ko from console for debugging (like getting context for bound elements...) thing is when I am trying to debug from event handler ko vanishes.
Have a look at screenshot below.
I have been able to access ko when not in callback,
then I placed breakpoint in event handler and tried to access ko again on a click and it's not there anymore, and then it fails with error of ko not being there.
What is going on, how can ko just disappear?
Not that I think it matters (same happens in any event handler), but someone will ask for code that makes ko disappear.
var parseHrefBinding = function (link) {
var newHref = null;
var boundEl = $(link).closest('[data-bind]')[0];
if (boundEl) {
var data = ko.dataFor(boundEl);
if (data) {
var a = document.createElement('a');
a.setAttribute('data-bind', link.attributes['data-bind'].value);
ko.applyBindings(data, a);
newHref = a.href;
}
}
return newHref;
};
$(document).on("click", "a", function () {
var res = true;
var link = this;
var parent = $(link).parents('a')[0];
if (parent) {
// The default Android browser (V2-4.1) sets nested a[href] to their parent's href.
var href = parent.href === link.href ? parseHrefBinding(link) : link.href;
setTimeout(function () {
document.location.href = href;
}, 10);
res = false;
}
return res;
});
God this is epic, it turns out that to use globals in event handlers on Android Browser 4 one has to add window in front of ko eg. window.ko see screenshot below.
so fix was as simple as changing
var data = ko.dataFor(boundEl);
to
var data = window.ko.dataFor(boundEl);
I am trying to display a 'mask' on my client while a file is dynamically generated server side. Seems like the recommend work around for this (since its not ajax) is to use an iframe and listen from the onload or done event to determine when the file has actually shipped to the client from the server.
here is my angular code:
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
e.load(function() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
});
angular.element('body').append(e);
This works great in Firefox but no luck in Chrome. I have also tried to use the onload function:
e.onload = function() { //unmask here }
But I did not have any luck there either.
Ideas?
Unfortunately it is not possible to use an iframe's onload event in Chrome if the content is an attachment. This answer may provide you with an idea of how you can work around it.
I hate this, but I couldn't find any other way than checking whether it is still loading or not except by checking at intervals.
var timer = setInterval(function () {
iframe = document.getElementById('iframedownload');
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Check if loading is complete
if (iframeDoc.readyState == 'complete' || iframeDoc.readyState == 'interactive') {
loadingOff();
clearInterval(timer);
return;
}
}, 4000);
You can do it in another way:
In the main document:
function iframeLoaded() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
}
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
angular.element('body').append(e);
In the iframe document (this is, inside the html of the page referenced by url)
window.onload = function() {
parent.iframeLoaded();
}
This will work if the main page, and the page inside the iframe are in the same domain.
Actually, you can access the parent through:
window.parent
parent
//and, if the parent is the top-level document, and not inside another frame
top
window.top
It's safer to use window.parent since the variables parent and top could be overwritten (usually not intended).
you have to consider 2 points:
1- first of all, if your url has different domain name, it is not possible to do this except when you have access to the other domain to add the Access-Control-Allow-Origin: * header, to fix this go to this link.
2- but if it has the same domain or you have added Access-Control-Allow-Origin: * to the headers of your domain, you can do what you want like this:
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
angular.element(document.body).append(e);
e[0].contentWindow.onload = function() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
};
I have done this in all kinds of browsers.
I had problems with the iframe taking too long to load. The iframe registered as loaded while the request wasn't handled. I came up with the following solution:
JS
Function:
function iframeReloaded(iframe, callback) {
let state = iframe.contentDocument.readyState;
let checkLoad = setInterval(() => {
if (state !== iframe.contentDocument.readyState) {
if (iframe.contentDocument.readyState === 'complete') {
clearInterval(checkLoad);
callback();
}
state = iframe.contentDocument.readyState;
}
}, 200)
}
Usage:
iframeReloaded(iframe[0], function () {
console.log('Reloaded');
})
JQuery
Function:
$.fn.iframeReloaded = function (callback) {
if (!this.is('iframe')) {
throw new Error('The element is not an iFrame, please provide the correct element');
}
let iframe = this[0];
let state = iframe.contentDocument.readyState;
let checkLoad = setInterval(() => {
if (state !== iframe.contentDocument.readyState) {
if (iframe.contentDocument.readyState === 'complete') {
clearInterval(checkLoad);
callback();
}
state = iframe.contentDocument.readyState;
}
}, 200)
}
Usage:
iframe.iframeReloaded(function () {
console.log('Reloaded');
})
I've just noticed that Chrome is not always firing the load event for the main page so this could have an effect on iframes too as they are basically treated the same way.
Use Dev Tools or the Performance api to check if the load event is being fired at all.
I just checked http://ee.co.uk/ and if you open the console and enter window.performance.timing you'll find the entries for domComplete, loadEventStart and loadEventEnd are 0 - at least at this current time:)
Looks like there is a problem with Chrome here - I've checked it on 2 PCs using the latest version 31.0.1650.63.
Update: checked ee again and load event fired but not on subsequent reloads so this is intermittent and may possibly be related to loading errors on their site. But the load event should fire whatever.
This problem has occurred on 5 or 6 sites for me now in the last day since I noticed my own site monitoring occasionally failed. Only just pinpointed the cause to this. I need some beauty sleep then I'll investigate further when I'm more awake.
How do I test to see if links are external or internal? Please note:
I cannot hard code the local domain.
I cannot test for "http". I could just as easily be linking to my own site with an http absolute link.
I want to use jQuery / javascript, not css.
I suspect the answer lies somewhere in location.href, but the solution evades me.
Thanks!
I know this post is old but it still shows at the top of results so I wanted to offer another approach. I see all the regex checks on an anchor element, but why not just use window.location.host and check against the element's host property?
function link_is_external(link_element) {
return (link_element.host !== window.location.host);
}
With jQuery:
$('a').each(function() {
if (link_is_external(this)) {
// External
}
});
and with plain javascript:
var links = document.getElementsByTagName('a');
for (var i = 0; i < links.length; i++) {
if (link_is_external(links[i])) {
// External
}
}
var comp = new RegExp(location.host);
$('a').each(function(){
if(comp.test($(this).attr('href'))){
// a link that contains the current host
$(this).addClass('local');
}
else{
// a link that does not contain the current host
$(this).addClass('external');
}
});
Note: this is just a quick & dirty example. It would match all href="#anchor" links
as external too. It might be improved by doing some extra RegExp checking.
Update 2016-11-17
This question still got a lot of traffic and I was told by a ton of people that this accepted solution will fail on several occasions. As I stated, this was a very quick and dirty answer to show the principal way how to solve this problem. A more sophisticated solution is to use the properties which are accessible on a <a> (anchor) element. Like #Daved already pointed out in this answer, the key is to compare the hostname with the current window.location.hostname. I would prefer to compare the hostname properties, because they never include the port which is included to the host property if it differs from 80.
So here we go:
$( 'a' ).each(function() {
if( location.hostname === this.hostname || !this.hostname.length ) {
$(this).addClass('local');
} else {
$(this).addClass('external');
}
});
State of the art:
Array.from( document.querySelectorAll( 'a' ) ).forEach( a => {
a.classList.add( location.hostname === a.hostname || !a.hostname.length ? 'local' : 'external' );
});
And the no-jQuery way
var nodes = document.getElementsByTagName("a"), i = nodes.length;
var regExp = new RegExp("//" + location.host + "($|/)");
while(i--){
var href = nodes[i].href;
var isLocal = (href.substring(0,4) === "http") ? regExp.test(href) : true;
alert(href + " is " + (isLocal ? "local" : "not local"));
}
All hrefs not beginning with http (http://, https://) are automatically treated as local
var external = RegExp('^((f|ht)tps?:)?//(?!' + location.host + ')');
Usage:
external.test('some url'); // => true or false
Here's a jQuery selector for only external links:
$('a[href^="(http:|https:)?//"])')
A jQuery selector only for internal links (not including hash links within the same page) needs to be a bit more complicated:
$('a:not([href^="(http:|https:)?//"],[href^="#"],[href^="mailto:"])')
Additional filters can be placed inside the :not() condition and separated by additional commas as needed.
http://jsfiddle.net/mblase75/Pavg2/
Alternatively, we can filter internal links using the vanilla JavaScript href property, which is always an absolute URL:
$('a').filter( function(i,el) {
return el.href.indexOf(location.protocol+'//'+location.hostname)===0;
})
http://jsfiddle.net/mblase75/7z6EV/
You forgot one, what if you use a relative path.
forexample: /test
hostname = new RegExp(location.host);
// Act on each link
$('a').each(function(){
// Store current link's url
var url = $(this).attr("href");
// Test if current host (domain) is in it
if(hostname.test(url)){
// If it's local...
$(this).addClass('local');
}
else if(url.slice(0, 1) == "/"){
$(this).addClass('local');
}
else if(url.slice(0, 1) == "#"){
// It's an anchor link
$(this).addClass('anchor');
}
else {
// a link that does not contain the current host
$(this).addClass('external');
}
});
There are also the issue of file downloads .zip (local en external) which could use the classes "local download" or "external download". But didn't found a solution for it yet.
const isExternalLink = (url) => {
const tmp = document.createElement('a');
tmp.href = url;
return tmp.host !== window.location.host;
};
// output: true
console.log(isExternalLink('https://foobar.com'));
console.log(isExternalLink('//foobar.com'));
// output: false
console.log(isExternalLink('https://www.stackoverflow.com'));
console.log(isExternalLink('//www.stackoverflow.com'));
console.log(isExternalLink('/foobar'));
console.log(isExternalLink('#foobar'));
The benefit of using this approach is that:
It would automatically resolve the hostname for relative paths and fragments;
It works with protocol-relative URLs
To demonstrate this, let's look at the following examples:
const lnk = document.createElement('a');
lnk.href = '/foobar';
console.log(lnk.host); // output: 'www.stackoverflow.com'
const lnk = document.createElement('a');
lnk.href = '#foobar';
console.log(lnk.host); // output: 'www.stackoverflow.com'
const lnk = document.createElement('a');
lnk.href = '//www.stackoverflow.com';
console.log(lnk.host); // output: 'www.stackoverflow.com'
With jQuery
jQuery('a').each(function() {
if (this.host !== window.location.host) {
console.log(jQuery(this).attr('href'));
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
You can use is-url-external module.
var isExternal = require('is-url-external');
isExternal('http://stackoverflow.com/questions/2910946'); // true | false
/**
* All DOM url
* [links description]
* #type {[type]}
*/
var links = document.querySelectorAll('a');
/**
* Home Page Url
* [HomeUrl description]
* #type {[type]}
*/
var HomeUrl = 'https://stackoverflow.com/'; // Current Page url by-> window.location.href
links.forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
// Make lowercase of urls
var url = link.href.toLowerCase();
var isExternalLink = !url.includes(HomeUrl);
// Check if external or internal
if (isExternalLink) {
if (confirm('it\'s an external link. Are you sure to go?')) {
window.location = link.href;
}
} else {
window.location = link.href;
}
})
})
Internal Link
External Link
This should work for any kind of link on every browser except IE.
// check if link points outside of app - not working in IE
try {
const href = $linkElement.attr('href'),
link = new URL(href, window.location);
if (window.location.host === link.host) {
// same app
} else {
// points outside
}
} catch (e) { // in case IE happens}
Yes, I believe you can retrieve the current domain name with location.href. Another possibility is to create a link element, set the src to / and then retrieving the canonical URL (this will retrieve the base URL if you use one, and not necessarily the domain name).
Also see this post: Get the full URI from the href property of a link
For those interested, I did a ternary version of the if block with a check to see what classes the element has and what class gets attached.
$(document).ready(function () {
$("a").click(function (e) {
var hostname = new RegExp(location.host);
var url = $(this).attr("href");
hostname.test(url) ?
$(this).addClass('local') :
url.slice(0, 1) == "/" && url.slice(-1) == "/" ?
$(this).addClass('localpage') :
url.slice(0, 1) == "#" ?
$(this).addClass('anchor') :
$(this).addClass('external');
var classes = $(this).attr("class");
console.log("Link classes: " + classes);
$(this).hasClass("external") ? googleAnalytics(url) :
$(this).hasClass("anchor") ? console.log("Handle anchor") : console.log("Handle local");
});
});
The google analytics bit can be ignored but this is where you'd probably like to do something with the url now that you know what type of link it is. Just add code inside the ternary block.
If you only want to check 1 type of link then replace the ternaries with an if statement instead.
Edited to add in an issue I came across. Some of my hrefs were "/Courses/" like so. I did a check for a localpage which checks if there is a slash at the start and end of the href. Although just checking for a '/' at the start is probably sufficient.
I use this function for jQuery:
$.fn.isExternal = function() {
var host = window.location.host;
var link = $('<a>', {
href: this.attr('href')
})[0].hostname;
return (link !== host);
};
Usage is: $('a').isExternal();
Example: https://codepen.io/allurewebsolutions/pen/ygJPgV
This doesn't exactly meet the "cannot hardcode my domain" prerequisite of the question, but I found this post searching for a similar solution, and in my case I could hard code my url. My concern was alerting users that they are leaving the site, but not if they are staying on site, including subdomains (example: blog.mysite.com, which would fail in most of these other answers). So here is my solution, which takes some bits from the top voted answers above:
Array.from( document.querySelectorAll( 'a' ) ).forEach( a => {
a.classList.add( a.hostname.includes("mywebsite.com") ? 'local' : 'external' );
});
$("a").on("click", function(event) {
if ($(this).hasClass('local')) {
return;
} else if ($(this).hasClass('external')) {
if (!confirm("You are about leave the <My Website> website.")) {
event.preventDefault();
}
}
});
this works for me:
function strip_scheme( url ) {
return url.replace(/^(?:(?:f|ht)tp(?:s)?\:)?\/\/(www\.)?/g, '');
}
function is_link_external( elem ) {
let domain = strip_scheme( elem.attr('href') );
let host = strip_scheme( window.location.host );
return ! domain.indexOf(host) == 0;
}