Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Backport popover code from Kiwix PWA #1252

Merged
merged 70 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
071b947
Add and remove popovers with mouseover
Jaifroid May 19, 2024
d5bafbc
Refactor
Jaifroid May 19, 2024
511395d
Make generic event handler
Jaifroid May 19, 2024
210a36d
Add missing files to precache
Jaifroid May 19, 2024
28f075b
Merge branch 'main' into Backport-popover-code-from-Kiwix-PWA
Jaifroid May 20, 2024
10c7ff0
Fix items from self review
Jaifroid May 20, 2024
a00f38d
Support dark mode
Jaifroid May 20, 2024
b248e3f
Add comment explaining inline styles
Jaifroid May 20, 2024
5fe97eb
Make feature conditional on css matches support
Jaifroid May 20, 2024
1642acd
Enable for restricted mode
Jaifroid May 20, 2024
3b8644e
Clearer event naming
Jaifroid May 20, 2024
e7e1cf9
Ad e2e test for popovers on Ray Charles
Jaifroid May 20, 2024
ac7b6de
Increase wait for popover
Jaifroid May 20, 2024
d9bdb7e
Work around unsupported sendKeys in older Chrome
Jaifroid May 20, 2024
8b6f9b2
Avoid buggy sendKeys
Jaifroid May 20, 2024
21c15e0
Work around Safari failure?
Jaifroid May 20, 2024
5ed4812
Use JS methods to select the popover
Jaifroid May 20, 2024
9b56c5e
Try again and check for text content
Jaifroid May 20, 2024
3bf2cf0
Fix opening new tab
Jaifroid May 20, 2024
926f5ae
Fix issues from self review
Jaifroid May 20, 2024
b31f615
Add UI option to disable
Jaifroid May 20, 2024
4719f8b
Added touch support
Jaifroid May 20, 2024
197dae9
Fix clicking on links inside balloon div
Jaifroid May 21, 2024
005dfd5
Make separate attachPopoverTirggerEvents fn
Jaifroid May 21, 2024
2899be9
Cosmetic from self-review
Jaifroid May 21, 2024
d2cb7b7
Try adding pointerdown support
Jaifroid May 21, 2024
53cd00d
Attempt to support Safari on iOS
Jaifroid May 21, 2024
149685a
Prevent accidental selection of link during long-press
Jaifroid May 21, 2024
2af63fd
Remove user-select none for all except webkit
Jaifroid May 21, 2024
7ba937f
Block popup from showing if user navigated
Jaifroid May 21, 2024
29611b6
Comment out some debug
Jaifroid May 21, 2024
81f441a
Add articleisloading block in Safe Mode
Jaifroid May 21, 2024
c12c192
Make attachKiwixPopoverDiv properly return a Promise
Jaifroid May 21, 2024
a3b9399
Try to prevent selection
Jaifroid May 21, 2024
2bd4b99
Revert "Try to prevent selection"
Jaifroid May 21, 2024
2e7d8ef
Finesse touch detection for Firefox
Jaifroid May 22, 2024
371abee
More finessing for iPad and Edge Legacy
Jaifroid May 22, 2024
de9d86d
Better handle context menu suppression
Jaifroid May 22, 2024
5dd13c0
Try again with edge legacy
Jaifroid May 22, 2024
333756d
Refactor for clarity
Jaifroid May 22, 2024
1d385ea
Fix issues from self review
Jaifroid May 22, 2024
f339108
More accurate function names
Jaifroid May 22, 2024
af76b68
Remove debug and add more comments
Jaifroid May 23, 2024
bd94143
Typos
Jaifroid May 23, 2024
dcc2279
Stupid typo
Jaifroid May 23, 2024
c3747f7
Revert appstate to global variable in app.js only
Jaifroid May 24, 2024
17e3402
Do not use global for popoverThrottle
Jaifroid May 24, 2024
d12c923
Missed baseUrl variable
Jaifroid May 24, 2024
8e2816b
Refactor to simplify
Jaifroid May 24, 2024
c44607d
Refactor for legibility
Jaifroid May 24, 2024
cd574f9
Use Array.from and remove console.debug
Jaifroid May 24, 2024
dd2aed8
Use Array.some() for more concision
Jaifroid May 24, 2024
e0cae4f
Clarify comment
Jaifroid May 24, 2024
0460851
Clarify further comment
Jaifroid May 24, 2024
0b578f0
And again
Jaifroid May 24, 2024
a420012
Correct comment
Jaifroid May 24, 2024
c602e25
Explain cleanup routine
Jaifroid May 24, 2024
9c5289c
Final comment clarification for app.js
Jaifroid May 24, 2024
1ea0908
Merge branch 'Backport-popover-code-from-Kiwix-PWA' of https://github…
Jaifroid May 24, 2024
5634bef
Refactor to separate popovers to new module
Jaifroid May 24, 2024
85f41aa
Add explanatory comment about timeout
Jaifroid May 24, 2024
2713a71
Fix new file missing from SW precache
Jaifroid May 24, 2024
6661fba
First attempt (working) to refactor getArticleLede
Jaifroid May 25, 2024
e9f5373
Further simplification
Jaifroid May 25, 2024
8bd5a23
Break out removeAllStyleElements
Jaifroid May 25, 2024
9bd4f08
Add helper functions
Jaifroid May 25, 2024
93cdb4a
Remove misleading and useless Promise syntax
Jaifroid May 25, 2024
a47ce7a
Deal better with dark theme
Jaifroid May 25, 2024
62e7287
Make border colour variable
Jaifroid May 25, 2024
99363d9
Better border styling and use computedStyles
Jaifroid May 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ const precacheFiles = [
'www/img/icons/kiwix-256.png',
'www/img/icons/kiwix-32.png',
'www/img/icons/kiwix-60.png',
'www/img/icons/new_window_black.svg',
'www/img/icons/new_window_white.svg',
'www/img/spinner.gif',
'www/img/Icon_External_Link.png',
'www/index.html',
Expand Down
8 changes: 8 additions & 0 deletions www/img/icons/new_window_black.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions www/img/icons/new_window_white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 77 additions & 23 deletions www/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
'use strict';

// The global parameters object is defined in init.js
/* global params, webpMachine */
/* global params, appstate, webpMachine */

// import styles from '../css/app.css' assert { type: "css" };
// import bootstrap from '../css/bootstrap.min.css' assert { type: "css" };
Expand All @@ -49,13 +49,6 @@ if (params.abort) {
// DEV: Ensure this matches the name defined in service-worker.js (a check is provided in refreshCacheStatus() below)
const ASSETS_CACHE = 'kiwixjs-assetsCache';

/**
* A global object for storing app state
*
* @type Object
*/
var appstate = {};

/**
* @type ZIMArchive | null
*/
Expand Down Expand Up @@ -105,7 +98,7 @@ const folderSelect = document.getElementById('folderSelect');
const archiveFiles = document.getElementById('archiveFiles');

// Unique identifier of the article expected to be displayed
var expectedArticleURLToBeDisplayed = '';
appstate.expectedArticleURLToBeDisplayed = '';
audiodude marked this conversation as resolved.
Show resolved Hide resolved

// define and store dark preference for matchMedia
var darkPreference = window.matchMedia('(prefers-color-scheme:dark)');
Expand Down Expand Up @@ -1790,6 +1783,7 @@ async function archiveReadyCallback (archive) {
params.originalContentInjectionMode = null;
}
}
appstate.wikimediaZimLoaded = /wikipedia|wikivoyage|mdwiki|wiktionary/i.test(archive.file.name);
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
// Set contentInjectionMode to serviceWorker when opening a new archive in case the user switched to Safe Mode/jquery Mode when opening the previous archive
if (params.contentInjectionMode === 'jquery') {
params.contentInjectionMode = settingsStore.getItem('contentInjectionMode');
Expand Down Expand Up @@ -2019,9 +2013,9 @@ function findDirEntryFromDirEntryIdAndLaunchArticleRead (dirEntryId) {
function isDirEntryExpectedToBeDisplayed (dirEntry) {
var curArticleURL = dirEntry.namespace + '/' + dirEntry.url;

if (expectedArticleURLToBeDisplayed !== curArticleURL) {
if (appstate.expectedArticleURLToBeDisplayed !== curArticleURL) {
console.debug('url of current article :' + curArticleURL + ', does not match the expected url :' +
expectedArticleURLToBeDisplayed);
appstate.expectedArticleURLToBeDisplayed);
return false;
}
return true;
Expand All @@ -2041,7 +2035,9 @@ function readArticle (dirEntry) {
// Reset search prefix to allow users to search the same string again if they want to
appstate.search.prefix = '';
// Only update for expectedArticleURLToBeDisplayed.
expectedArticleURLToBeDisplayed = dirEntry.namespace + '/' + dirEntry.url;
appstate.expectedArticleURLToBeDisplayed = dirEntry.namespace + '/' + dirEntry.url;
// Calculate the current article's ZIM baseUrl to use when processing relative links
appstate.baseUrl = encodeURI(dirEntry.namespace + '/' + dirEntry.url.replace(/[^/]+$/, ''));
// We must remove focus from UI elements in order to deselect whichever one was clicked (in both jQuery and SW modes),
// but we should not do this when opening the landing page (or else one of the Unit Tests fails, at least on Chrome 58)
if (!params.isLandingPage) articleContainer.contentWindow.focus();
Expand Down Expand Up @@ -2227,12 +2223,22 @@ function articleLoadedSW (iframeArticleContent) {
}
resizeIFrame();

if (iframeArticleContent.contentWindow) {
var iframeWindow = iframeArticleContent.contentWindow;
if (iframeWindow) {
// Configure home key press to focus #prefix only if the feature is in active state
if (params.useHomeKeyToFocusSearchBar) { iframeArticleContent.contentWindow.onkeydown = focusPrefixOnHomeKey; }
if (params.useHomeKeyToFocusSearchBar) { iframeWindow.onkeydown = focusPrefixOnHomeKey; }
if (params.openExternalLinksInNewTabs) {
// Add event listener to iframe window to check for links to external resources
iframeArticleContent.contentWindow.onclick = filterClickEvent;
iframeWindow.onclick = filterClickEvent;
}
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
const iframeDoc = iframeWindow.document;
if (!iframeDoc) return;
// Attach the popover CSS to the current article document
uiUtil.attachKiwixPopoverCss(iframeDoc);
// Add event listeners to iframe window to check for links
iframeWindow.addEventListener('mouseover', handleEvent, true);
iframeWindow.addEventListener('focus', handleEvent, true);
}
// If we are in a zimit2 ZIM and params.serviceWorkerLocal is true, and it's a landing page, then we should display a warning
if (!params.hideActiveContentWarning && params.isLandingPage && params.zimType === 'zimit2' && params.serviceWorkerLocal) {
Expand All @@ -2254,6 +2260,50 @@ function articleLoadedSW (iframeArticleContent) {
params.isLandingPage = false;
};

/**
* Event handler for attaching preview popovers
* @param {Event} event The event produced by the mouseover or focus action
*/
function handleEvent (event) {
// Check if the hovered or focused element or its parent is a link (but throttle its activation)
if (window.popoverThrottle) return;
window.popoverThrottle = true;
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
setTimeout(function () {
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
let a = event.target;
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
const iframeDoc = a.ownerDocument;
// console.debug('iframeDoc', iframeDoc);
if (iframeDoc) {
const iframeWindow = iframeDoc.defaultView;
while (a && a !== iframeWindow && a.nodeName !== 'A') {
a = a.parentNode;
}
// If a link was hovered or focused, process it
if (a && a.nodeName === 'A') {
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
// console.debug(`a.${event.type}`, a);
// Check if a popover div is currently being hovered
const divs = iframeDoc.getElementsByClassName('kiwixtooltip');
let divIsHovered = false;
Array.prototype.slice.call(divs).forEach(function (div) {
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
if (div.matches(':hover')) divIsHovered = true;
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
});
// Only add a popover to the link if a current popover is not being hovered (prevents popovers showing for links in a popover)
if (!divIsHovered) {
uiUtil.attachKiwixPopoverDiv(event, a, appstate.baseUrl, null, selectedArchive);
}
const outHandler = function () {
setTimeout(function () {
a.popoverisloading = false;
uiUtil.removeKiwixPopoverDivs(iframeDoc);
a.removeEventListener(event.type === 'mouseover' ? 'mouseout' : 'blur', outHandler);
}, 250);
};
a.addEventListener(event.type === 'mouseover' ? 'mouseout' : 'blur', outHandler);
}
}
window.popoverThrottle = false;
}, 10);
};

// Handles a click on a Zimit link that has been processed by Wombat
function handleClickOnReplayLink (ev, anchor) {
var basePath = window.location.href.replace(/^(.*?\/)www\/.*$/, '$1');
Expand Down Expand Up @@ -2441,6 +2491,10 @@ function handleMessageChannelMessage (event) {
window.timeout = setTimeout(function () {
uiUtil.spinnerDisplay(false);
}, 1000);
if (/\bhtml/i.test(mimetype)) {
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
// Calculate the current article's ZIM baseUrl to use when attaching popovers
appstate.baseUrl = encodeURI(dirEntry.namespace + '/' + dirEntry.url.replace(/[^/]+$/, ''));
}
// Ensure the article onload event gets attached to the right iframe
articleLoader();
}
Expand Down Expand Up @@ -2510,7 +2564,7 @@ function displayArticleContentInIframe (dirEntry, htmlArticle) {

// Calculate the current article's ZIM baseUrl to use when processing relative links
// (duplicated because we sometimes bypass readArticle above)
var baseUrl = encodeURI(dirEntry.namespace + '/' + dirEntry.url.replace(/[^/]+$/, ''))
appstate.baseUrl = encodeURI(dirEntry.namespace + '/' + dirEntry.url.replace(/[^/]+$/, ''))

// Add CSP to prevent external scripts and content - note that any existing CSP can only be hardened, not loosened
htmlArticle = htmlArticle.replace(/(<head\b[^>]*>)\s*/, '$1\n <meta http-equiv="Content-Security-Policy" content="default-src \'self\' data: file: blob: about: chrome-extension: moz-extension: https://browser-extension.kiwix.org https://kiwix.github.io \'unsafe-inline\' \'unsafe-eval\';"></meta>\n ');
Expand Down Expand Up @@ -2574,7 +2628,7 @@ function displayArticleContentInIframe (dirEntry, htmlArticle) {
// DEV: Note that deriveZimUrlFromRelativeUrl produces a *decoded* URL (and incidentally would remove any URI component
// if we had captured it). We therefore re-encode the URI with encodeURI (which does not encode forward slashes) instead
// of encodeURIComponent.
assetZIMUrlEnc = encodeURI(uiUtil.deriveZimUrlFromRelativeUrl(relAssetUrl, baseUrl));
assetZIMUrlEnc = encodeURI(uiUtil.deriveZimUrlFromRelativeUrl(relAssetUrl, appstate.baseUrl));
}
newBlock = blockStart + 'data-kiwixurl' + equals + assetZIMUrlEnc + blockClose;
// Replace any srcset with data-kiwixsrcset
Expand Down Expand Up @@ -2771,14 +2825,14 @@ function displayArticleContentInIframe (dirEntry, htmlArticle) {
// Zimit ZIMs store URLs percent-encoded and with querystring and
// deriveZimUrlFromRelativeUrls strips any querystring and decodes
var zimUrlToTransform = zimUrl;
zimUrl = encodeURI(uiUtil.deriveZimUrlFromRelativeUrl(zimUrlToTransform, baseUrl)) +
zimUrl = encodeURI(uiUtil.deriveZimUrlFromRelativeUrl(zimUrlToTransform, appstate.baseUrl)) +
href.replace(uriComponent, '').replace('#' + anchorParameter, '');
// zimUrlFullEncoding = encodeURI(uiUtil.deriveZimUrlFromRelativeUrl(zimUrlToTransform, baseUrl) +
// zimUrlFullEncoding = encodeURI(uiUtil.deriveZimUrlFromRelativeUrl(zimUrlToTransform, appstate.baseUrl) +
// href.replace(uriComponent, '').replace('#' + anchorParameter, ''));
}
} else {
// It's a relative URL, so we need to calculate the full ZIM URL
zimUrl = uiUtil.deriveZimUrlFromRelativeUrl(uriComponent, baseUrl);
zimUrl = uiUtil.deriveZimUrlFromRelativeUrl(uriComponent, appstate.baseUrl);
}
goToArticle(zimUrl, downloadAttrValue, contentType);
});
Expand All @@ -2789,12 +2843,12 @@ function displayArticleContentInIframe (dirEntry, htmlArticle) {
// Make an array from the images that need to be processed
var images = Array.prototype.slice.call(iframeArticleContent.contentDocument.querySelectorAll('img[data-kiwixurl]'));
// This ensures cancellation of image extraction if the user navigates away from the page before extraction has finished
images.owner = expectedArticleURLToBeDisplayed;
images.owner = appstate.expectedArticleURLToBeDisplayed;
// DEV: This self-invoking function is recursive, calling itself only when an image has been fully processed into a
// blob: or data: URI (or returns an error). This ensures that images are processed sequentially from the top of the
// DOM, making for a better user experience (because images above the fold are extracted first)
(function extractImage () {
if (!images.length || images.busy || images.owner !== expectedArticleURLToBeDisplayed) return;
if (!images.length || images.busy || images.owner !== appstate.expectedArticleURLToBeDisplayed) return;
images.busy = true;
// Extract the image at the top of the images array and remove it from the array
var image = images.shift();
Expand Down Expand Up @@ -2948,7 +3002,7 @@ function displayArticleContentInIframe (dirEntry, htmlArticle) {
Array.prototype.slice.call(iframe.querySelectorAll('video, audio, source, track'))
.forEach(function (mediaSource) {
var source = mediaSource.getAttribute('src');
source = source ? uiUtil.deriveZimUrlFromRelativeUrl(source, baseUrl) : null;
source = source ? uiUtil.deriveZimUrlFromRelativeUrl(source, appstate.baseUrl) : null;
// We have to exempt text tracks from using deriveZimUrlFromRelativeurl due to a bug in Firefox [kiwix-js #496]
source = source || decodeURIComponent(mediaSource.dataset.kiwixurl);
if (!source || !regexpZIMUrlWithNamespace.test(source)) {
Expand Down
12 changes: 11 additions & 1 deletion www/js/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
* @property {string} cacheIDB - Name of the Indexed DB database
* @property {boolean} isFileSystemApiSupported - A boolean indicating whether the FileSystem API is supported.
* @property {boolean} isWebkitDirApiSupported - A boolean indicating whether the Webkit Directory API is supported.
* @property {boolean} useLibzim - A boolean indicating weather to use the libzim to load zim files.
* @property {boolean} useLibzim - A boolean indicating whether to use the libzim to load zim files.
* @property {boolean} showPopoverPreviews - A boolean indicating whether to show previews of ZIM links (currently only for Wikimedia archives)
* @property {"wasm-dev" | 'wasm' | 'asm' | 'asm-dev' | 'default'} libzimMode - A value indicating which libzim mode is selected.
* @property {DecompressorAPI} decompressorAPI

Expand All @@ -74,6 +75,14 @@
*/
var params = {};

/**
* A global object for storing app state
*
* @type Object
*/
// eslint-disable-next-line no-unused-vars
var appstate = {};
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved

/**
* Set parameters from the Settings Store, together with any defaults
* Note that the params global object is declared in init.js so that it is available to modules
Expand Down Expand Up @@ -132,6 +141,7 @@ params['libzimMode'] = getSetting('libzimMode') || 'wasm'; // Sets a value indic
params['useLibzim'] = !!getSetting('useLibzim'); // Sets a value indicating which libzim mode is selected
params['previousZimFileName'] = getSetting('previousZimFileName') || ''; // Sets the name of the last opened zim file
params['reopenLastArchive'] = getSetting('reopenLastArchive') !== false; // Sets a Boolean defaulting to true indicating whether to reopen the last opened zim file if possible
params['showPopoverPreviews'] = getSetting('showPopoverPreviews') !== false; // Sets a Boolean defaulting to true indicating whether to show previews of article contents when hovering a ZIM link

/**
* Apply any override parameters that might be in the querystring.
Expand Down
Loading
Loading