// ==UserScript== // @name Messenger Modal Photo Zoom, Drag, Under Mouse // @namespace http://tampermonkey.net/ // @version 1.9 // @description Adds zoom support for opened photos on messenger.com // @match https://www.messenger.com/* // @grant none // @downloadURL none // ==/UserScript== (function() { 'use strict'; // Global variables for the current image and its transformation parameters. let currentImage = null; let currentScale = 1; let currentTranslateX = 0; let currentTranslateY = 0; let isDragging = false; let dragStartX = 0, dragStartY = 0; let startTranslateX = 0, startTranslateY = 0; // Container for the fixed global control bar. let controlsContainer = null; const step = 0.1; // Zoom increment step // Function: updateTransform // Applies a CSS transform (translation and scaling) to the current image. // Uses !important to override inline Messenger styles, and sets a high z-index so the image appears on top. function updateTransform() { if (currentImage) { const transformString = `translate(${currentTranslateX}px, ${currentTranslateY}px) scale(${currentScale})`; currentImage.style.setProperty('transform', transformString, 'important'); currentImage.style.setProperty('transform-origin', '0 0', 'important'); // Set a high z-index to ensure the image appears above interfering elements. currentImage.style.setProperty('z-index', '100000', 'important'); } } // Function: createControls // Creates a fixed control bar at the top center with three buttons: // Zoom In ("+"), Reset (magnifier icon), and Zoom Out ("-"). function createControls() { controlsContainer = document.createElement('div'); controlsContainer.style.position = 'fixed'; controlsContainer.style.top = '10px'; controlsContainer.style.left = '50%'; controlsContainer.style.transform = 'translateX(-50%)'; controlsContainer.style.zIndex = '10000'; controlsContainer.style.display = 'flex'; controlsContainer.style.gap = '10px'; controlsContainer.style.background = 'rgba(0, 0, 0, 0.5)'; controlsContainer.style.padding = '5px 10px'; controlsContainer.style.borderRadius = '5px'; // Zoom In Button: increases zoom using the image center as the reference. const zoomInButton = document.createElement('button'); zoomInButton.innerHTML = '+'; zoomInButton.style.fontSize = '18px'; zoomInButton.style.padding = '5px 10px'; zoomInButton.style.cursor = 'pointer'; zoomInButton.style.border = 'none'; zoomInButton.style.background = 'white'; zoomInButton.style.borderRadius = '3px'; zoomInButton.addEventListener('click', function(e) { e.stopPropagation(); if (currentImage) { const rect = currentImage.getBoundingClientRect(); const offsetX = rect.width / 2; // Use image center as reference. const offsetY = rect.height / 2; const zoomFactor = 1 + step; currentTranslateX += (1 - zoomFactor) * offsetX; currentTranslateY += (1 - zoomFactor) * offsetY; currentScale *= zoomFactor; updateTransform(); } }); // Reset Button: resets zoom and translation to default values. const resetButton = document.createElement('button'); resetButton.innerHTML = '🔍'; resetButton.style.fontSize = '18px'; resetButton.style.padding = '5px 10px'; resetButton.style.cursor = 'pointer'; resetButton.style.border = 'none'; resetButton.style.background = 'white'; resetButton.style.borderRadius = '3px'; resetButton.addEventListener('click', function(e) { e.stopPropagation(); currentScale = 1; currentTranslateX = 0; currentTranslateY = 0; updateTransform(); }); // Zoom Out Button: decreases zoom using the image center as the reference. const zoomOutButton = document.createElement('button'); zoomOutButton.innerHTML = '-'; zoomOutButton.style.fontSize = '18px'; zoomOutButton.style.padding = '5px 10px'; zoomOutButton.style.cursor = 'pointer'; zoomOutButton.style.border = 'none'; zoomOutButton.style.background = 'white'; zoomOutButton.style.borderRadius = '3px'; zoomOutButton.addEventListener('click', function(e) { e.stopPropagation(); if (currentImage) { const rect = currentImage.getBoundingClientRect(); const offsetX = rect.width / 2; const offsetY = rect.height / 2; const zoomFactor = 1 - step; currentTranslateX += (1 - zoomFactor) * offsetX; currentTranslateY += (1 - zoomFactor) * offsetY; currentScale *= zoomFactor; updateTransform(); } }); controlsContainer.appendChild(zoomInButton); controlsContainer.appendChild(resetButton); controlsContainer.appendChild(zoomOutButton); document.body.appendChild(controlsContainer); } // Function: removeControls // Removes the global control bar from the document. function removeControls() { if (controlsContainer) { controlsContainer.remove(); controlsContainer = null; } } // Function: isVisible // Checks if an element is visible by determining if it has a nonzero bounding rectangle. function isVisible(el) { return !!(el && (el.offsetWidth || el.offsetHeight || el.getClientRects().length)); } // Function: isModalActive // Determines if a photo modal is active by checking for visible elements with aria-labels // "Next photo" or "Previous photo". This ensures the script applies to modal photos only. function isModalActive() { const next = document.querySelector('[aria-label="Next photo"]'); const prev = document.querySelector('[aria-label="Previous photo"]'); return (next && isVisible(next)) || (prev && isVisible(prev)); } // Function: setCurrentImage // Sets the current image that will receive zoom and drag functionality. // If a new image is detected, resets the transformation parameters. function setCurrentImage(img) { if (!img.src || !img.src.startsWith('blob:')) return; if (!isModalActive()) return; // When switching to a new image, reset transformation parameters. if (currentImage !== img) { currentImage = img; currentScale = 1; currentTranslateX = 0; currentTranslateY = 0; updateTransform(); } // Ensure enhanced events are added only once per image. if (!currentImage.dataset.zoomEnhanced) { currentImage.dataset.zoomEnhanced = "true"; // Mouse wheel zoom: zooms under the mouse pointer and adjusts translation so the pointer remains fixed. currentImage.addEventListener('wheel', function(e) { e.preventDefault(); const rect = currentImage.getBoundingClientRect(); const offsetX = e.clientX - rect.left; // Mouse position relative to the image. const offsetY = e.clientY - rect.top; const zoomFactor = e.deltaY < 0 ? (1 + step) : (1 - step); currentTranslateX += (1 - zoomFactor) * offsetX; currentTranslateY += (1 - zoomFactor) * offsetY; currentScale *= zoomFactor; updateTransform(); }, { passive: false }); // Drag functionality: allows the user to drag the image to reposition it. currentImage.addEventListener('mousedown', function(e) { isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY; startTranslateX = currentTranslateX; startTranslateY = currentTranslateY; e.preventDefault(); }); document.addEventListener('mousemove', function(e) { if (!isDragging) return; const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; currentTranslateX = startTranslateX + dx; currentTranslateY = startTranslateY + dy; updateTransform(); }); document.addEventListener('mouseup', function() { isDragging = false; }); } } // MutationObserver: watches for DOM changes to detect when a modal is active and new images are added. const observer = new MutationObserver(function(mutations) { if (!isModalActive()) { removeControls(); currentImage = null; return; } mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (node.nodeType !== 1) return; if (node.tagName.toLowerCase() === 'img') { setCurrentImage(node); } else { const imgs = node.querySelectorAll('img'); imgs.forEach(function(img) { setCurrentImage(img); }); } }); }); // When a modal is active and a current image exists but the control bar is missing, create the controls. if (isModalActive() && currentImage && !controlsContainer) { createControls(); } }); observer.observe(document.body, { childList: true, subtree: true }); })();