Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open external links in new tabs, and warn the user #862

Merged
merged 1 commit into from
Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
};
});