' + content + '
'; + // 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 {PromiseLoading ...
'; + 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 +}; diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js index a838072a..37e0cca9 100644 --- a/www/js/lib/uiUtil.js +++ b/www/js/lib/uiUtil.js @@ -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' + content + '
'; - // 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 = 'Loading ...
'; - 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 = ` `; - // 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 or 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,