NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Github image viewer // @description View readme/issues/PR images in full screen, w/o leaving the page // @author Aziks // @version 1.5 // @homepageURL https://github.com/Aziks0/github-image-viewer // @downloadURL https://github.com/Aziks0/github-image-viewer/raw/release/github-image-viewer.user.js // @license GPL-3.0-or-later // @run-at document-idle // @match https://github.com/*/* // @require https://unpkg.com/feather-icons // @grant none // ==/UserScript== /** * Add a style element to the page * * @param {string} styles CSS styles */ const addStyles = (styles) => { const styleElement = document.createElement('style'); styleElement.appendChild(document.createTextNode(styles)); document.head.appendChild(styleElement); }; /** * Get the README element * * @returns The README element, or null if it doesn't exist */ const getReadmeElement = () => document.getElementById('readme'); /** * Get the discussion element. It's present on issue, pull request and * discussion pages. * * @returns The discussion element, or null if it doesn't exist */ const getDiscussionElement = () => document.querySelector('#discussion_bucket .js-discussion'); const isReadmePage = () => (getReadmeElement() ? true : false); const isDiscussionPage = () => (getDiscussionElement() ? true : false); /** * Get all the image elements from the README * * @returns The image elements contained in the README */ const getReadmeImageElements = () => { const readme = getReadmeElement(); const article = readme.getElementsByTagName('article')[0]; return article.getElementsByTagName('img'); }; /** * Get all the image elements from issue, pull request and discussion pages * * @returns The image elements contained in issue, pull request or discussion page */ const getDiscussionImageElements = () => { const discussion = getDiscussionElement(); return discussion.getElementsByTagName('img'); }; /** * Filter the image elements that are not linked to images * * @param {HTMLImageElement[]} imageElements * * @returns An array containing image elements that are linked to image */ const filterRepoImageElements = (imageElements) => { return imageElements.filter((imageElement) => { const link = imageElement.parentElement.getAttribute('href'); if (!link) return false; return ( link.includes('githubusercontent.com') || link.match('/(blob|raw)/.*\\.(png|jpe?g|gif)(\\?.*)?$') ); }); }; /** * Create a portal element * * @returns A portal element */ const createPortal = () => { const closeOnClick = (event) => { const element = event.target; if (element.id !== 'gip-overlay-container') return; togglePortal(false); }; const background = document.createElement('div'); background.classList.add('gip-overlay-background', 'gip-fixed'); const container = document.createElement('div'); container.classList.add('gip-overlay-container', 'gip-fixed'); container.setAttribute('id', 'gip-overlay-container'); container.addEventListener('click', closeOnClick); const portal = document.createElement('div'); portal.classList.add('gip-portal'); portal.setAttribute('style', 'display: none;'); portal.setAttribute('id', 'gip-portal'); portal.appendChild(background); portal.appendChild(container); return portal; }; /** * Add a portal to the page */ const addPortalToPage = () => { const styles = ` .gip-portal { position: absolute; top: 0; right: 0; left: 0; } .gip-fixed { position: fixed; top: 0; right: 0; bottom: 0; left: 0; } .gip-overlay-background { background-color: rgba(30, 30, 30, .7); z-index: 499; } .gip-overlay-container { display: flex; justify-content: center; align-items: center; z-index: 500; }`; addStyles(styles); const portal = createPortal(); document.body.appendChild(portal); }; /** * Toggle the portal * * @param {boolean} display */ const togglePortal = (display) => { const style = display ? '' : 'display: none'; const portal = document.getElementById('gip-portal'); portal.setAttribute('style', style); }; /** * Add an image element to portal, the previous content of the portal is removed * * @param {string} source The image source URL */ const addImageToPortal = (source) => { const image = document.createElement('img'); image.setAttribute('src', source); image.classList.add('gip-image-viewer'); // Replace content with the new image const overlay = document.getElementById('gip-overlay-container'); overlay.innerHTML = ''; overlay.appendChild(image); }; /** * Get the raw url of an image * * @param {HTMLAnchorElement} anchor * * @returns The raw url of the image */ const getRawImageUrl = (anchor) => { const url = anchor.href; if (url.includes('githubusercontent.com')) return url; // We already have the raw URL // Otherwise the raw URL is contained in the child image element source return anchor.firstElementChild.getAttribute('src'); }; /** * Add a click event listener to all the images from * `imageElements`. When the event is triggered, the image from `imageElements` * is added to the portal and the portal is shown. * * Add a CSS class that display an icon on hover. * * @param {HTMLImageElement[]} imageElements An array containing image elements */ const setImagesEvents = (imageElements) => { /** @param {MouseEvent} event */ const onClick = (event) => { event.preventDefault(); /** @type {HTMLImageElement} */ const image = event.target; const url = getRawImageUrl(image.parentElement); addImageToPortal(url); togglePortal(true); }; imageElements.forEach((imageElement) => { const parent = imageElement.parentElement; parent.classList.add('gip-image-container'); parent.addEventListener('click', onClick); const icon = document.createElement('i'); icon.setAttribute('data-feather', 'maximize-2'); icon.classList.add('gip-image-maximize-icon'); parent.appendChild(icon); }); }; const main = () => { /** @type {HTMLCollectionOf<HTMLImageElement>} */ let unfilteredImageElements; if (isReadmePage()) { unfilteredImageElements = getReadmeImageElements(); } else if (isDiscussionPage()) { unfilteredImageElements = getDiscussionImageElements(); } if (!unfilteredImageElements) return; const imageElements = filterRepoImageElements( Array.from(unfilteredImageElements) ); setImagesEvents(imageElements); feather.replace(); }; const globalStyles = ` .gip-image-viewer { max-width: 90%; max-height: 90%; } .gip-image-container { position: relative; } .gip-image-maximize-icon { display: none; position: absolute; height: 18px; width: 18px; top: -6px; right: 4px; color: white; background-color: #00000070; } .gip-image-container:hover .gip-image-maximize-icon { display: inline-block; } `; addStyles(globalStyles); addPortalToPage(); main(); document.addEventListener('pjax:success', main);