Skip to content

Commit

Permalink
Open external links in new tabs and warn the user
Browse files Browse the repository at this point in the history
Fixes #404

This is a refresh of the work of Jaifroid in branch https://github.com/kiwix/kiwix-js/tree/Check-for-external-links-and-warn
  • Loading branch information
mossroy committed Jun 2, 2022
1 parent a57d622 commit 6bc0b93
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 5 deletions.
7 changes: 7 additions & 0 deletions www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,13 @@ <h3>Display settings</h3>
<strong>Use Home key to focus search bar</strong> (may rarely have side effects on ZIM files that handle Home key)
</label>
</div>
<div class="checkbox">
<label title="Opens the external links outside kiwix-js (avoids some side-effects affecting kiwix-js UI).">
<input type="checkbox" name="openExternalLinksInNewTabs"
id="openExternalLinksInNewTabsCheck" checked>
<strong>Open external links in new tabs</strong>. Disabling this might break kiwix-js UI in some specific cases
</label>
</div>
<div class="form-group">
<label title="Allows selection of themes either for the app only, or for the app and the loaded content.">
<b>Select app theme</b> (content inversion is experimental):
Expand Down
35 changes: 31 additions & 4 deletions www/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
params['appTheme'] = settingsStore.getItem('appTheme') || 'light'; // Currently implemented: light|dark|dark_invert|dark_mwInvert|auto|auto_invert|auto_mwInvert|
// A global parameter to turn on/off the use of Keyboard HOME Key to focus search bar
params['useHomeKeyToFocusSearchBar'] = settingsStore.getItem('useHomeKeyToFocusSearchBar') === 'true';
// A global parameter to turn on/off opening external links in new tab (for ServiceWorker mode)
params['openExternalLinksInNewTabs'] = settingsStore.getItem('openExternalLinksInNewTabs') ? settingsStore.getItem('openExternalLinksInNewTabs') === 'true' : true;
// A parameter to access the URL of any extension that this app was launched from
params['referrerExtensionURL'] = settingsStore.getItem('referrerExtensionURL');
// A parameter to set the content injection mode ('jquery' or 'serviceworker') used by this app
Expand Down Expand Up @@ -162,6 +164,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
document.getElementById('titleSearchRangeVal').textContent = params.maxSearchResultsSize;
document.getElementById('appThemeSelect').value = params.appTheme;
document.getElementById('useHomeKeyToFocusSearchBarCheck').checked = params.useHomeKeyToFocusSearchBar;
document.getElementById('openExternalLinksInNewTabsCheck').checked = params.openExternalLinksInNewTabs;
switchHomeKeyToFocusSearchBar();
document.getElementById('bypassAppCacheCheck').checked = !params.appCache;
document.getElementById('appVersion').innerHTML = 'Kiwix ' + params.appVersion;
Expand Down Expand Up @@ -478,6 +481,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
settingsStore.setItem('useHomeKeyToFocusSearchBar', params.useHomeKeyToFocusSearchBar, Infinity);
switchHomeKeyToFocusSearchBar();
});
$('input:checkbox[name=openExternalLinksInNewTabs]').on('change', function () {
params.openExternalLinksInNewTabs = this.checked ? true : false;
settingsStore.setItem('openExternalLinksInNewTabs', params.openExternalLinksInNewTabs, Infinity);
});
document.getElementById('appThemeSelect').addEventListener('change', function (e) {
params.appTheme = e.target.value;
settingsStore.setItem('appTheme', params.appTheme, Infinity);
Expand Down Expand Up @@ -1456,6 +1463,22 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// Configure home key press to focus #prefix only if the feature is in active state
if (params.useHomeKeyToFocusSearchBar)
iframeArticleContent.contentWindow.addEventListener('keydown', focusPrefixOnHomeKey);
if (params.openExternalLinksInNewTabs) {
// Add event listener to iframe window to check for links to external resources
iframeArticleContent.contentWindow.addEventListener('click', function (event) {
// Find the closest enclosing A tag (if any)
var clickedAnchor = uiUtil.closestAnchorEnclosingElement(event.target);
if (clickedAnchor) {
var href = clickedAnchor.getAttribute('href');
// We assume that, if an absolute http(s) link is hardcoded inside an HTML string,
// it means it's a link to an external website.
// We also do it for ftp even if it's not supported any more by recent browsers...
if (/^(?:http|ftp)/i.test(href)) {
uiUtil.warnAndOpenExternalLinkInNewTab(event, clickedAnchor);
}
}
});
}
// Reset UI when the article is unloaded
iframeArticleContent.contentWindow.onunload = function () {
// remove eventListener to avoid memory leaks
Expand Down Expand Up @@ -1561,7 +1584,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys

// A string to hold any anchor parameter in clicked ZIM URLs (as we must strip these to find the article in the ZIM)
var anchorParameter;

/**
* Display the the given HTML article in the web page,
* and convert links to javascript calls
Expand Down Expand Up @@ -1699,10 +1722,14 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
} else if (anchorTarget) {
// It's a local anchor link : remove escapedUrl if any (see above)
anchor.setAttribute('href', '#' + anchorTarget[1]);
} else if (anchor.protocol !== currentProtocol ||
anchor.host !== currentHost) {
} else if ((anchor.protocol !== currentProtocol ||
anchor.host !== currentHost) && params.openExternalLinksInNewTabs) {
// It's an external URL : we should open it in a new tab
anchor.target = '_blank';
anchor.addEventListener('click', function(event) {
// Find the closest enclosing A tag
var clickedAnchor = uiUtil.closestAnchorEnclosingElement(event.target);
uiUtil.warnAndOpenExternalLinkInNewTab(event, clickedAnchor);
});
} else {
// It's a link to an article or file in the ZIM
var uriComponent = uiUtil.removeUrlParameters(href);
Expand Down
57 changes: 56 additions & 1 deletion www/js/lib/uiUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,59 @@ define(rqDef, function(settingsStore) {

// If global variable webpMachine is true (set in init.js), then we need to initialize the WebP Polyfill
if (webpMachine) webpMachine = new webpHero.WebpMachine({useCanvasElements: true});

/**
* Warn the user that he/she clicked on an external link, and open it in a new tab
*
* @param {Event} event the click event (on an anchor) to handle
* @param {Element} clickedAnchor the DOM anchor that has been clicked (optional, defaults to event.target)
*/
function warnAndOpenExternalLinkInNewTab(event, clickedAnchor) {
event.preventDefault();
event.stopPropagation();
if (!clickedAnchor) clickedAnchor = event.target;
var target = clickedAnchor.target;
var message = '<p>Do you want to open this external link?';
if (!target || target === '_blank') {
message += ' (in a new tab)';
}
message += '</p><p style="word-break:break-all;">' + clickedAnchor.href + '</p>';
systemAlert(message, 'Opening external link', true).then(function (response) {
if (response) {
if (!target)
target = '_blank';
window.open(clickedAnchor.href, target);
}
});
}

/**
* Finds the closest <a> or <area> enclosing tag of an element.
* Returns undefined if there isn't any.
*
* @param {Element} element
* @returns {Element} closest enclosing anchor tag (if any)
*/
function closestAnchorEnclosingElement(element) {
if (Element.prototype.closest) {
// Recent browsers support that natively. See https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
return element.closest('a,area');
} else {
// For other browsers, notably IE, we do that by hand (we did not manage to make polyfills work on IE11)
var currentElement = element;
while (currentElement.tagName !== 'A' && currentElement.tagName !== 'AREA') {
// If there is no parent Element, we did not find any enclosing A tag
if (!currentElement.parentElement) {
return;
} else {
// Else we try the next parent Element
currentElement = currentElement.parentElement;
}
}
// If we reach this line, it means the currentElement is the enclosing Anchor we're looking for
return currentElement;
}
}

/**
* Functions and classes exposed by this module
Expand All @@ -610,6 +663,8 @@ define(rqDef, function(settingsStore) {
removeAnimationClasses: removeAnimationClasses,
applyAnimationToSection: applyAnimationToSection,
applyAppTheme: applyAppTheme,
reportAssemblerErrorToAPIStatusPanel: reportAssemblerErrorToAPIStatusPanel
reportAssemblerErrorToAPIStatusPanel: reportAssemblerErrorToAPIStatusPanel,
warnAndOpenExternalLinkInNewTab: warnAndOpenExternalLinkInNewTab,
closestAnchorEnclosingElement: closestAnchorEnclosingElement
};
});

0 comments on commit 6bc0b93

Please sign in to comment.