Hover Zoom Revamped

Basic userscript implementation of the Imagus Hoverzoom extension

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         Hover Zoom Revamped
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Basic userscript implementation of the Imagus Hoverzoom extension
// @author       TetteDev
// @match        *://*/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @license      MIT
// @run-at       document-start
// @noframes
// ==/UserScript==

const HoverImageId = `hover_image_${makeid(6)}`;
const HoverAttribute = "data-hoverable";
const NodeIsImage = (node) => { return node.tagName === "IMG" || node.nodeName === "IMG"; };

let mouseX = 0;
let mouseClientX = 0;
let mouseY = 0;
let mouseClientY = 0;
let renderAtMousePosition = false;
if (renderAtMousePosition) {
    const fnUpdateMousePosition = (e) => {
        mouseX = e.pageX;
        mouseClientX = e.clientX;
        mouseY = e.pageY;
        mouseClientY = e.clientY;
    };
    document.addEventListener('mousemove', fnUpdateMousePosition, false);
};

function containsImageDimensions(url) {
    const dimensionRegex = /(\d{2,4})x(\d{2,4})/;
    const match = url.match(dimensionRegex);

    if (match) {
        const width = parseInt(match[1], 10);
        const height = parseInt(match[2], 10);

        if (width > 0 && height > 0 && width <= 10000 && height <= 10000) {
            return {
                width: width,
                height: height
            };
        }
    }
    return null;
}
function isLazyLoaded(imgElement) {
    if (!imgElement || imgElement.tagName.toLowerCase() !== 'img') {
        return false;
    }

    if (imgElement.hasAttribute('loading') && imgElement.getAttribute('loading') === 'lazy') {
        return true;
    }

    const lazyAttributes = ['data-src', 'data-lazy-src', 'data-srcset'];
    if (lazyAttributes.some((attr) => imgElement.hasAttribute(attr))) {
        return true;
    }

    const possibleIndicatorsClassnameLazy = ["lazy", "lazyloaded"];
    if (possibleIndicatorsClassnameLazy.some((lazy) => imgElement.className.includes(lazy))) {
        return true;
    }

    // check if src might contain 'lazy' or 'lazyloaded'

    return false;
}
function makeid(length, allowedCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') {
    let result = '';
    const characters = allowedCharacters || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
    const charactersLength = characters.length;
    let counter = 0;
    while (counter < length) {
      result += characters.charAt(Math.floor(Math.random() * charactersLength));
      counter += 1;
    }
    return result;
}

(function() {
    'use strict';

    const fnTryQuerySelector = (node, selector, all = false) => {
        if (!node) return [];
        if (!selector) return [];
        try {
            return all ? node.querySelectorAll(selector) : node.querySelector(selector);
        } catch (err) { return []; }
    };
    const fnTryHasAttribute = (node, attributeName) => {
        if(typeof node === 'object' && node !== null && 'getAttribute' in node && node.hasAttribute(attributeName)) return true;
        else return false;
    };

    window.addEventListener('load', function () {
        document.querySelectorAll("img").forEach((node) => {
            const isImageOrContainsImage = NodeIsImage(node) || (fnTryQuerySelector(node, "img", true).length > 0);
            if (isImageOrContainsImage) {
                if (fnTryHasAttribute(node, HoverAttribute)) return;

                node.setAttribute(HoverAttribute, "true");
                node.addEventListener("mouseenter", OnElementMouseEnter); node.addEventListener("mouseleave", OnElementMouseLeave);
            }
        });
        new MutationObserver((mutationRecords, observer) => {
            mutationRecords.forEach((mutation) => {
                const addedNodes = mutation.addedNodes;
                if (addedNodes.length == 0) return;

                addedNodes.forEach((node) => {
                    if (node.id === HoverImageId) return;
                    if (fnTryHasAttribute(node, HoverAttribute)) return;

                    const isImageOrContainsImage = NodeIsImage(node) || (fnTryQuerySelector(node, "img", true).length > 0);
                    // TODO: if NodeIsImage(node) returned false but isImageOrContainsImage is still true
                    // determine if its worth making it hoverable by checking the child "img" tags in that
                    // element and confirming they meet some kind of requirements, such as
                    // bigger than some size, child images count is only 1 etc etc

                    if (isImageOrContainsImage) {
                        node.setAttribute(HoverAttribute, "true");
                        node.addEventListener("mouseenter", OnElementMouseEnter); node.addEventListener("mouseleave", OnElementMouseLeave);
                    }
                });
            });
        }).observe(document, { attributes: false, childList: true, characterData: false, subtree: true });
    });

    function OnElementMouseEnter(event) {
        OnElementMouseLeave(null);

        const targetImage = NodeIsImage(event.currentTarget) ? event.currentTarget : event.currentTarget.querySelector("img");
        let targetImageSource = "";
        if (isLazyLoaded(targetImage)) {
            let dataSetUrls = targetImage.hasAttribute("data-srcset") ? targetImage.getAttribute("data-srcset").split(',').map((url) => url.trim().split(' ')[0]) : [];
            if (dataSetUrls) {
                targetImageSource = dataSetUrls.pop();
            }
            else {
                targetImageSource = targetImage.getAttribute("data-src");
            }
        }
        else {
            targetImageSource = targetImage.src;
        }

        if (!targetImageSource) {
            targetImageSource = targetImage.getAttribute("data-src") || targetImage.src || null;

            if (!targetImageSource) {
                event.currentTarget.removeEventListener("mouseenter", OnElementMouseEnter);
                event.currentTarget.removeEventListener("mouseleave", OnElementMouseLeave);
                return;
            }
        }
        let isBase64 = targetImageSource.includes("base64");

        let width = "auto";
        let height = "auto";

        let urlDerivedImageDimensions = isBase64 ? null : containsImageDimensions(targetImageSource);
        if (urlDerivedImageDimensions) {
            width = `${urlDerivedImageDimensions.width}px`;
            height = `${urlDerivedImageDimensions.height}px`;
        } else {
            let hasWidthAttribute = targetImage.hasAttribute("width");
            if (hasWidthAttribute) width = `${targetImage.getAttribute("width")}px`;

            let hasHeightAttribute = targetImage.hasAttribute("height");
            if (hasHeightAttribute) height = `${targetImage.getAttribute("height")}px`;
        }

        // Default: render it at the center of the page
        let renderPositionX = "50%";
        let renderPositionY = "50%";
        if (renderAtMousePosition) {
            // Render at mouse
            renderPositionX = `${(mouseX)}px`;
            renderPositionY = `${(mouseY)}px`;

            // Render at mouse with some offset
            /*
            const imageWidth = parseInt(width);
            const imageHeight = parseInt(height);
            renderPositionX = (mouseClientX + (imageWidth / 2));
            if (renderPositionX + imageWidth > window.innerWidth) {
                renderPositionX = window.innerWidth - imageWidth;
            }
            renderPositionY = (mouseClientY + (imageHeight / 2));
            if (renderPositionY + imageHeight > window.innerHeight) {
                renderPositionY = window.innerHeight - imageHeight;
            }
            renderPositionX = `${renderPositionY}px`;
            renderPositionY = `${renderPositionY}px`;
            */


            // Render at mouse but use percentages relative to the page view
            /*
            const xOffsetPercentage = 20;
            const yOffsetPercentage = 25;
            const xPositionPercentage = ((mouseX/window.innerWidth)*100) + xOffsetPercentage;
            const yPositionPercentage = ((mouseY/window.innerHeight)*100) + yOffsetPercentage;

            renderPositionX = `${xPositionPercentage}%`;
            renderPositionY = `${yPositionPercentage}%`;
            */
        }

        const hover = document.createElement("img");
        hover.src = targetImageSource;
        hover.id = HoverImageId;
        hover.style.cssText = `
        margin: 0 auto;
        position: ${(renderAtMousePosition ? 'absolute' : 'fixed')};
        left: ${renderPositionX} !important;
        top: ${renderPositionY} !important;
        transform: translate(-50%, -50%);
        width: ${width} !important;
        height: ${height} !important;
        z-index: 9999;
        border: 1px dashed red;`;

        document.body.appendChild(hover);
    }

    function OnElementMouseLeave(event) {
        let hover = document.querySelector(`#${HoverImageId}`);
        if (hover) hover.remove();
    }
})();