Hover Zoom Minus --

image popup: zoom, pan, scroll, pin, scale

目前為 2024-03-16 提交的版本,檢視 最新版本

// ==UserScript==
// @name             Hover Zoom Minus --
// @namespace        Hover Zoom Minus --
// @version          1.0.4
// @description      image popup: zoom, pan, scroll, pin, scale
// @author           Ein, Copilot AI
// @match            *://*/*
// @license          MIT
// @grant            none
// ==/UserScript==

// 1. to use hover over the image(container) to view a popup of the target image
// 2. to zoom in/out use wheel up/down. to reset press the scroll button
// 3. click left mouse to lock popup this will make it move along with the mouse, click again to release (indicated by green border)
// 4. while being locked"Y" the wheel up/down will act as scroll up/down
// 5. double click will lock it on screen preventing it from being hidden
// 6. hover below the image and click blue bar this will make a 3rd mode for wheel bottom, which will scroll next/previous image under an album
// 7. while locked at screen (indicated by red outline) a single click with the blurred background will unblur it, only one popup per time, so the locked popup will prevent other popup to spawn
// 8. double clicking on blurred background will de-spawn popup
// 9. click  on top right corner to scale image
// 10. to turn on/off hover at the bottom of the page

(function() {
    'use strict';

    // Configuration ----------------------------------------------------------

    // Define regexp of web page you want HoverZoomMinus to run with,
    // 1st array value - default status at start of page: '1' for on, '0' for off
    // 2nd array value - spawn position for popup: 'center' for center of screen, '' for cursor position
    // 3rd array value - allowed interval for spawning popup; i.e. when exited on popup but immediately touches an img container thus making it "blink spawn blink spawn", experiment it with the right number

    const siteConfig = {
        'reddit.com*': [1, 'center', '0'],
        '9gag.com*': [1, 'center', '0'],
        'feedly.com*': [1, 'center', '200'],
        '4chan.org*': [1, '', '400'],
        'deviantart.com*': [0, 'center', '300']
    };

    // image container [hover box where popup triggers]
    const imgContainers = `
    /* ------- reddit */ ._3Oa0THmZ3f5iZXAQ0hBJ0k > div, ._35oEP5zLnhKEbj5BlkTBUA, ._1ti9kvv_PMZEF2phzAjsGW > div, ._28TEYBuEdOuE3kN6UyoKMa div, ._3Oa0THmZ3f5iZXAQ0hBJ0k.WjuR4W-BBrvdtABBeKUMx div, ._3m20hIKOhTTeMgPnfMbVNN,
    /* --------- 9gag */ .post-container .post-view > picture,
    /* ------- feedly */ .PinableImageContainer, .entryBody,
    /* -------- 4chan */ div.post div.file a,
    /* --- deviantart */ ._3_LJY, ._2e1g3, ._2SlAD, ._1R2x6
    `;
    // target img
    const imgElements = `
    /* ------- reddit */ ._2_tDEnGMLxpM6uOa2kaDB3, ._1dwExqTGJH2jnA-MYGkEL-, ._2_tDEnGMLxpM6uOa2kaDB3._1XWObl-3b9tPy64oaG6fax,
    /* --------- 9gag */ .post-container .post-view > picture > img,
    /* ------- feedly */ .pinable, .entryBody img,
    /* -------- 4chan */ div.post div.file img:nth-child(1), ._3Oa0THmZ3f5iZXAQ0hBJ0k.WjuR4W-BBrvdtABBeKUMx img, div.post div.file .fileThumb img,
    /* --- deviantart */ ._3_LJY img, ._2e1g3 img, ._2SlAD img, ._1R2x6 img
    `;
    // excluded element
    const nopeElements = `
    /* ------- reddit */ ._2ED-O3JtIcOqp8iIL1G5cg
    `;
    // AlbumSelector take note that it will only load image those that are already on the DOM tree
    // example reddit will not include all until you press tha navigator. so most of the time this will only load a few ones
    // unless you update it via interacting with reddit's navigator button or maybe you could make a special function for "specialElements" so it will load and include all image in the DOM tree
    let albumSelector = [
        /* ------- reddit */ { imgElement: '._1dwExqTGJH2jnA-MYGkEL-', albumElements: '._1apobczT0TzIKMWpza0OhL' },
        /* ------- sample */ { imgElement: 'imgElementSelector2', albumElements: 'albumSelector2' },
    ];

    // specialElements were if targeted, will call it's paired function
    const specialElements = [
        /* -------- 4chan */ { selector: 'div.post div.file .fileThumb img', func: SP1 }
    ];

    function SP1(imageElement) {
        let src = imageElement.getAttribute('src');
        if (src && src.includes('s.jpg')) {
            let newSrc = src.replace('s.jpg', '.jpg');
            imageElement.setAttribute('src', newSrc);
        }
    }

    //-------------------------------------------------------------------------

    // Variables
    const currentHref = window.location.href;
    let enableP, positionP, intervalP, URLmatched;
    Object.keys(siteConfig).some((config) => {
        const regex = new RegExp(config);
        if (currentHref.match(regex)) {
            [enableP, positionP, intervalP] = siteConfig[config];
            URLmatched = true;
            return true;
        }
    });


    // The HoverZoomMinus Function---------------------------------------------
    function HoverZoomMinus() {
        let isshowPopupEnabled = true;
        isshowPopupEnabled = true;

        const style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = `
		.popup-container {
			display: none;
			z-index: 1001;
			cursor: move;
		}
		.popup-image {
			max-height: calc(90vh - 10px);
			display: none;
		}
		.popup-backdrop {
			position: fixed;
			top: 0;
			left: 0;
			width: 100vw;
			height: 100vh;
			display: none;
			z-index: 1000;
		}
		.LockedX {
			height: 40px;
			width: calc(100%);
			background: #0000;
			position: absolute;
			bottom: 0;
			z-index: 9999;
		}
        .scaleTR {
            height: 40px;
            width: 40px;
            background: #0000;
			position: absolute;
			top: 0;
            right: 0;
			z-index: 9999;
        }
        `;
        document.head.appendChild(style);
        const backdrop = document.createElement('div');
        backdrop.className = 'popup-backdrop';
        document.body.appendChild(backdrop);
        const popupContainer = document.createElement('div');
        popupContainer.className = 'popup-container';
        document.body.appendChild(popupContainer);
        const popup = document.createElement('img');
        popup.className = 'popup-image';
        popupContainer.appendChild(popup);
        const LockedX = document.createElement('div');
        LockedX.className = 'LockedX';
        popupContainer.appendChild(LockedX);
        const scaleTR = document.createElement('div');
        scaleTR.className = 'scaleTR';
        popupContainer.appendChild(scaleTR);

        //-------------------------------------------------------------------------

        // Zoom, Pan, Scroll

        let isLockedY = false;
        let isLockedX = false;
        let offsetX, offsetY, rectT, rectH, rectL, rectW;
        let scale = 1;
        const ZOOM_SPEED = 0.005;
        let clickTimeout;
        let popupTimer;


        popupContainer.addEventListener('click', function(event) {
            if (clickTimeout) clearTimeout(clickTimeout);
            clickTimeout = setTimeout(function() {
                if (event.target.matches('.LockedX') || event.target.closest('.LockedX') || event.target.matches('.scaleTR') || event.target.closest('.scaleTR')) {
                    event.stopPropagation();
                } else {
                    isLockedY = !isLockedY;
                    isscaleTR = false;
                    if (isLockedY) {
                        popupContainer.style.outline = ishidePopupEnabled ? '' : '6px solid #ae0001';
                        isLockedX = false;
                        popupContainer.style.borderTop = '';
                        popupContainer.style.borderBottom = '';
                        popupContainer.style.borderLeft = '6px solid #00ff00';
                        popupContainer.style.borderRight = '6px solid #00ff00';
                        let rect = popupContainer.getBoundingClientRect();
                        offsetX = event.clientX - rect.left - (rect.width / 2);
                        offsetY = event.clientY - rect.top - (rect.height / 2);
                    } else {
                        popupContainer.style.border = '';
                    }
                }}, 300);
        });

        // Scale
        let isscaleTR = false;

        scaleTR.addEventListener('click', function(event) {
            ishidePopupEnabled = false;
            isscaleTR = !isscaleTR;
            if (isscaleTR) {
                isLockedY = false;
                let rect = scaleTR.getBoundingClientRect();
                offsetX = event.clientX - rect.left - (rect.width / 2);
                offsetY = event.clientY - rect.top - (rect.height / 2);
                rectT = rect.top;
                rectH = rect.height;
                rectW = rect.width;
                rectL = rect.left;
            } else {
                scaleTR.style.border = '';
            }

        });

        let scaletrY, scaletrX;
        scaleTR.addEventListener('mouseenter', function() {
            scaleTR.style.height = 'calc(100% - 12px)';
            scaleTR.style.width = 'calc(100% - 12px)';
            scaleTR.style.border = '6px solid #0000ff';

        });
        scaleTR.addEventListener('mouseleave', function(event) {
            scaleTR.style.height = '40px';
            scaleTR.style.width = '40px';
            scaleTR.style.borderBottom = '6px solid #0000';
            scaleTR.style.borderLeft = '6px solid #0000';
        });

        document.addEventListener('mousemove', function(event) {
            if (isLockedY) {
                popupContainer.style.left = (event.clientX - offsetX) + 'px';
                popupContainer.style.top = (event.clientY - offsetY) + 'px';
            } else if (isscaleTR) {
                scaletrY = ((rectT + rectH - event.clientY) / (rectH));
                scaletrX = ((event.clientX - rectL) / (rectW));
                popupContainer.style.transform = `translate(-50%, -50%) scale(${scaletrX}, ${scaletrY})`
                scaleTR.style.borderTop = '6px solid #0000ff';
            }
        });

        let imgElementsList = [];
        function ZoomOrScroll(event) {
            event.preventDefault();
            if (isLockedY) {
                let deltaY = event.deltaY * -ZOOM_SPEED;
                let newTop = parseInt(popupContainer.style.top) || 0;
                newTop += deltaY * 100;
                popupContainer.style.top = newTop + 'px';
                offsetY -= deltaY * 100;
            } else if (isLockedX && currentZeroImgElement) {
                let pair = albumSelector.find(pair => currentZeroImgElement.matches(pair.imgElement));
                if (pair) {
                    let ancestorElement = currentZeroImgElement.closest(pair.albumElements);
                    if (ancestorElement) {
                        imgElementsList = Array.from(ancestorElement.querySelectorAll(pair.imgElement));
                        let zeroIndex = imgElementsList.indexOf(currentZeroImgElement);
                        let direction = event.deltaY > 0 ? 1 : -1;
                        let newIndex = (zeroIndex + direction + imgElementsList.length) % imgElementsList.length;
                        currentZeroImgElement = imgElementsList[newIndex];
                        popup.src = currentZeroImgElement.src;
                    }
                }
            } else {
                scale += event.deltaY * -ZOOM_SPEED;
                scale = Math.min(Math.max(0.125, scale), 10);
                popupContainer.style.transform = `translate(-50%, -50%) scale(${scale})`;
            }
        }

        popupContainer.addEventListener('wheel', ZoomOrScroll);

        //-------------------------------------------------------------------------

        // show popup
        function showPopup(src, mouseX, mouseY) {
            if (!isshowPopupEnabled) return;
            popup.src = src;
            popup.style.display = 'block';
            popupContainer.style.display = 'block';
            popupContainer.style.position = 'fixed';
            popupContainer.style.transform = 'translate(-50%, -50%)';
            backdrop.style.display = 'block';
            backdrop.style.zIndex = '999';
            backdrop.style.backdropFilter = 'blur(10px)';

            if (positionP === 'center') {
                popupContainer.style.top = '50%';
                popupContainer.style.left = '50%';
            } else {
                popupContainer.style.top = `${mouseY}px`;
                popupContainer.style.left = `${mouseX}px`;
            }
        }

        let currentZeroImgElement;
        document.addEventListener('mouseover', function(e) {
            if (popupTimer) return;
            let target = e.target.closest(imgContainers);
            if (target.querySelector(nopeElements)) return;
            const imageElement = target.querySelector(imgElements);
            specialElements.forEach(pair => {
                if (imageElement.matches(pair.selector)) {
                    pair.func(imageElement);
                }
            });

            if (imageElement) {
                currentZeroImgElement = imageElement;
                if (intervalP === '') {
                    showPopup(imageElement.src, e.clientX, e.clientY);
                } else {
                    popupTimer = setTimeout(() => {
                        showPopup(imageElement.src, e.clientX, e.clientY);
                        popupTimer = null;
                    }, parseInt(intervalP));
                }
            }
        });
        //-------------------------------------------------------------------------

        // hide popup
        function hidePopup() {
            if (!ishidePopupEnabled) return;
            imgElementsList = [];
            isshowPopupEnabled = true;
            if (popupTimer) {
                clearTimeout(popupTimer);
            }
            document.body.appendChild(backdrop);
            popup.style.display = 'none';
            popupContainer.style.display = 'none';
            popupContainer.style.left = '50%';
            popupContainer.style.top = '50%';
            popupContainer.style.position = 'fixed';
            popupContainer.style.transform = 'translate(-50%, -50%) scale(1)';
            popupContainer.style.border = '';
            popupContainer.style.outline = '';
            backdrop.style.zIndex = '';
            backdrop.style.display = 'none';
            backdrop.style.backdropFilter = '';
            isLockedY = false;
            isLockedX = false;
            scaleTR.style.borderTop = '';
            scaleTR.style.borderRight = '';
            scaleTR.style.boxShadow = '';

        }

        popupContainer.addEventListener('mouseout', function(event) {
            let relatedTarget = event.relatedTarget;
            if (relatedTarget && (popupContainer.contains(relatedTarget) || relatedTarget.matches('.imgContainers') || relatedTarget.closest('.imgContainers'))) {
                return;
            }

            hidePopup();
            if (intervalP !== '') {
                popupTimer = setTimeout(() => {
                    popupTimer = null;
                }, parseInt(intervalP));
            }
        });

        document.addEventListener('keydown', function(event) {
            if (event.key === "Escape") {
                event.preventDefault();
                hidePopup();
            }
        });

        //-------------------------------------------------------------------------

        // lock popup in screen
        let ishidePopupEnabled = true;

        function togglehidePopup(event) {
            ishidePopupEnabled = !ishidePopupEnabled;
            popupContainer.style.outline = ishidePopupEnabled ? '' : '6px solid #ae0001';
        }

        popupContainer.addEventListener('dblclick', function(event) {
            clearTimeout(clickTimeout);
            togglehidePopup();
        });
        backdrop.addEventListener('dblclick', function(event) {
            clearTimeout(clickTimeout);
            ishidePopupEnabled = true;
            hidePopup();
        });


        backdrop.addEventListener('click', function(event) {
            if (clickTimeout) clearTimeout(clickTimeout);
            clickTimeout = setTimeout(function() {
                if (isscaleTR) {
                    isscaleTR = false;
                    scaleTR.style.border = '';
                    scaleTR.style.boxShadow = '';
                } else {
                    backdrop.style.zIndex = '';
                    backdrop.style.display = 'none';
                    backdrop.style.backdropFilter = '';
                    isshowPopupEnabled = false;
                }
            }, 300);
        });


        // Album scrolling
        LockedX.addEventListener('mouseenter', function() {
            LockedX.style.background = 'linear-gradient(to right, rgba(0, 0, 255, 0) 0%, rgba(0, 0, 255, 0.5) 25%, rgba(0, 0, 255, 0.5) 75%, rgba(0, 0, 255, 0) 100%)';
        });

        LockedX.addEventListener('mouseleave', function() {
            LockedX.style.background = '#0000';
        });

        LockedX.addEventListener('click', function(event) {
            ishidePopupEnabled = false;
            isLockedX = !isLockedX;
            if (isLockedX) {
                isLockedY = false;
                popupContainer.style.borderTop = '6px solid #00ff00';
                popupContainer.style.borderBottom = '6px solid #00ff00';
                popupContainer.style.borderLeft = '';
                popupContainer.style.borderRight = '';
                popupContainer.style.outline = '';
            } else {
                popupContainer.style.border = '';
                popupContainer.style.outline = '6px solid #ae0001';
            }

        });
    }

    //-------------------------------------------------------------------------

    // Is to be run -----------------------------------------------------------
    if (URLmatched) {
        const indicatorBar = document.createElement('div');
        indicatorBar.style.cssText = `
        position: fixed;
        bottom: 0;
        left: 50%;
        transform: translateX(-50%);
        z-index: 9999;
        height: 30px;
        width: 50vw;
        background: #0000;`;
        document.body.appendChild(indicatorBar);

        function toggleIndicator() {
            enableP = 1 - enableP;
            indicatorBar.style.background = enableP ? 'linear-gradient(to right, rgba(50, 190, 152, 0) 0%, rgba(50, 190, 152, 0.5) 25%, rgba(50, 190, 152, 0.5) 75%, rgba(50, 190, 152, 0) 100%)' : 'linear-gradient(to right, rgba(174, 0, 1, 0) 0%, rgba(174, 0, 1, 0.5) 25%, rgba(174, 0, 1, 0.5) 75%, rgba(174, 0, 1, 0) 100%)';
            setTimeout(() => {
                indicatorBar.style.background = '#0000';
            }, 1000);
            if (enableP === 1) {
                HoverZoomMinus();
            } else {
                const existingPopup = document.body.querySelector('.popup-container');
                if (existingPopup) document.body.removeChild(existingPopup);
                const existingBackdrop = document.body.querySelector('.popup-backdrop');
                if (existingBackdrop) document.body.removeChild(existingBackdrop);

            }
        }

        let hoverTimeout;
        indicatorBar.addEventListener('mouseenter', () => {
            hoverTimeout = setTimeout(toggleIndicator, 500);
        });
        indicatorBar.addEventListener('mouseleave', () => {
            clearTimeout(hoverTimeout);
            indicatorBar.style.background = '#0000';
        });
        if (enableP === 1) {
            HoverZoomMinus();
        }
    } else {
        return;
    }

})();