string consists of two parts, the appTheme (theme to apply to the app shell only), and an optional
* contentTheme beginning with an underscore: e.g. 'dark_invert' = 'dark' (appTheme) + '_invert' (contentTheme)
* Current themes are: light, dark, dark_invert, dark_mwInvert but code below is written for extensibility
* For each appTheme (except the default 'light'), a corresponding set of rules must be present in app.css
* For each contentTheme, a stylesheet must be provided in www/css that is named 'kiwixJS' + contentTheme
* A rule may additionally be needed in app.css for full implementation of contentTheme
*
* @param {String} theme The theme to apply (light|dark[_invert|_mwInvert]|auto[_invert|_mwInvert])
*/
function applyAppTheme(theme) {
var darkPreference = window.matchMedia('(prefers-color-scheme:dark)');
// Resolve the app theme from the matchMedia preference (for auto themes) or from the theme string
var appTheme = /^auto/.test(theme) ? darkPreference.matches ? 'dark' : 'light' : theme.replace(/_.*$/, '');
// Get contentTheme from chosen theme
var contentTheme = theme.replace(/^[^_]*/, '');
var htmlEl = document.querySelector('html');
var footer = document.querySelector('footer');
var oldTheme = htmlEl.dataset.theme || '';
var iframe = document.getElementById('articleContent');
var doc = iframe.contentDocument;
var kiwixJSSheet = doc ? doc.getElementById('kiwixJSTheme') || null : null;
var oldAppTheme = oldTheme.replace(/_.*$/, '');
var oldContentTheme = oldTheme.replace(/^[^_]*/, '');
// Remove oldAppTheme and oldContentTheme
if (oldAppTheme) htmlEl.classList.remove(oldAppTheme);
// A missing contentTheme implies _light
footer.classList.remove(oldContentTheme || '_light');
// Apply new appTheme (NB it will not be added twice if it's already there)
if (appTheme) htmlEl.classList.add(appTheme);
// We also add the contentTheme to the footer to avoid dark css rule being applied to footer when content
// is not dark (but we want it applied when the content is dark or inverted)
footer.classList.add(contentTheme || '_light');
// Embed a reference to applied theme, so we can remove it generically in the future
htmlEl.dataset.theme = appTheme + contentTheme;
// Hide any previously displayed help
var oldHelp = document.getElementById(oldContentTheme.replace(/_/, '') + '-help');
if (oldHelp) oldHelp.style.display = 'none';
// Show any specific help for selected contentTheme
var help = document.getElementById(contentTheme.replace(/_/, '') + '-help');
if (help) help.style.display = 'block';
// Remove the contentTheme for auto themes whenever system is in light mode
if (/^auto/.test(theme) && appTheme === 'light') contentTheme = null;
// Hide any previously displayed description for auto themes
var oldDescription = document.getElementById('kiwix-auto-description');
if (oldDescription) oldDescription.style.display = 'none';
// Show description for auto themes
var description = document.getElementById('kiwix-' + theme.replace(/_.*$/, '') + '-description');
if (description) description.style.display = 'block';
// If there is no ContentTheme or we are applying a different ContentTheme, remove any previously applied ContentTheme
if (oldContentTheme && oldContentTheme !== contentTheme) {
iframe.classList.remove(oldContentTheme);
if (kiwixJSSheet) {
kiwixJSSheet.disabled = true;
kiwixJSSheet.parentNode.removeChild(kiwixJSSheet);
}
}
// Apply the requested ContentTheme (if not already attached)
if (contentTheme && (!kiwixJSSheet || !~kiwixJSSheet.href.search('kiwixJS' + contentTheme + '.css'))) {
iframe.classList.add(contentTheme);
// Use an absolute reference because Service Worker needs this (if an article loaded in SW mode is in a ZIM
// subdirectory, then relative links injected into the article will not work as expected)
// Note that location.pathname returns the path plus the filename, but is useful because it removes any query string
var prefix = (window.location.protocol + '//' + window.location.host + window.location.pathname).replace(/\/[^/]*$/, '');
if (doc) {
var link = doc.createElement('link');
link.setAttribute('id', 'kiwixJSTheme');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css');
link.setAttribute('href', prefix + '/css/kiwixJS' + contentTheme + '.css');
doc.head.appendChild(link);
}
}
// If we are in Config and a real document has been loaded already, expose return link so user can see the result of the change
// DEV: The Placeholder string below matches the dummy article.html that is loaded before any articles are loaded
if (document.getElementById('liConfigureNav').classList.contains('active') && doc &&
doc.title !== "Placeholder for injecting an article into the iframe") {
showReturnLink();
}
}
// Displays the return link and handles click event. Called by applyAppTheme()
function showReturnLink() {
var viewArticle = document.getElementById('viewArticle');
viewArticle.style.display = 'block';
viewArticle.addEventListener('click', function(e) {
e.preventDefault();
document.getElementById('liConfigureNav').classList.remove('active');
document.getElementById('liHomeNav').classList.add('active');
removeAnimationClasses();
if (params.showUIAnimations) {
applyAnimationToSection('home');
} else {
document.getElementById('configuration').style.display = 'none';
document.getElementById('articleContent').style.display = 'block';
}
document.getElementById('navigationButtons').style.display = 'inline-flex';
document.getElementById('formArticleSearch').style.display = 'block';
viewArticle.style.display = 'none';
});
}
// Reports an error in loading one of the ASM or WASM machines to the UI API Status Panel
// This can't be done in app.js because the error occurs after the API panel is first displayed
function reportAssemblerErrorToAPIStatusPanel(decoderType, error, assemblerMachineType) {
console.error('Could not instantiate any ' + decoderType + ' decoder!', error);
params.decompressorAPI.assemblerMachineType = assemblerMachineType;
params.decompressorAPI.errorStatus = 'Error loading ' + decoderType + ' decompressor!';
var decompAPI = document.getElementById('decompressorAPIStatus');
decompAPI.textContent = 'Decompressor API: ' + params.decompressorAPI.errorStatus;
decompAPI.className = 'apiBroken';
document.getElementById('apiStatusDiv').className = 'card card-danger';
}
// Reports the search provider to the API Status Panel
function reportSearchProviderToAPIStatusPanel(provider) {
var providerAPI = document.getElementById('searchProviderStatus');
if (providerAPI) {
providerAPI.textContent = 'Search Provider: ' + (/^fulltext/.test(provider) ? 'Title + Xapian [' + provider + ']' :
/^title/.test(provider) ? 'Title only [' + provider + ']' : 'Not initialized');
providerAPI.className = /^fulltext/.test(provider) ? 'apiAvailable' : !/ERROR/.test(provider) ? 'apiUnavailable' : 'apiBroken';
}
}
// 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 = 'Do you want to open this external link?';
if (!target || target === '_blank') {
message += ' (in a new tab)';
}
message += '
' + clickedAnchor.href + '
';
systemAlert(message, 'Opening external link', true).then(function (response) {
if (response) {
if (!target)
target = '_blank';
window.open(clickedAnchor.href, target);
}
});
}
/**
* Finds the closest or 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
*/
return {
systemAlert: systemAlert,
feedNodeWithDataURI: feedNodeWithDataURI,
determineCanvasElementsWorkaround: determineCanvasElementsWorkaround,
replaceCSSLinkWithInlineCSS: replaceCSSLinkWithInlineCSS,
deriveZimUrlFromRelativeUrl: deriveZimUrlFromRelativeUrl,
removeUrlParameters: removeUrlParameters,
displayActiveContentWarning: displayActiveContentWarning,
displayFileDownloadAlert: displayFileDownloadAlert,
checkUpdateStatus: checkUpdateStatus,
checkServerIsAccessible: checkServerIsAccessible,
spinnerDisplay: spinnerDisplay,
isElementInView: isElementInView,
removeAnimationClasses: removeAnimationClasses,
applyAnimationToSection: applyAnimationToSection,
applyAppTheme: applyAppTheme,
reportAssemblerErrorToAPIStatusPanel: reportAssemblerErrorToAPIStatusPanel,
reportSearchProviderToAPIStatusPanel: reportSearchProviderToAPIStatusPanel,
warnAndOpenExternalLinkInNewTab: warnAndOpenExternalLinkInNewTab,
closestAnchorEnclosingElement: closestAnchorEnclosingElement
};
});