mirror of
https://github.com/kiwix/kiwix-js-pwa.git
synced 2025-08-03 19:38:36 -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/images.js',
|
||||
'www/js/lib/kiwixServe.js',
|
||||
'www/js/lib/popovers.js',
|
||||
'www/js/lib/settingsStore.js',
|
||||
'www/js/lib/transformStyles.js',
|
||||
'www/js/lib/transformZimit.js',
|
||||
@ -260,6 +261,8 @@ if (typeof chrome !== 'undefined' && chrome.action) {
|
||||
// Process install event
|
||||
self.addEventListener('install', function (event) {
|
||||
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
|
||||
var requests = precacheFiles.map(function (urlPath) {
|
||||
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');
|
||||
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);
|
||||
}).catch(function (msgPortData) {
|
||||
console.error('Invalid message received from app.js for ' + strippedUrl, msgPortData);
|
||||
@ -633,7 +625,7 @@ function zimitResolver (event, rqUrl) {
|
||||
} else {
|
||||
// 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
|
||||
// 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;
|
||||
try {
|
||||
rtnRequest = new Request(rqUrl, event.request);
|
||||
@ -767,11 +759,9 @@ function fetchUrlFromZIM (urlObjectOrString, range, expectedHeaders) {
|
||||
var httpResponse = new Response(slicedData, responseInit);
|
||||
|
||||
// Let's send the content back from the ServiceWorker
|
||||
// resolve({ response: httpResponse, data: msgPortEvent.data });
|
||||
resolve(httpResponse);
|
||||
} else if (msgPortEvent.data.action === 'sendRedirect') {
|
||||
console.debug('[SW] Redirecting to ' + msgPortEvent.data.redirectUrl);
|
||||
// resolve({ response: Response.redirect(prefix + msgPortEvent.data.redirectUrl) });
|
||||
resolve(Response.redirect(prefix + msgPortEvent.data.redirectUrl));
|
||||
} else {
|
||||
reject(msgPortEvent.data, titleWithNameSpace);
|
||||
|
@ -299,7 +299,7 @@ div[style*="background-color: #cee0f2"], div[style*="background-color: #cedff2"]
|
||||
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"]),
|
||||
h1, h2, h3, h4, h5, h6, ul, li, input, select, #bodyContent code {
|
||||
border-color: #555 !important;
|
||||
|
@ -29,6 +29,7 @@
|
||||
// import bootstrap from '../css/bootstrap.min.css' assert { type: "css" };
|
||||
import zimArchiveLoader from './lib/zimArchiveLoader.js';
|
||||
import uiUtil from './lib/uiUtil.js';
|
||||
import popovers from './lib/popovers.js';
|
||||
import util from './lib/util.js';
|
||||
import utf8 from './lib/utf8.js';
|
||||
import cache from './lib/cache.js';
|
||||
@ -5231,11 +5232,13 @@ function filterClickEvent (event) {
|
||||
return;
|
||||
}
|
||||
// Remove any Kiwix Popovers that may be hanging around
|
||||
uiUtil.removeKiwixPopoverDivs(event.target.ownerDocument);
|
||||
popovers.removeKiwixPopoverDivs(event.target.ownerDocument);
|
||||
if (params.contentInjectionMode === 'jquery') return;
|
||||
// Trap clicks in the iframe to restore Fullscreen mode
|
||||
if (params.lockDisplayOrientation) refreshFullScreen(event);
|
||||
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
|
||||
// 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
|
||||
@ -5329,7 +5332,7 @@ var articleLoadedSW = function (dirEntry, container) {
|
||||
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 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 () {
|
||||
parseAnchorsJQuery(dirEntry);
|
||||
}, 1500);
|
||||
@ -5373,7 +5376,8 @@ var articleLoadedSW = function (dirEntry, container) {
|
||||
}
|
||||
if (dirEntry) uiUtil.makeReturnLink(dirEntry.getTitleOrUrl());
|
||||
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;
|
||||
} else {
|
||||
@ -6540,7 +6544,8 @@ function displayArticleContentInContainer (dirEntry, htmlArticle) {
|
||||
loadCSSJQuery();
|
||||
images.prepareImagesJQuery(articleWindow);
|
||||
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;
|
||||
if (params.allowHTMLExtraction && appstate.target === 'iframe') {
|
||||
@ -6797,6 +6802,7 @@ function loadCSSJQuery () {
|
||||
* @param {String} baseUrl The baseUrl against which relative links will be calculated
|
||||
*/
|
||||
function addListenersToLink (a, href, baseUrl) {
|
||||
appstate.baseUrl = baseUrl;
|
||||
var uriComponent = uiUtil.removeUrlParameters(href);
|
||||
// var namespace = baseUrl.replace(/^([-ABCIJMUVWX])\/.+/, '$1');
|
||||
var loadingContainer = false;
|
||||
@ -6828,7 +6834,7 @@ function addListenersToLink (a, href, baseUrl) {
|
||||
a.newcontainer = false;
|
||||
}
|
||||
loadingContainer = false;
|
||||
a.articleloading = false;
|
||||
a.articleisloading = false;
|
||||
a.dataset.touchevoked = false;
|
||||
a.popoverisloading = false;
|
||||
};
|
||||
@ -6923,7 +6929,7 @@ function addListenersToLink (a, href, baseUrl) {
|
||||
if (!a.touched || a.newcontainer || appstate.startVector) return;
|
||||
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
|
||||
a.dataset.touchevoked = true;
|
||||
uiUtil.attachKiwixPopoverDiv(event, a, baseUrl, darkTheme);
|
||||
popovers.populateKiwixPopoverDiv(event, a, appstate, darkTheme, appstate.selectedArchive);
|
||||
} else {
|
||||
a.newcontainer = true;
|
||||
onDetectedClick(event);
|
||||
@ -6937,7 +6943,7 @@ function addListenersToLink (a, href, baseUrl) {
|
||||
a.newcontainer = false;
|
||||
loadingContainer = false;
|
||||
// Cancel any popovers because user has clicked
|
||||
a.articleloading = true;
|
||||
a.articleisloading = true;
|
||||
setTimeout(reset, 1000);
|
||||
});
|
||||
// This detects right-click in all browsers (only if the option is enabled)
|
||||
@ -6952,7 +6958,7 @@ function addListenersToLink (a, href, baseUrl) {
|
||||
// return;
|
||||
} else if (!a.touched) {
|
||||
a.touched = true;
|
||||
uiUtil.attachKiwixPopoverDiv(e, a, baseUrl, darkTheme);
|
||||
popovers.populateKiwixPopoverDiv(e, a, appstate, darkTheme, appstate.selectedArchive);
|
||||
}
|
||||
} else {
|
||||
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
|
||||
// (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) {
|
||||
// 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) {
|
||||
// console.debug('a.mouseover');
|
||||
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) {
|
||||
if (a.dataset.touchevoked === 'true') return;
|
||||
uiUtil.removeKiwixPopoverDivs(e.target.ownerDocument);
|
||||
popovers.removeKiwixPopoverDivs(e.target.ownerDocument);
|
||||
setTimeout(reset, 1000);
|
||||
});
|
||||
a.addEventListener('focus', function (e) {
|
||||
@ -7016,7 +7028,7 @@ function addListenersToLink (a, href, baseUrl) {
|
||||
// console.debug('a.focus');
|
||||
if (a.touched) return;
|
||||
a.focused = true;
|
||||
uiUtil.attachKiwixPopoverDiv(e, a, baseUrl, darkTheme);
|
||||
popovers.populateKiwixPopoverDiv(e, a, appstate, darkTheme, appstate.selectedArchive);
|
||||
}, 200);
|
||||
});
|
||||
a.addEventListener('blur', function (e) {
|
||||
@ -7029,7 +7041,7 @@ function addListenersToLink (a, href, baseUrl) {
|
||||
a.addEventListener('click', function (e) {
|
||||
console.log('a.click', e);
|
||||
// Cancel any popovers because user has clicked
|
||||
a.articleloading = true;
|
||||
a.articleisloading = true;
|
||||
// Prevent opening multiple windows
|
||||
if (loadingContainer || a.touched) {
|
||||
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.
|
||||
* Returns undefined if there isn't any.
|
||||
@ -1857,9 +1481,6 @@ export default {
|
||||
initTouchZoom: initTouchZoom,
|
||||
appIsFullScreen: appIsFullScreen,
|
||||
lockDisplayOrientation: lockDisplayOrientation,
|
||||
attachKiwixPopoverCss: attachKiwixPopoverCss,
|
||||
attachKiwixPopoverDiv: attachKiwixPopoverDiv,
|
||||
removeKiwixPopoverDivs: removeKiwixPopoverDivs,
|
||||
reportAssemblerErrorToAPIStatusPanel: reportAssemblerErrorToAPIStatusPanel,
|
||||
reportSearchProviderToAPIStatusPanel: reportSearchProviderToAPIStatusPanel,
|
||||
warnAndOpenExternalLinkInNewTab: warnAndOpenExternalLinkInNewTab,
|
||||
|
Loading…
x
Reference in New Issue
Block a user