mirror of
https://github.com/kiwix/kiwix-js-pwa.git
synced 2025-09-09 20:32:45 -04:00
Port code updates for popovers from Kiwix JS (#607)
This commit is contained in:
parent
9b224bc64c
commit
5ec729de5a
@ -196,6 +196,7 @@ const precacheFiles = [
|
|||||||
'www/js/lib/filecache.js',
|
'www/js/lib/filecache.js',
|
||||||
'www/js/lib/images.js',
|
'www/js/lib/images.js',
|
||||||
'www/js/lib/kiwixServe.js',
|
'www/js/lib/kiwixServe.js',
|
||||||
|
'www/js/lib/popovers.js',
|
||||||
'www/js/lib/settingsStore.js',
|
'www/js/lib/settingsStore.js',
|
||||||
'www/js/lib/transformStyles.js',
|
'www/js/lib/transformStyles.js',
|
||||||
'www/js/lib/transformZimit.js',
|
'www/js/lib/transformZimit.js',
|
||||||
@ -260,6 +261,8 @@ if (typeof chrome !== 'undefined' && chrome.action) {
|
|||||||
// Process install event
|
// Process install event
|
||||||
self.addEventListener('install', function (event) {
|
self.addEventListener('install', function (event) {
|
||||||
console.debug('[SW] Install Event processing');
|
console.debug('[SW] Install Event processing');
|
||||||
|
// DEV: We can't skip waiting because too many params are loaded at an early stage from the old file before the new one can activate...
|
||||||
|
// self.skipWaiting();
|
||||||
// We try to circumvent the browser's cache by adding a header to the Request, and it ensures all files are explicitly versioned
|
// We try to circumvent the browser's cache by adding a header to the Request, and it ensures all files are explicitly versioned
|
||||||
var requests = precacheFiles.map(function (urlPath) {
|
var requests = precacheFiles.map(function (urlPath) {
|
||||||
return new Request(urlPath + '?v' + appVersion, { cache: 'no-cache' });
|
return new Request(urlPath + '?v' + appVersion, { cache: 'no-cache' });
|
||||||
@ -277,11 +280,6 @@ self.addEventListener('install', function (event) {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
// .then(function () {
|
|
||||||
// // DEV: We can't skip waiting because too many params are loaded at an early stage from the old file before the new one can activate...
|
|
||||||
// // console.warn('Attempting to skip waiting...');
|
|
||||||
// // self.skipWaiting();
|
|
||||||
// });
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -419,12 +417,6 @@ self.addEventListener('fetch', function (event) {
|
|||||||
}
|
}
|
||||||
const range = modRequestOrResponse.headers.get('range');
|
const range = modRequestOrResponse.headers.get('range');
|
||||||
return fetchUrlFromZIM(urlObject, range).then(function (response) {
|
return fetchUrlFromZIM(urlObject, range).then(function (response) {
|
||||||
// // DEV: For normal reads, this is now done in app.js, but for libzim, we have to do it here
|
|
||||||
// // Add css or js assets to ASSETS_CACHE (or update their cache entries) unless the URL schema is not supported
|
|
||||||
// if (data && data.origin === 'libzim' && regexpCachedContentTypes.test(response.headers.get('Content-Type')) &&
|
|
||||||
// !regexpExcludedURLSchema.test(event.request.url)) {
|
|
||||||
// event.waitUntil(updateCache(ASSETS_CACHE, rqUrl, response.clone()));
|
|
||||||
// }
|
|
||||||
return cacheAndReturnResponseForAsset(event, response);
|
return cacheAndReturnResponseForAsset(event, response);
|
||||||
}).catch(function (msgPortData) {
|
}).catch(function (msgPortData) {
|
||||||
console.error('Invalid message received from app.js for ' + strippedUrl, msgPortData);
|
console.error('Invalid message received from app.js for ' + strippedUrl, msgPortData);
|
||||||
@ -633,7 +625,7 @@ function zimitResolver (event, rqUrl) {
|
|||||||
} else {
|
} else {
|
||||||
// The loaded ZIM archive is not a Zimit archive, or sw-Zimit is unsupported, so we should just return the request
|
// The loaded ZIM archive is not a Zimit archive, or sw-Zimit is unsupported, so we should just return the request
|
||||||
// If the reqUrl is not the same as event.request.url, we need to modify the request
|
// If the reqUrl is not the same as event.request.url, we need to modify the request
|
||||||
// Note that we can't clone requests with streaming bodies, hence we check for an error and use another method in that cse (see https://developer.mozilla.org/en-US/docs/Web/API/Request/clone)
|
// Note that we can't clone requests with streaming bodies, hence we check for an error and use another method in that case (see https://developer.mozilla.org/en-US/docs/Web/API/Request/clone)
|
||||||
var rtnRequest;
|
var rtnRequest;
|
||||||
try {
|
try {
|
||||||
rtnRequest = new Request(rqUrl, event.request);
|
rtnRequest = new Request(rqUrl, event.request);
|
||||||
@ -767,11 +759,9 @@ function fetchUrlFromZIM (urlObjectOrString, range, expectedHeaders) {
|
|||||||
var httpResponse = new Response(slicedData, responseInit);
|
var httpResponse = new Response(slicedData, responseInit);
|
||||||
|
|
||||||
// Let's send the content back from the ServiceWorker
|
// Let's send the content back from the ServiceWorker
|
||||||
// resolve({ response: httpResponse, data: msgPortEvent.data });
|
|
||||||
resolve(httpResponse);
|
resolve(httpResponse);
|
||||||
} else if (msgPortEvent.data.action === 'sendRedirect') {
|
} else if (msgPortEvent.data.action === 'sendRedirect') {
|
||||||
console.debug('[SW] Redirecting to ' + msgPortEvent.data.redirectUrl);
|
console.debug('[SW] Redirecting to ' + msgPortEvent.data.redirectUrl);
|
||||||
// resolve({ response: Response.redirect(prefix + msgPortEvent.data.redirectUrl) });
|
|
||||||
resolve(Response.redirect(prefix + msgPortEvent.data.redirectUrl));
|
resolve(Response.redirect(prefix + msgPortEvent.data.redirectUrl));
|
||||||
} else {
|
} else {
|
||||||
reject(msgPortEvent.data, titleWithNameSpace);
|
reject(msgPortEvent.data, titleWithNameSpace);
|
||||||
|
@ -299,7 +299,7 @@ div[style*="background-color: #cee0f2"], div[style*="background-color: #cedff2"]
|
|||||||
border-color: lightgray !important;
|
border-color: lightgray !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
div:not([style*="-color"]), div[style*="background-color"], span:not([class*="color"]),
|
div:not([style*="-color"]):not(.kiwixtooltip), div[style*="background-color"]:not(.kiwixtooltip), span:not([class*="color"]),
|
||||||
summary:not([style*="-color"]), table, tr:not([style*="border-bottom"]), th, td:not([style*="border-bottom"]),
|
summary:not([style*="-color"]), table, tr:not([style*="border-bottom"]), th, td:not([style*="border-bottom"]),
|
||||||
h1, h2, h3, h4, h5, h6, ul, li, input, select, #bodyContent code {
|
h1, h2, h3, h4, h5, h6, ul, li, input, select, #bodyContent code {
|
||||||
border-color: #555 !important;
|
border-color: #555 !important;
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
// import bootstrap from '../css/bootstrap.min.css' assert { type: "css" };
|
// import bootstrap from '../css/bootstrap.min.css' assert { type: "css" };
|
||||||
import zimArchiveLoader from './lib/zimArchiveLoader.js';
|
import zimArchiveLoader from './lib/zimArchiveLoader.js';
|
||||||
import uiUtil from './lib/uiUtil.js';
|
import uiUtil from './lib/uiUtil.js';
|
||||||
|
import popovers from './lib/popovers.js';
|
||||||
import util from './lib/util.js';
|
import util from './lib/util.js';
|
||||||
import utf8 from './lib/utf8.js';
|
import utf8 from './lib/utf8.js';
|
||||||
import cache from './lib/cache.js';
|
import cache from './lib/cache.js';
|
||||||
@ -5231,11 +5232,13 @@ function filterClickEvent (event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Remove any Kiwix Popovers that may be hanging around
|
// Remove any Kiwix Popovers that may be hanging around
|
||||||
uiUtil.removeKiwixPopoverDivs(event.target.ownerDocument);
|
popovers.removeKiwixPopoverDivs(event.target.ownerDocument);
|
||||||
if (params.contentInjectionMode === 'jquery') return;
|
if (params.contentInjectionMode === 'jquery') return;
|
||||||
// Trap clicks in the iframe to restore Fullscreen mode
|
// Trap clicks in the iframe to restore Fullscreen mode
|
||||||
if (params.lockDisplayOrientation) refreshFullScreen(event);
|
if (params.lockDisplayOrientation) refreshFullScreen(event);
|
||||||
if (clickedAnchor) {
|
if (clickedAnchor) {
|
||||||
|
// This prevents any popover from being displayed when the user clicks on a link
|
||||||
|
clickedAnchor.articleisloading = true;
|
||||||
// Check for Zimit links that would normally be handled by the Replay Worker
|
// Check for Zimit links that would normally be handled by the Replay Worker
|
||||||
// DEV: '__WB_pmw' is a function inserted by wombat.js, so this detects links that have been rewritten in zimit2 archives
|
// DEV: '__WB_pmw' is a function inserted by wombat.js, so this detects links that have been rewritten in zimit2 archives
|
||||||
// however, this misses zimit2 archives where the framework doesn't support wombat.js, so monitor if always processing zimit2 links
|
// however, this misses zimit2 archives where the framework doesn't support wombat.js, so monitor if always processing zimit2 links
|
||||||
@ -5329,7 +5332,7 @@ var articleLoadedSW = function (dirEntry, container) {
|
|||||||
if (!appstate.isReplayWorkerAvailable) {
|
if (!appstate.isReplayWorkerAvailable) {
|
||||||
// We need to keep tabs on the opened tabs or windows if the user wants right-click functionality, and also parse download links
|
// We need to keep tabs on the opened tabs or windows if the user wants right-click functionality, and also parse download links
|
||||||
// We need to set a timeout so that dynamically generated URLs are parsed as well (e.g. in Gutenberg ZIMs)
|
// We need to set a timeout so that dynamically generated URLs are parsed as well (e.g. in Gutenberg ZIMs)
|
||||||
if (params.windowOpener && !appstate.pureMode && !params.useLibzim && dirEntry) {
|
if ((params.windowOpener || appstate.wikimediaZimLoaded) && !appstate.pureMode && !params.useLibzim && dirEntry) {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
parseAnchorsJQuery(dirEntry);
|
parseAnchorsJQuery(dirEntry);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
@ -5373,7 +5376,8 @@ var articleLoadedSW = function (dirEntry, container) {
|
|||||||
}
|
}
|
||||||
if (dirEntry) uiUtil.makeReturnLink(dirEntry.getTitleOrUrl());
|
if (dirEntry) uiUtil.makeReturnLink(dirEntry.getTitleOrUrl());
|
||||||
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
|
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
|
||||||
uiUtil.attachKiwixPopoverCss(doc, params.cssTheme === 'darkReader');
|
var darkTheme = (params.cssUITheme == 'auto' ? cssUIThemeGetOrSet('auto', true) : params.cssUITheme) !== 'light';
|
||||||
|
popovers.attachKiwixPopoverCss(doc, darkTheme);
|
||||||
}
|
}
|
||||||
params.isLandingPage = false;
|
params.isLandingPage = false;
|
||||||
} else {
|
} else {
|
||||||
@ -6540,7 +6544,8 @@ function displayArticleContentInContainer (dirEntry, htmlArticle) {
|
|||||||
loadCSSJQuery();
|
loadCSSJQuery();
|
||||||
images.prepareImagesJQuery(articleWindow);
|
images.prepareImagesJQuery(articleWindow);
|
||||||
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
|
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
|
||||||
uiUtil.attachKiwixPopoverCss(articleWindow.document);
|
var darkTheme = (params.cssUITheme == 'auto' ? cssUIThemeGetOrSet('auto', true) : params.cssUITheme) !== 'light';
|
||||||
|
popovers.attachKiwixPopoverCss(articleWindow.document, darkTheme);
|
||||||
}
|
}
|
||||||
var determinedTheme = params.cssTheme === 'auto' ? cssUIThemeGetOrSet('auto') : params.cssTheme;
|
var determinedTheme = params.cssTheme === 'auto' ? cssUIThemeGetOrSet('auto') : params.cssTheme;
|
||||||
if (params.allowHTMLExtraction && appstate.target === 'iframe') {
|
if (params.allowHTMLExtraction && appstate.target === 'iframe') {
|
||||||
@ -6797,6 +6802,7 @@ function loadCSSJQuery () {
|
|||||||
* @param {String} baseUrl The baseUrl against which relative links will be calculated
|
* @param {String} baseUrl The baseUrl against which relative links will be calculated
|
||||||
*/
|
*/
|
||||||
function addListenersToLink (a, href, baseUrl) {
|
function addListenersToLink (a, href, baseUrl) {
|
||||||
|
appstate.baseUrl = baseUrl;
|
||||||
var uriComponent = uiUtil.removeUrlParameters(href);
|
var uriComponent = uiUtil.removeUrlParameters(href);
|
||||||
// var namespace = baseUrl.replace(/^([-ABCIJMUVWX])\/.+/, '$1');
|
// var namespace = baseUrl.replace(/^([-ABCIJMUVWX])\/.+/, '$1');
|
||||||
var loadingContainer = false;
|
var loadingContainer = false;
|
||||||
@ -6828,7 +6834,7 @@ function addListenersToLink (a, href, baseUrl) {
|
|||||||
a.newcontainer = false;
|
a.newcontainer = false;
|
||||||
}
|
}
|
||||||
loadingContainer = false;
|
loadingContainer = false;
|
||||||
a.articleloading = false;
|
a.articleisloading = false;
|
||||||
a.dataset.touchevoked = false;
|
a.dataset.touchevoked = false;
|
||||||
a.popoverisloading = false;
|
a.popoverisloading = false;
|
||||||
};
|
};
|
||||||
@ -6923,7 +6929,7 @@ function addListenersToLink (a, href, baseUrl) {
|
|||||||
if (!a.touched || a.newcontainer || appstate.startVector) return;
|
if (!a.touched || a.newcontainer || appstate.startVector) return;
|
||||||
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
|
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
|
||||||
a.dataset.touchevoked = true;
|
a.dataset.touchevoked = true;
|
||||||
uiUtil.attachKiwixPopoverDiv(event, a, baseUrl, darkTheme);
|
popovers.populateKiwixPopoverDiv(event, a, appstate, darkTheme, appstate.selectedArchive);
|
||||||
} else {
|
} else {
|
||||||
a.newcontainer = true;
|
a.newcontainer = true;
|
||||||
onDetectedClick(event);
|
onDetectedClick(event);
|
||||||
@ -6937,7 +6943,7 @@ function addListenersToLink (a, href, baseUrl) {
|
|||||||
a.newcontainer = false;
|
a.newcontainer = false;
|
||||||
loadingContainer = false;
|
loadingContainer = false;
|
||||||
// Cancel any popovers because user has clicked
|
// Cancel any popovers because user has clicked
|
||||||
a.articleloading = true;
|
a.articleisloading = true;
|
||||||
setTimeout(reset, 1000);
|
setTimeout(reset, 1000);
|
||||||
});
|
});
|
||||||
// This detects right-click in all browsers (only if the option is enabled)
|
// This detects right-click in all browsers (only if the option is enabled)
|
||||||
@ -6952,7 +6958,7 @@ function addListenersToLink (a, href, baseUrl) {
|
|||||||
// return;
|
// return;
|
||||||
} else if (!a.touched) {
|
} else if (!a.touched) {
|
||||||
a.touched = true;
|
a.touched = true;
|
||||||
uiUtil.attachKiwixPopoverDiv(e, a, baseUrl, darkTheme);
|
popovers.populateKiwixPopoverDiv(e, a, appstate, darkTheme, appstate.selectedArchive);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!params.windowOpener) return;
|
if (!params.windowOpener) return;
|
||||||
@ -7001,14 +7007,20 @@ function addListenersToLink (a, href, baseUrl) {
|
|||||||
// The popover feature requires as a minimum that the browser supports the css matches function
|
// The popover feature requires as a minimum that the browser supports the css matches function
|
||||||
// (having this condition prevents very erratic popover placement in IE11, for example, so the feature is disabled)
|
// (having this condition prevents very erratic popover placement in IE11, for example, so the feature is disabled)
|
||||||
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews && 'matches' in Element.prototype) {
|
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews && 'matches' in Element.prototype) {
|
||||||
|
// Prevent accidental selection of the anchor text in some contexts
|
||||||
|
if (a.style.userSelect === undefined && appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
|
||||||
|
// This prevents selection of the text in a touched link in iOS Safari
|
||||||
|
a.style.webkitUserSelect = 'none';
|
||||||
|
a.style.msUserSelect = 'none';
|
||||||
|
}
|
||||||
a.addEventListener('mouseover', function (e) {
|
a.addEventListener('mouseover', function (e) {
|
||||||
// console.debug('a.mouseover');
|
// console.debug('a.mouseover');
|
||||||
if (a.dataset.touchevoked === 'true') return;
|
if (a.dataset.touchevoked === 'true') return;
|
||||||
uiUtil.attachKiwixPopoverDiv(e, a, baseUrl, darkTheme);
|
popovers.populateKiwixPopoverDiv(e, a, appstate, darkTheme, appstate.selectedArchive);
|
||||||
});
|
});
|
||||||
a.addEventListener('mouseout', function (e) {
|
a.addEventListener('mouseout', function (e) {
|
||||||
if (a.dataset.touchevoked === 'true') return;
|
if (a.dataset.touchevoked === 'true') return;
|
||||||
uiUtil.removeKiwixPopoverDivs(e.target.ownerDocument);
|
popovers.removeKiwixPopoverDivs(e.target.ownerDocument);
|
||||||
setTimeout(reset, 1000);
|
setTimeout(reset, 1000);
|
||||||
});
|
});
|
||||||
a.addEventListener('focus', function (e) {
|
a.addEventListener('focus', function (e) {
|
||||||
@ -7016,7 +7028,7 @@ function addListenersToLink (a, href, baseUrl) {
|
|||||||
// console.debug('a.focus');
|
// console.debug('a.focus');
|
||||||
if (a.touched) return;
|
if (a.touched) return;
|
||||||
a.focused = true;
|
a.focused = true;
|
||||||
uiUtil.attachKiwixPopoverDiv(e, a, baseUrl, darkTheme);
|
popovers.populateKiwixPopoverDiv(e, a, appstate, darkTheme, appstate.selectedArchive);
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
a.addEventListener('blur', function (e) {
|
a.addEventListener('blur', function (e) {
|
||||||
@ -7029,7 +7041,7 @@ function addListenersToLink (a, href, baseUrl) {
|
|||||||
a.addEventListener('click', function (e) {
|
a.addEventListener('click', function (e) {
|
||||||
console.log('a.click', e);
|
console.log('a.click', e);
|
||||||
// Cancel any popovers because user has clicked
|
// Cancel any popovers because user has clicked
|
||||||
a.articleloading = true;
|
a.articleisloading = true;
|
||||||
// Prevent opening multiple windows
|
// Prevent opening multiple windows
|
||||||
if (loadingContainer || a.touched) {
|
if (loadingContainer || a.touched) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
486
www/js/lib/popovers.js
Normal file
486
www/js/lib/popovers.js
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
/**
|
||||||
|
* popovers.js : Functions to add popovers to the UI
|
||||||
|
*
|
||||||
|
* Copyright 2013-2024 Jaifroid and contributors
|
||||||
|
* Licence GPL v3:
|
||||||
|
*
|
||||||
|
* This file is part of Kiwix.
|
||||||
|
*
|
||||||
|
* Kiwix is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public Licence as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the Licence, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Kiwix is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public Licence for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public Licence
|
||||||
|
* along with Kiwix (file LICENSE-GPLv3.txt). If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* global params */
|
||||||
|
|
||||||
|
import uiUtil from './uiUtil.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a linked article in a loaded document in order to extract the first main paragraph (the 'lede') and first
|
||||||
|
* main image (if any). This function currently only parses Wikimedia articles. It returns an HTML string, formatted
|
||||||
|
* for display in a popover
|
||||||
|
* @param {String} href The href of the article link from which to extract the lede
|
||||||
|
* @param {String} baseUrl The base URL of the currently loaded article
|
||||||
|
* @param {Document} articleDocument The DOM of the currently loaded article
|
||||||
|
* @param {ZIMArchive} archive The archive from which to extract the lede
|
||||||
|
* @returns {Promise<String>} A Promise for the linked article's lede HTML including first main image URL if any
|
||||||
|
*/
|
||||||
|
function getArticleLede (href, baseUrl, articleDocument, archive) {
|
||||||
|
const uriComponent = uiUtil.removeUrlParameters(href);
|
||||||
|
const zimURL = uiUtil.deriveZimUrlFromRelativeUrl(uriComponent, baseUrl);
|
||||||
|
console.debug('Previewing ' + zimURL);
|
||||||
|
const promiseForArticle = function (dirEntry) {
|
||||||
|
// Wrap legacy callback-based code in a Promise
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// As we're reading Wikipedia articles, we can assume that they are UTF-8 encoded HTML data
|
||||||
|
archive.readUtf8File(dirEntry, function (fileDirEntry, htmlArticle) {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(htmlArticle, 'text/html');
|
||||||
|
const articleBody = doc.body;
|
||||||
|
if (articleBody) {
|
||||||
|
// Establish the popup balloon's base URL and the absolute path for calculating the ZIM URL of links and images
|
||||||
|
const balloonBaseURL = encodeURI(fileDirEntry.namespace + '/' + fileDirEntry.url.replace(/[^/]+$/, ''));
|
||||||
|
const docUrl = new URL(articleDocument.location.href);
|
||||||
|
const rootRelativePathPrefix = docUrl.pathname.replace(/([^.]\.zim\w?\w?\/).+$/i, '$1');
|
||||||
|
// Clean up the lede content
|
||||||
|
const nonEmptyParagraphs = cleanUpLedeContent(articleBody);
|
||||||
|
// Concatenate paragraphs to fill the balloon
|
||||||
|
let balloonString = '';
|
||||||
|
if (nonEmptyParagraphs.length > 0) {
|
||||||
|
balloonString = fillBalloonString(nonEmptyParagraphs, balloonBaseURL, rootRelativePathPrefix);
|
||||||
|
}
|
||||||
|
// If we have a lede, we can now add an image to the balloon, but only if we are in ServiceWorker mode
|
||||||
|
if (balloonString && params.contentInjectionMode === 'serviceworker') {
|
||||||
|
const imageHTML = getImageHTMLFromNode(articleBody, balloonBaseURL, rootRelativePathPrefix);
|
||||||
|
if (imageHTML) {
|
||||||
|
balloonString = imageHTML + balloonString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!balloonString) {
|
||||||
|
reject(new Error('No article lede or image'));
|
||||||
|
} else {
|
||||||
|
resolve(balloonString);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error('No article body found'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const processDirEntry = function (dirEntry) {
|
||||||
|
if (!dirEntry) throw new Error('No directory entry found');
|
||||||
|
if (dirEntry.redirect) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
archive.resolveRedirect(dirEntry, function (reDirEntry) {
|
||||||
|
if (!reDirEntry) reject(new Error('Could not resolve redirect'));
|
||||||
|
resolve(promiseForArticle(reDirEntry));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return promiseForArticle(dirEntry);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Do a binary search in the URL index to get the directory entry for the requested article
|
||||||
|
return archive.getDirEntryByPath(zimURL).then(processDirEntry).catch(function (err) {
|
||||||
|
throw new Error('Could not get Directory Entry for ' + zimURL, err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to clean up the lede content
|
||||||
|
function cleanUpLedeContent (node) {
|
||||||
|
// Remove all standalone style elements from the given DOM node, because their content is shown by innerText and textContent
|
||||||
|
const styleElements = Array.from(node.querySelectorAll('style'));
|
||||||
|
styleElements.forEach(style => {
|
||||||
|
style.parentNode.removeChild(style);
|
||||||
|
});
|
||||||
|
const paragraphs = Array.from(node.querySelectorAll('p'));
|
||||||
|
// Filter out empty paragraphs or those with less than 50 characters
|
||||||
|
const parasWithContent = paragraphs.filter(para => {
|
||||||
|
const text = para.innerText.trim();
|
||||||
|
return !/^\s*$/.test(text) && text.length >= 50;
|
||||||
|
});
|
||||||
|
return parasWithContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to concatenate paragraphs to fill the balloon
|
||||||
|
function fillBalloonString (paras, baseURL, pathPrefix) {
|
||||||
|
let cumulativeCharCount = 0;
|
||||||
|
let concatenatedText = '';
|
||||||
|
// Add enough paras to complete the word count
|
||||||
|
for (let i = 0; i < paras.length; i++) {
|
||||||
|
// Get the character count: to fill the larger box we need ~850 characters (815 plus leeway)
|
||||||
|
const plainText = paras[i].innerText;
|
||||||
|
cumulativeCharCount += plainText.length;
|
||||||
|
// In ServiceWorker mode, we need to transform the URLs of any links in the paragraph
|
||||||
|
if (params.contentInjectionMode === 'serviceworker') {
|
||||||
|
const links = Array.from(paras[i].querySelectorAll('a'));
|
||||||
|
links.forEach(link => {
|
||||||
|
const href = link.getAttribute('href');
|
||||||
|
if (href && !/^#/.test(href)) {
|
||||||
|
const zimURL = uiUtil.deriveZimUrlFromRelativeUrl(href, baseURL);
|
||||||
|
link.href = pathPrefix + encodeURI(zimURL);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Get the transformed HTML. Note that in Safe mode, we risk breaking the UI if user clicks on an
|
||||||
|
// embedded link, so only use innerText in that case
|
||||||
|
const content = params.contentInjectionMode === 'jquery' ? plainText
|
||||||
|
: paras[i].innerHTML;
|
||||||
|
concatenatedText += '<p>' + content + '</p>';
|
||||||
|
// If we have enough characters to fill the box, break
|
||||||
|
if (cumulativeCharCount >= 850) break;
|
||||||
|
}
|
||||||
|
return concatenatedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get the first main image from the given node
|
||||||
|
function getImageHTMLFromNode (node, baseURL, pathPrefix) {
|
||||||
|
const images = node.querySelectorAll('img');
|
||||||
|
let firstImage = null;
|
||||||
|
if (images) {
|
||||||
|
// Iterate over images until we find one with a width greater than 50 pixels
|
||||||
|
// (this filters out small icons)
|
||||||
|
const imageArray = Array.from(images);
|
||||||
|
for (let j = 0; j < imageArray.length; j++) {
|
||||||
|
if (imageArray[j] && imageArray[j].width > 50) {
|
||||||
|
firstImage = imageArray[j];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstImage) {
|
||||||
|
// Calculate root relative URL of image
|
||||||
|
const imageZimURL = encodeURI(uiUtil.deriveZimUrlFromRelativeUrl(firstImage.getAttribute('src'), baseURL));
|
||||||
|
firstImage.src = pathPrefix + imageZimURL;
|
||||||
|
return firstImage.outerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function to attach the tooltip CSS for popovers (NB this does not attach the box itself, only the CSS)
|
||||||
|
* @param {Document} doc The document to which to attach the popover stylesheet
|
||||||
|
* @param {Boolean} dark An optional parameter to adjust the background colour for dark themes (generally not needed for inversion-based themes)
|
||||||
|
*/
|
||||||
|
function attachKiwixPopoverCss (doc, dark) {
|
||||||
|
const colour = dark && !/invert/i.test(params.cssTheme) ? 'darkgray' : 'black';
|
||||||
|
const backgroundColour = dark && !/invert/i.test(params.cssTheme) ? '#111' : '#ebf4fb';
|
||||||
|
const borderColour = dark ? 'darkslategray' : 'skyblue';
|
||||||
|
// DEV: Firefox OS blocks loading stylesheet files into iframe DOM content even if it is same origin, so we are forced to insert a style element instead
|
||||||
|
uiUtil.insertLinkElement(doc, `
|
||||||
|
.kiwixtooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1em;
|
||||||
|
/* prettify */
|
||||||
|
padding: 0 5px 5px;
|
||||||
|
color: ${colour};
|
||||||
|
background: ${backgroundColour};
|
||||||
|
border: 0.1em solid ${borderColour};
|
||||||
|
/* round the corners */
|
||||||
|
border-radius: 0.5em;
|
||||||
|
/* handle overflow */
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
/* handle text wrap */
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
/* add fade-in transition */
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiwixtooltip img {
|
||||||
|
float: right;
|
||||||
|
margin-left: 5px;
|
||||||
|
max-width: 40%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popcloseicon {
|
||||||
|
padding-top: 1px;
|
||||||
|
padding-right: 2px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popcloseicon:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popbreakouticon {
|
||||||
|
height: 18px;
|
||||||
|
margin-right: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popbreakouticon:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent native iOS popover on Safari if option is enabled */
|
||||||
|
body {
|
||||||
|
-webkit-touch-callout: none !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
// The id of the style element for easy manipulation
|
||||||
|
'kiwixtooltipstylesheet');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches a popover div for the given link to the given document's DOM
|
||||||
|
* @param {Event} ev The event which has fired this popover action
|
||||||
|
* @param {Element} link The link element that is being actioned
|
||||||
|
* @param {Object} state The globlal object defined in app.js that holds the current state of the app
|
||||||
|
* @param {Boolean} dark An optional value to switch colour theme to dark if true
|
||||||
|
* @param {ZIMArchive} archive The archive from which the popover information is extracted
|
||||||
|
* @returns {Promise<div>} A Promise for the attached popover div or undefined if the popover is not attached
|
||||||
|
*/
|
||||||
|
function populateKiwixPopoverDiv (ev, link, state, dark, archive) {
|
||||||
|
// Do not show popover if the user has initiated an article load (set in filterClickEvent)
|
||||||
|
if (link.articleisloading || link.popoverisloading) return Promise.resolve();
|
||||||
|
const linkHref = link.getAttribute('href');
|
||||||
|
// Do not show popover if there is no href or with certain landing pages
|
||||||
|
if (!linkHref || /^wikivoyage/i.test(archive.file.name) &&
|
||||||
|
(state.expectedArticleURLToBeDisplayed === archive.landingPageUrl ||
|
||||||
|
state.expectedArticleURLToBeDisplayed === 'A/Wikivoyage:Offline_reader_Expedition/Home_page')) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
link.popoverisloading = true;
|
||||||
|
// Do not display a popover if one is already showing for the current link
|
||||||
|
const kiwixPopover = ev.target.ownerDocument.querySelector('.kiwixtooltip');
|
||||||
|
// DEV: popoverIsLoading will get reset in app.js after user deselects link
|
||||||
|
if (kiwixPopover && kiwixPopover.dataset.href === linkHref) return Promise.resolve();
|
||||||
|
// console.debug('Attaching popover...');
|
||||||
|
const currentDocument = ev.target.ownerDocument;
|
||||||
|
const articleWindow = currentDocument.defaultView;
|
||||||
|
// Remove any existing popover(s) that the user may not have closed before creating a new one
|
||||||
|
removeKiwixPopoverDivs(currentDocument);
|
||||||
|
// Timeout below ensures that popovers are not loaded if a user is simply moving their mouse around on a page
|
||||||
|
// without hovering. It provides a 600ms pause before app begins the process of binary search and decompression
|
||||||
|
setTimeout(function () {
|
||||||
|
// Check if the user has moved away from the link or has clicked it, and abort display of popover if so
|
||||||
|
if (link.articleisloading || !link.matches(':hover') && !link.touched && currentDocument.activeElement !== link) {
|
||||||
|
// Aborting popover display because user has moved away from link or clicked it
|
||||||
|
link.popoverisloading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Create a new Kiwix popover container
|
||||||
|
const divWithArrow = createNewKiwixPopoverCointainer(articleWindow, link, ev);
|
||||||
|
const div = divWithArrow.div;
|
||||||
|
const span = divWithArrow.span;
|
||||||
|
// Get the article's 'lede' (first main paragraph or two) and the first main image (if any)
|
||||||
|
getArticleLede(linkHref, state.baseUrl, currentDocument, archive).then(function (html) {
|
||||||
|
div.style.justifyContent = '';
|
||||||
|
div.style.alignItems = '';
|
||||||
|
div.style.display = 'block';
|
||||||
|
const breakoutIconFile = window.location.pathname.replace(/\/[^/]*$/, '') + (dark ? '/img/icons/new_window_white.svg' : '/img/icons/new_window_black.svg');
|
||||||
|
const backgroundColour = dark && !/invert/i.test(params.cssTheme) ? 'black' : '#ebf4fb';
|
||||||
|
// DEV: Most style declarations in this div only work properly inline. If added in stylesheet, even with !important, the positioning goes awry
|
||||||
|
// (appears to be a timing issue related to the reservation of space given that the div is inserted dynamically).
|
||||||
|
div.innerHTML = `<div style="position: relative; overflow: hidden; height: ${div.style.height};">
|
||||||
|
<div style="background: ${backgroundColour} !important; opacity: 70%; position: absolute; top: 0; right: 0; display: flex; align-items: center; padding: 0; z-index: 1;">
|
||||||
|
<img id="popbreakouticon" src="${breakoutIconFile}" />
|
||||||
|
<span id="popcloseicon">X</span>
|
||||||
|
</div>
|
||||||
|
<div style="padding-top: 3px">${html}</div>
|
||||||
|
</div>`;
|
||||||
|
// Now it is populated, we can attach the arrow to the div
|
||||||
|
div.appendChild(span);
|
||||||
|
// Programme the icons
|
||||||
|
addEventListenersToPopoverIcons(link, div, currentDocument);
|
||||||
|
setTimeout(function () {
|
||||||
|
div.popoverisloading = false;
|
||||||
|
}, 900);
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.warn(err);
|
||||||
|
// Remove the div
|
||||||
|
div.style.opacity = '0';
|
||||||
|
div.parentElement.removeChild(div);
|
||||||
|
link.dataset.touchevoked = false;
|
||||||
|
link.popoverisloading = false;
|
||||||
|
});
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new empty Kiwix popover container and attach it to the current document appropriately sized and positioned
|
||||||
|
* in relation to the given anchor and available screen width and height. Also returns the arrow span element which can be
|
||||||
|
* attached to the div after the div is populated with content.
|
||||||
|
* @param {Window} win The window of the article DOM
|
||||||
|
* @param {Element} anchor The anchor element that is being actioned
|
||||||
|
* @param {Event} event The event which has fired this popover action
|
||||||
|
* @returns {Object} An object containing the popover div and the arrow span elements
|
||||||
|
*/
|
||||||
|
function createNewKiwixPopoverCointainer (win, anchor, event) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
const linkHref = anchor.getAttribute('href');
|
||||||
|
const currentDocument = win.document;
|
||||||
|
div.popoverisloading = true;
|
||||||
|
const screenWidth = win.innerWidth - 40;
|
||||||
|
const screenHeight = document.documentElement.clientHeight;
|
||||||
|
let margin = 40;
|
||||||
|
let divWidth = 512;
|
||||||
|
if (screenWidth <= divWidth) {
|
||||||
|
divWidth = screenWidth;
|
||||||
|
margin = 10;
|
||||||
|
}
|
||||||
|
// Check if we have restricted screen height
|
||||||
|
const divHeight = screenHeight < 512 ? 160 : 256;
|
||||||
|
div.style.width = divWidth + 'px';
|
||||||
|
div.style.height = divHeight + 'px';
|
||||||
|
div.style.display = 'flex';
|
||||||
|
div.style.justifyContent = 'center';
|
||||||
|
div.style.alignItems = 'center';
|
||||||
|
div.className = 'kiwixtooltip';
|
||||||
|
div.innerHTML = '<p>Loading ...</p>';
|
||||||
|
div.dataset.href = linkHref;
|
||||||
|
// DEV: We need to insert the div into the target document before we can obtain its computed dimensions accurately
|
||||||
|
currentDocument.body.appendChild(div);
|
||||||
|
// Calculate the position of the link that is being hovered
|
||||||
|
const linkRect = anchor.getBoundingClientRect();
|
||||||
|
// Initially position the div 20px above the link
|
||||||
|
let triangleDirection = 'top';
|
||||||
|
const divOffsetHeight = /UWP/.test(params.appType) ? div.offsetHeight * params.relativeFontSize / 100 + 20 : div.offsetHeight + 20;
|
||||||
|
let divRectY = linkRect.top - divOffsetHeight;
|
||||||
|
if (/UWP/.test(params.appType)) divRectY = divRectY * 100 / params.relativeFontSize;
|
||||||
|
let triangleY = divHeight + 6;
|
||||||
|
// If we're less than half margin from the top, move the div below the link
|
||||||
|
if (divRectY < margin / 2) {
|
||||||
|
triangleDirection = 'bottom';
|
||||||
|
divRectY = linkRect.bottom + 20;
|
||||||
|
triangleY = -16;
|
||||||
|
if (/UWP/.test(params.appType)) divRectY = divRectY * 100 / params.relativeFontSize;
|
||||||
|
}
|
||||||
|
// Position it horizontally in relation to the pointer position
|
||||||
|
let divRectX, triangleX;
|
||||||
|
if (event.type === 'touchstart') {
|
||||||
|
divRectX = event.touches[0].clientX - divWidth / 2;
|
||||||
|
triangleX = event.touches[0].clientX - divRectX - 20;
|
||||||
|
} else if (event.type === 'focus') {
|
||||||
|
divRectX = linkRect.left + linkRect.width / 2 - divWidth / 2;
|
||||||
|
triangleX = linkRect.left + linkRect.width / 2 - divRectX - 20;
|
||||||
|
} else {
|
||||||
|
divRectX = event.clientX - divWidth / 2;
|
||||||
|
triangleX = event.clientX - divRectX - 20;
|
||||||
|
}
|
||||||
|
// If right edge of div is greater than margin from the right side of window, shift it to margin
|
||||||
|
if (divRectX + divWidth * params.relativeFontSize / 100 > screenWidth - margin) {
|
||||||
|
triangleX += divRectX;
|
||||||
|
divRectX = screenWidth - divWidth * params.relativeFontSize / 100 - margin;
|
||||||
|
triangleX -= divRectX;
|
||||||
|
}
|
||||||
|
// If we're less than margin to the left, shift it to margin px from left
|
||||||
|
if (divRectX * params.relativeFontSize / 100 < margin) {
|
||||||
|
triangleX += divRectX;
|
||||||
|
divRectX = margin;
|
||||||
|
triangleX -= divRectX;
|
||||||
|
}
|
||||||
|
// Adjust triangleX if necessary
|
||||||
|
if (triangleX < 10) triangleX = 10;
|
||||||
|
if (triangleX > divWidth - 10) triangleX = divWidth - 10;
|
||||||
|
// Adjust positions to take into account the font zoom factor
|
||||||
|
divRectX = divRectX * 100 / params.relativeFontSize;
|
||||||
|
triangleX = triangleX * 100 / params.relativeFontSize;
|
||||||
|
const adjustedScrollY = win.scrollY * 100 / params.relativeFontSize;
|
||||||
|
// Now set the calculated x and y positions
|
||||||
|
div.style.top = divRectY + adjustedScrollY + 'px';
|
||||||
|
div.style.left = divRectX + 'px';
|
||||||
|
div.style.opacity = '1';
|
||||||
|
// Now create the arrow span element. Note that we cannot attach it yet as we need to populate the div first
|
||||||
|
// and doing so will overwrite the innerHTML of the div
|
||||||
|
const triangleColour = getComputedStyle(div).borderBottomColor; // Same as border colour of div (UWP needs specific border colour)
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.style.cssText = `
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-${triangleDirection}: 16px solid ${triangleColour} !important;
|
||||||
|
border-left: 8px solid transparent !important;
|
||||||
|
border-right: 8px solid transparent !important;
|
||||||
|
position: absolute;
|
||||||
|
top: ${triangleY}px;
|
||||||
|
left: ${triangleX}px;
|
||||||
|
`;
|
||||||
|
return { div: div, span: span };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds event listeners to the popover's control icons
|
||||||
|
* @param {Element} anchor The anchor which launched the popover
|
||||||
|
* @param {Element} popover The containing element of the popover (div)
|
||||||
|
* @param {Document} doc The doucment on which to operate
|
||||||
|
*/
|
||||||
|
function addEventListenersToPopoverIcons (anchor, popover, doc) {
|
||||||
|
const breakout = function (e) {
|
||||||
|
// Adding the newcontainer property to the anchor will be cauught by the filterClickEvent function and will open in new tab
|
||||||
|
anchor.newcontainer = true;
|
||||||
|
anchor.click();
|
||||||
|
closePopover(popover);
|
||||||
|
}
|
||||||
|
const closeIcon = doc.getElementById('popcloseicon');
|
||||||
|
const breakoutIcon = doc.getElementById('popbreakouticon');
|
||||||
|
// Register mousedown event (should work in all contexts)
|
||||||
|
closeIcon.addEventListener('mousedown', function () {
|
||||||
|
closePopover(popover);
|
||||||
|
}, true);
|
||||||
|
breakoutIcon.addEventListener('mousedown', breakout, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove any preview popover DIVs found in the given document
|
||||||
|
* @param {Document} doc The document from which to remove any popovers
|
||||||
|
*/
|
||||||
|
function removeKiwixPopoverDivs (doc) {
|
||||||
|
const divs = doc.getElementsByClassName('kiwixtooltip');
|
||||||
|
// Timeout is set to allow for a delay before removing popovers - so user can hover the popover itself to prevent it from closing,
|
||||||
|
// or so that links and buttons in the popover can be clicked
|
||||||
|
setTimeout(function () {
|
||||||
|
// Gather any popover divs (on rare occasions, more than one may be displayed)
|
||||||
|
Array.prototype.slice.call(divs).forEach(function (div) {
|
||||||
|
// Do not remove any popover in process of loading
|
||||||
|
if (div.popoverisloading) return;
|
||||||
|
let timeoutID;
|
||||||
|
const fadeOutDiv = function () {
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
// Do not close any div which is being hovered
|
||||||
|
if (!div.matches(':hover')) {
|
||||||
|
closePopover(div);
|
||||||
|
} else {
|
||||||
|
timeoutID = setTimeout(fadeOutDiv, 250);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
timeoutID = setTimeout(fadeOutDiv, 0);
|
||||||
|
});
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the specified popover div, with fadeout effect, and removes it from the DOM
|
||||||
|
* @param {Element} div The div to close
|
||||||
|
*/
|
||||||
|
function closePopover (div) {
|
||||||
|
div.style.opacity = '0';
|
||||||
|
// Timeout allows the animation to complete before removing the div
|
||||||
|
setTimeout(function () {
|
||||||
|
if (div && div.parentElement) {
|
||||||
|
div.parentElement.removeChild(div);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions and classes exposed by this module
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
attachKiwixPopoverCss: attachKiwixPopoverCss,
|
||||||
|
populateKiwixPopoverDiv: populateKiwixPopoverDiv,
|
||||||
|
removeKiwixPopoverDivs: removeKiwixPopoverDivs
|
||||||
|
};
|
@ -1420,382 +1420,6 @@ function lockDisplayOrientation (val) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a linked article in a loaded document in order to extract the first main paragraph (the 'lede') and first
|
|
||||||
* main image (if any). This function currently only parses Wikimedia articles. It returns an HTML string, formatted
|
|
||||||
* for display in a popover
|
|
||||||
*
|
|
||||||
* @param {String} href The href of the article link from which to extract the lede
|
|
||||||
* @param {String} baseUrl The base URL of the currently loaded article
|
|
||||||
* @param {Document} articleDocument The DOM of the currently loaded article
|
|
||||||
* @returns {Promise<String>} A Promise for the linked article's lede HTML including first main image URL if any
|
|
||||||
*/
|
|
||||||
function getArticleLede (href, baseUrl, articleDocument) {
|
|
||||||
var uriComponent = removeUrlParameters(href);
|
|
||||||
var zimURL = deriveZimUrlFromRelativeUrl(uriComponent, baseUrl);
|
|
||||||
console.debug('Previewing ' + zimURL);
|
|
||||||
return appstate.selectedArchive.getDirEntryByPath(zimURL).then(function (dirEntry) {
|
|
||||||
var readArticle = function (dirEntry) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
appstate.selectedArchive.readUtf8File(dirEntry, function (fileDirEntry, htmlArticle) {
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(htmlArticle, 'text/html');
|
|
||||||
// const articleBody = doc.getElementById('mw-content-text');
|
|
||||||
const articleBody = doc.body;
|
|
||||||
if (articleBody) {
|
|
||||||
let balloonString = '';
|
|
||||||
// Remove all standalone style elements, because their content is shown by both innerText and textContent
|
|
||||||
const styleElements = Array.from(articleBody.querySelectorAll('style'));
|
|
||||||
styleElements.forEach(style => {
|
|
||||||
style.parentNode.removeChild(style);
|
|
||||||
});
|
|
||||||
const paragraphs = Array.from(articleBody.querySelectorAll('p'));
|
|
||||||
// Filter out empty paragraphs or those with less than 50 characters
|
|
||||||
const nonEmptyParagraphs = paragraphs.filter(para => {
|
|
||||||
const text = para.innerText.trim();
|
|
||||||
return !/^\s*$/.test(text) && text.length >= 50;
|
|
||||||
});
|
|
||||||
if (nonEmptyParagraphs.length > 0) {
|
|
||||||
var cumulativeCharCount = 0;
|
|
||||||
// Add enough paras to complete the word count
|
|
||||||
for (let i = 0; i < nonEmptyParagraphs.length; i++) {
|
|
||||||
// Get the character count: to fill the larger box we need ~850 characters (815 plus leeway)
|
|
||||||
var plainText = nonEmptyParagraphs[i].innerText;
|
|
||||||
cumulativeCharCount += plainText.length;
|
|
||||||
// In Restricted mode, we risk breaking the UI if user clicks on an embedded link, so only use innerText
|
|
||||||
var content = params.contentInjectionMode === 'jquery' ? plainText
|
|
||||||
: nonEmptyParagraphs[i].innerHTML;
|
|
||||||
balloonString += '<p>' + content + '</p>';
|
|
||||||
// console.debug('Cumulatve character count: ' + cumulativeCharCount);
|
|
||||||
if (cumulativeCharCount >= 850) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const images = articleBody.querySelectorAll('img');
|
|
||||||
let firstImage = null;
|
|
||||||
if (images && params.contentInjectionMode === 'serviceworker') {
|
|
||||||
// Iterate over images until we find one with a width greater than 50 pixels
|
|
||||||
// (this filters out small icons)
|
|
||||||
const imageArray = Array.from(images);
|
|
||||||
for (let j = 0; j < imageArray.length; j++) {
|
|
||||||
if (imageArray[j] && imageArray[j].width > 50) {
|
|
||||||
firstImage = imageArray[j];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (firstImage) {
|
|
||||||
// Calculate absolute URL of image
|
|
||||||
var balloonBaseURL = encodeURI(fileDirEntry.namespace + '/' + fileDirEntry.url.replace(/[^/]+$/, ''));
|
|
||||||
var imageZimURL = encodeURI(deriveZimUrlFromRelativeUrl(firstImage.getAttribute('src'), balloonBaseURL));
|
|
||||||
var absolutePath = articleDocument.location.href.replace(/([^.]\.zim\w?\w?\/).+$/i, '$1');
|
|
||||||
firstImage.src = absolutePath + imageZimURL;
|
|
||||||
balloonString = firstImage.outerHTML + balloonString;
|
|
||||||
}
|
|
||||||
// console.debug(balloonString);
|
|
||||||
if (!balloonString) {
|
|
||||||
reject(new Error('No article lede or image'));
|
|
||||||
} else {
|
|
||||||
resolve(balloonString);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reject(new Error('No article body found'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (dirEntry.redirect) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
appstate.selectedArchive.resolveRedirect(dirEntry, function (reDirEntry) {
|
|
||||||
resolve(readArticle(reDirEntry));
|
|
||||||
});
|
|
||||||
}).catch(error => {
|
|
||||||
return Promise.reject(error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(readArticle(dirEntry));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function to attach the tooltip CSS for popovers (NB this does not attach the box itself, only the CSS)
|
|
||||||
* @param {Document} doc The document to which to attach the blloon.css styelesheet
|
|
||||||
* @param {Boolean} dark An optional parameter to adjust the background colour for dark themes
|
|
||||||
*/
|
|
||||||
function attachKiwixPopoverCss (doc, dark) {
|
|
||||||
const colour = dark ? '#darkgray' : '#black';
|
|
||||||
const backgroundColour = dark ? '#111' : '#ebf4fb';
|
|
||||||
insertLinkElement(doc, `
|
|
||||||
.kiwixtooltip {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1em;
|
|
||||||
/* prettify */
|
|
||||||
padding: 0 5px 5px;
|
|
||||||
color: ${colour};
|
|
||||||
background: ${backgroundColour};
|
|
||||||
border: 0.1em solid #b7ddf2;
|
|
||||||
/* round the corners */
|
|
||||||
border-radius: 0.5em;
|
|
||||||
/* handle overflow */
|
|
||||||
overflow: visible;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
/* handle text wrap */
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
/* add fade-in transition */
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kiwixtooltip img {
|
|
||||||
float: right;
|
|
||||||
margin-left: 5px;
|
|
||||||
max-width: 40%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#popcloseicon {
|
|
||||||
padding-top: 1px;
|
|
||||||
padding-right: 2px;
|
|
||||||
font-size: 20px;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
#popcloseicon:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#popbreakouticon {
|
|
||||||
height: 18px;
|
|
||||||
margin-right: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#popbreakouticon:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}`,
|
|
||||||
// The id of the style element for easy manipulation
|
|
||||||
'kiwixtooltipstylesheet'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attaches a popover div for the given link to the given document's DOM
|
|
||||||
* @param {Event} ev The event which has fired this popover action
|
|
||||||
* @param {Element} link The link element that is being actioned
|
|
||||||
* @param {String} articleBaseUrl The base URL of the currently loaded document
|
|
||||||
* @param {Boolean} dark An optional value to switch colour theme to dark if true
|
|
||||||
*/
|
|
||||||
function attachKiwixPopoverDiv (ev, link, articleBaseUrl, dark) {
|
|
||||||
// Do not show popover if the user has initiated an article load
|
|
||||||
if (link.articleloading || link.popoverisloading) return;
|
|
||||||
var linkHref = link.getAttribute('href');
|
|
||||||
// Do not show popover if there is no href or with certain landing pages
|
|
||||||
if (!linkHref || /^wikivoyage/i.test(appstate.selectedArchive.file.name) &&
|
|
||||||
(appstate.expectedArticleURLToBeDisplayed === appstate.selectedArchive.landingPageUrl ||
|
|
||||||
appstate.expectedArticleURLToBeDisplayed === 'A/Wikivoyage:Offline_reader_Expedition/Home_page')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
link.popoverisloading = true;
|
|
||||||
// Do not disply a popover if one is already showing for the current link
|
|
||||||
var kiwixPopover = ev.target.ownerDocument.querySelector('.kiwixtooltip');
|
|
||||||
if (kiwixPopover && kiwixPopover.dataset.href === linkHref) return;
|
|
||||||
// console.debug('Attaching popover...');
|
|
||||||
var currentDocument = ev.target.ownerDocument;
|
|
||||||
var articleWindow = currentDocument.defaultView;
|
|
||||||
removeKiwixPopoverDivs(currentDocument);
|
|
||||||
setTimeout(function () {
|
|
||||||
// Do not show popover if the user has initiated an article load
|
|
||||||
if (link.articleloading) return;
|
|
||||||
// Check if the link is still being hovered over, and abort display of popover if not
|
|
||||||
if (!link.matches(':hover') && currentDocument.activeElement !== link) {
|
|
||||||
link.popoverisloading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var div = document.createElement('div');
|
|
||||||
div.popoverisloading = true; // console.debug('div.popoverisloading', div.popoverisloading);
|
|
||||||
var screenWidth = articleWindow.innerWidth - 40;
|
|
||||||
var screenHeight = document.documentElement.clientHeight;
|
|
||||||
var margin = 40;
|
|
||||||
var divWidth = 512;
|
|
||||||
if (screenWidth <= divWidth) {
|
|
||||||
divWidth = screenWidth;
|
|
||||||
margin = 10;
|
|
||||||
}
|
|
||||||
// Check if we have restricted screen height
|
|
||||||
var divHeight = screenHeight < 512 ? 160 : 256;
|
|
||||||
div.style.width = divWidth + 'px';
|
|
||||||
div.style.height = divHeight + 'px';
|
|
||||||
div.style.display = 'flex';
|
|
||||||
div.style.justifyContent = 'center';
|
|
||||||
div.style.alignItems = 'center';
|
|
||||||
div.className = 'kiwixtooltip';
|
|
||||||
div.innerHTML = '<p>Loading ...</p>';
|
|
||||||
div.dataset.href = linkHref;
|
|
||||||
currentDocument.body.appendChild(div);
|
|
||||||
// Calculate the position of the link that is being hovered
|
|
||||||
var linkRect = link.getBoundingClientRect();
|
|
||||||
// Initially position the div 20px above the link
|
|
||||||
var triangleDirection = 'top';
|
|
||||||
var divOffsetHeight = /UWP/.test(params.appType) ? div.offsetHeight * params.relativeFontSize / 100 + 20 : div.offsetHeight + 20;
|
|
||||||
var divRectY = linkRect.top - divOffsetHeight;
|
|
||||||
if (/UWP/.test(params.appType)) divRectY = divRectY * 100 / params.relativeFontSize;
|
|
||||||
var triangleY = divHeight + 6;
|
|
||||||
// If we're less than half margin from the top, move the div below the link
|
|
||||||
if (divRectY < margin / 2) {
|
|
||||||
triangleDirection = 'bottom';
|
|
||||||
divRectY = linkRect.bottom + 20;
|
|
||||||
triangleY = -16;
|
|
||||||
if (/UWP/.test(params.appType)) divRectY = divRectY * 100 / params.relativeFontSize;
|
|
||||||
}
|
|
||||||
// Position it horizontally in relation to the pointer position
|
|
||||||
var divRectX, triangleX;
|
|
||||||
if (ev.type === 'touchstart') {
|
|
||||||
divRectX = ev.touches[0].clientX - divWidth / 2;
|
|
||||||
triangleX = ev.touches[0].clientX - divRectX - 20;
|
|
||||||
} else if (ev.type === 'focus') {
|
|
||||||
divRectX = linkRect.left + linkRect.width / 2 - divWidth / 2;
|
|
||||||
triangleX = linkRect.left + linkRect.width / 2 - divRectX - 20;
|
|
||||||
} else {
|
|
||||||
divRectX = ev.clientX - divWidth / 2;
|
|
||||||
triangleX = ev.clientX - divRectX - 20;
|
|
||||||
}
|
|
||||||
// If right edge of div is greater than margin from the right side of window, shift it to margin
|
|
||||||
if (divRectX + divWidth * params.relativeFontSize / 100 > screenWidth - margin) {
|
|
||||||
triangleX += divRectX;
|
|
||||||
divRectX = screenWidth - divWidth * params.relativeFontSize / 100 - margin;
|
|
||||||
triangleX -= divRectX;
|
|
||||||
}
|
|
||||||
// If we're less than margin to the left, shift it to margin px from left
|
|
||||||
if (divRectX * params.relativeFontSize / 100 < margin) {
|
|
||||||
triangleX += divRectX;
|
|
||||||
divRectX = margin;
|
|
||||||
triangleX -= divRectX;
|
|
||||||
}
|
|
||||||
// Adjust triangleX if necessary
|
|
||||||
if (triangleX < 10) triangleX = 10;
|
|
||||||
if (triangleX > divWidth - 10) triangleX = divWidth - 10;
|
|
||||||
// Adjust positions to take into account the font zoom factor
|
|
||||||
divRectX = divRectX * 100 / params.relativeFontSize;
|
|
||||||
triangleX = triangleX * 100 / params.relativeFontSize;
|
|
||||||
var adjustedScrollY = articleWindow.scrollY * 100 / params.relativeFontSize;
|
|
||||||
// Now set the calculated x and y positions, taking into account the zoom factor
|
|
||||||
div.style.top = divRectY + adjustedScrollY + 'px';
|
|
||||||
div.style.left = divRectX + 'px';
|
|
||||||
div.style.opacity = '1';
|
|
||||||
getArticleLede(linkHref, articleBaseUrl, currentDocument).then(function (html) {
|
|
||||||
link.articleloading = false;
|
|
||||||
div.style.justifyContent = '';
|
|
||||||
div.style.alignItems = '';
|
|
||||||
div.style.display = 'block';
|
|
||||||
var breakoutIconFile = window.location.pathname.replace(/\/[^/]*$/, '') + (dark ? '/img/icons/new_window_white.svg' : '/img/icons/new_window_black.svg');
|
|
||||||
var backgroundColour = dark ? '#222' : '#ebf4fb';
|
|
||||||
div.innerHTML = `<div style="position: relative; overflow: hidden; height: ${divHeight}px;">
|
|
||||||
<div style="background: ${backgroundColour} !important; opacity: 70%; position: absolute; top: 0; right: 0; display: flex; align-items: center; padding: 0;">
|
|
||||||
<img id="popbreakouticon" src="${breakoutIconFile}" />
|
|
||||||
<span id="popcloseicon">X</span>
|
|
||||||
</div>
|
|
||||||
<div style="padding-top: 3px">${html}</div>
|
|
||||||
</div>`;
|
|
||||||
// Now insert the arrow
|
|
||||||
var tooltipStyle = articleWindow.document.getElementById('kiwixtooltipstylesheet');
|
|
||||||
var triangleColour = '#b7ddf2'; // Same as border colour of div
|
|
||||||
if (tooltipStyle) {
|
|
||||||
var span = document.createElement('span');
|
|
||||||
span.style.cssText = `
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-${triangleDirection}: 16px solid ${triangleColour};
|
|
||||||
border-left: 8px solid transparent !important;
|
|
||||||
border-right: 8px solid transparent !important;
|
|
||||||
position: absolute;
|
|
||||||
top: ${triangleY}px;
|
|
||||||
left: ${triangleX}px;
|
|
||||||
`;
|
|
||||||
div.appendChild(span);
|
|
||||||
}
|
|
||||||
// Programme the icons
|
|
||||||
addEventListenersToPopoverIcons(link, div, currentDocument);
|
|
||||||
setTimeout(function () {
|
|
||||||
div.popoverisloading = false; // console.debug('div.popoverisloading', div.popoverisloading);
|
|
||||||
}, 900);
|
|
||||||
}).catch(function (err) {
|
|
||||||
console.warn(err);
|
|
||||||
// Remove the div
|
|
||||||
div.style.opacity = '0';
|
|
||||||
div.parentElement.removeChild(div);
|
|
||||||
link.articleloading = false;
|
|
||||||
link.dataset.touchevoked = false;
|
|
||||||
link.popoverisloading = false;
|
|
||||||
});
|
|
||||||
}, 600);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds event listeners to the popover's control icons
|
|
||||||
*
|
|
||||||
* @param {Element} anchor The anchor which launched the popover
|
|
||||||
* @param {Element} popover The containing element of the popover (div)
|
|
||||||
* @param {Document} doc The doucment on which to operate
|
|
||||||
*/
|
|
||||||
function addEventListenersToPopoverIcons (anchor, popover, doc) {
|
|
||||||
var breakout = function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
anchor.newcontainer = true;
|
|
||||||
anchor.click();
|
|
||||||
closePopover(popover);
|
|
||||||
}
|
|
||||||
var closeIcon = doc.getElementById('popcloseicon');
|
|
||||||
var breakoutIcon = doc.getElementById('popbreakouticon');
|
|
||||||
// Register click event for full support
|
|
||||||
closeIcon.addEventListener('mousedown', function () {
|
|
||||||
closePopover(popover);
|
|
||||||
}, true);
|
|
||||||
breakoutIcon.addEventListener('mousedown', breakout, true);
|
|
||||||
// Register either pointerdown or touchstart if supported
|
|
||||||
var eventName = window.PointerEvent ? 'pointerdown' : 'touchstart';
|
|
||||||
closeIcon.addEventListener(eventName, function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
closePopover(popover);
|
|
||||||
}, true);
|
|
||||||
breakoutIcon.addEventListener(eventName, breakout, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove any preview popover DIVs
|
|
||||||
*
|
|
||||||
* @param {Document} doc The document from which to remove any popovers
|
|
||||||
*/
|
|
||||||
function removeKiwixPopoverDivs (doc) {
|
|
||||||
var divs = doc.getElementsByClassName('kiwixtooltip');
|
|
||||||
setTimeout(function () {
|
|
||||||
Array.prototype.slice.call(divs).forEach(function (div) {
|
|
||||||
if (div.popoverisloading) return;
|
|
||||||
var timeoutID;
|
|
||||||
var fadeOutDiv = function () {
|
|
||||||
clearTimeout(timeoutID);
|
|
||||||
if (!div.matches(':hover')) {
|
|
||||||
closePopover(div);
|
|
||||||
} else {
|
|
||||||
timeoutID = setTimeout(fadeOutDiv, 250);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
timeoutID = setTimeout(fadeOutDiv, 0);
|
|
||||||
});
|
|
||||||
}, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Directly close any popovers
|
|
||||||
function closePopover (div) {
|
|
||||||
div.style.opacity = '0';
|
|
||||||
setTimeout(function () {
|
|
||||||
if (div && div.parentElement) {
|
|
||||||
div.parentElement.removeChild(div);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the closest <a> or <area> enclosing tag of an element.
|
* Finds the closest <a> or <area> enclosing tag of an element.
|
||||||
* Returns undefined if there isn't any.
|
* Returns undefined if there isn't any.
|
||||||
@ -1857,9 +1481,6 @@ export default {
|
|||||||
initTouchZoom: initTouchZoom,
|
initTouchZoom: initTouchZoom,
|
||||||
appIsFullScreen: appIsFullScreen,
|
appIsFullScreen: appIsFullScreen,
|
||||||
lockDisplayOrientation: lockDisplayOrientation,
|
lockDisplayOrientation: lockDisplayOrientation,
|
||||||
attachKiwixPopoverCss: attachKiwixPopoverCss,
|
|
||||||
attachKiwixPopoverDiv: attachKiwixPopoverDiv,
|
|
||||||
removeKiwixPopoverDivs: removeKiwixPopoverDivs,
|
|
||||||
reportAssemblerErrorToAPIStatusPanel: reportAssemblerErrorToAPIStatusPanel,
|
reportAssemblerErrorToAPIStatusPanel: reportAssemblerErrorToAPIStatusPanel,
|
||||||
reportSearchProviderToAPIStatusPanel: reportSearchProviderToAPIStatusPanel,
|
reportSearchProviderToAPIStatusPanel: reportSearchProviderToAPIStatusPanel,
|
||||||
warnAndOpenExternalLinkInNewTab: warnAndOpenExternalLinkInNewTab,
|
warnAndOpenExternalLinkInNewTab: warnAndOpenExternalLinkInNewTab,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user