' + 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) ? 'lightgray' : 'black'; const backgroundColour = dark && !/invert/i.test(params.cssTheme) ? '#121e1e' : '#ebf4fb'; const borderColour = 'skyblue !important'; // 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; z-index: 2; } .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 rect = anchor.getBoundingClientRect(); const linkRect = { top: rect.top, bottom: rect.bottom, left: rect.left, right: rect.right, width: rect.width }; // Note that since Chromium 128 getBoundingClientRect() now returns zoom-adjusted values, but if this is the case, // then currentCSSZoom will be defined as well, so we can adjust for this. Note that UWP also requires adjustment. if (/UWP/.test(params.appType) || 'MSBlobBuilder' in window || anchor.currentCSSZoom || isSafari()) { linkRect.top = linkRect.top / zoomFactor; linkRect.bottom = linkRect.bottom / zoomFactor; linkRect.left = linkRect.left / zoomFactor; linkRect.right = linkRect.right / zoomFactor; linkRect.width = linkRect.width / zoomFactor; } // Initially position the div 20px above the link const spacing = 20; let triangleDirection = 'top'; const divOffsetHeight = div.offsetHeight + spacing; let divRectY = linkRect.top - divOffsetHeight; 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 + spacing; triangleY = -16; } // 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 - spacing; } else if (event.type === 'focus') { divRectX = linkRect.left * zoomFactor + linkRect.width / 2 - divWidth / 2; triangleX = linkRect.left * zoomFactor + linkRect.width / 2 - divRectX - spacing; } else { divRectX = event.clientX - divWidth / 2; triangleX = event.clientX - divRectX - spacing; } // If right edge of div is greater than margin from the right side of window, shift it to margin if (divRectX + divWidth * zoomFactor > screenWidth - margin) { triangleX += divRectX; divRectX = screenWidth - divWidth * zoomFactor - margin; triangleX -= divRectX; } // If we're less than margin to the left, shift it to margin px from left if (divRectX * zoomFactor < 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 const adjustedScrollY = win.scrollY / zoomFactor; if (isSafari()) { // We have to reinstate zoomFactor as it is only applied to horizontal positioning in Safari zoomFactor = params.relativeFontSize / 100; } divRectX = divRectX / zoomFactor; triangleX = triangleX / zoomFactor; // 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 }; } function isSafari () { return typeof navigator !== 'undefined' && /^((?!chrome|android).)*safari/i.test(navigator.userAgent) && CSS.supports('-webkit-backdrop-filter', 'blur(1px)'); }; /** * 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 };