Nexus Mods Gallery Preview Hover (Universal)

Show thumbgallery preview images when hovering on a NexusMods main mod link or mod thumbnail, works for any game. Preview popup is fixed near the anchor, mouse can interact with popup.

  1. // ==UserScript==
  2. // @name Nexus Mods Gallery Preview Hover (Universal)
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.5
  5. // @description Show thumbgallery preview images when hovering on a NexusMods main mod link or mod thumbnail, works for any game. Preview popup is fixed near the anchor, mouse can interact with popup.
  6. // @author GPT
  7. // @license MIT
  8. // @match *://*/*
  9. // @grant GM_xmlhttpRequest
  10. // @connect nexusmods.com
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. let previewDiv = null;
  17. let previewTimer = null;
  18. let currentLink = null;
  19. let hoverOnPreview = false;
  20. let hoverOnLink = false;
  21.  
  22. // Only match main mod links (mods/<number>), for any game on NexusMods
  23. function isModMainLink(href) {
  24. // e.g. https://www.nexusmods.com/skyrimspecialedition/mods/12345
  25. // https://www.nexusmods.com/fallout4/mods/6789
  26. return /^https?:\/\/(www\.)?nexusmods\.com\/[^\/]+\/mods\/\d+$/.test(href);
  27. }
  28.  
  29. // Create the preview popup near the anchor (link or image)
  30. function createPreview(images, anchor) {
  31. removePreview();
  32. previewDiv = document.createElement('div');
  33. previewDiv.style.position = 'absolute';
  34. previewDiv.style.zIndex = 99999;
  35. previewDiv.style.background = '#222';
  36. previewDiv.style.padding = '8px';
  37. previewDiv.style.borderRadius = '8px';
  38. previewDiv.style.boxShadow = '0 2px 12px rgba(0,0,0,0.35)';
  39. previewDiv.style.maxWidth = '560px';
  40. previewDiv.style.maxHeight = '420px';
  41. previewDiv.style.overflowY = 'auto';
  42. previewDiv.style.overflowX = 'hidden';
  43. previewDiv.style.display = 'grid';
  44. previewDiv.style.gridTemplateColumns = '1fr 1fr';
  45. previewDiv.style.gap = '8px';
  46.  
  47. // Add all images to the popup grid
  48. for (let img of images) {
  49. let i = document.createElement('img');
  50. i.src = img;
  51. i.style.maxHeight = '180px';
  52. i.style.maxWidth = '260px';
  53. i.style.width = '100%';
  54. i.style.objectFit = 'cover';
  55. i.style.borderRadius = '4px';
  56. previewDiv.appendChild(i);
  57. }
  58.  
  59. document.body.appendChild(previewDiv);
  60.  
  61. // Position the preview below and slightly to the right of the anchor
  62. let rect = anchor.getBoundingClientRect();
  63. let scrollX = window.scrollX || document.documentElement.scrollLeft;
  64. let scrollY = window.scrollY || document.documentElement.scrollTop;
  65. let left = rect.left + scrollX + 8;
  66. let top = rect.bottom + scrollY + 6;
  67. previewDiv.style.left = left + 'px';
  68. previewDiv.style.top = top + 'px';
  69.  
  70. // When mouse enters/leaves the popup
  71. previewDiv.addEventListener('mouseenter', function() {
  72. hoverOnPreview = true;
  73. clearTimeout(previewTimer);
  74. });
  75. previewDiv.addEventListener('mouseleave', function() {
  76. hoverOnPreview = false;
  77. startPreviewTimeout();
  78. });
  79. }
  80.  
  81. // Start delayed removal of preview
  82. function startPreviewTimeout() {
  83. previewTimer = setTimeout(() => { removePreview(); }, 200);
  84. }
  85.  
  86. // Remove the preview popup
  87. function removePreview() {
  88. if (previewDiv && previewDiv.parentNode) {
  89. previewDiv.parentNode.removeChild(previewDiv);
  90. previewDiv = null;
  91. }
  92. currentLink = null;
  93. clearTimeout(previewTimer);
  94. hoverOnPreview = false;
  95. hoverOnLink = false;
  96. }
  97.  
  98. // Fetch thumbgallery images from the mod page
  99. function fetchGallery(link, anchor) {
  100. if (currentLink === link) return; // Prevent duplicate requests
  101. currentLink = link;
  102. GM_xmlhttpRequest({
  103. method: 'GET',
  104. url: link,
  105. onload: function(response) {
  106. let html = response.responseText;
  107. let match = html.match(/<ul class="thumbgallery gallery clearfix"[^>]*>([\s\S]*?)<\/ul>/);
  108. if (match) {
  109. let ul = match[1];
  110. let imgRegex = /<img\s+[^>]*src="([^"]+)"[^>]*>/g;
  111. let images = [];
  112. let m;
  113. while ((m = imgRegex.exec(ul)) !== null) {
  114. images.push(m[1]);
  115. }
  116. if (images.length > 0) {
  117. createPreview(images, anchor);
  118. }
  119. }
  120. }
  121. });
  122. }
  123.  
  124. // Listen for mouseover on mod main links
  125. document.body.addEventListener('mouseover', function(e) {
  126. let target = e.target;
  127. if (target.tagName === 'A' && isModMainLink(target.href)) {
  128. hoverOnLink = true;
  129. fetchGallery(target.href, target);
  130. target.addEventListener('mouseleave', function handler() {
  131. hoverOnLink = false;
  132. startPreviewTimeout();
  133. target.removeEventListener('mouseleave', handler);
  134. });
  135. }
  136. }, true);
  137.  
  138. // Listen for mouseover on mod thumbnails (img inside mod main link)
  139. document.body.addEventListener('mouseover', function(e) {
  140. let target = e.target;
  141. // Typical thumbnails are <img> inside <a>
  142. if (
  143. target.tagName === 'IMG'
  144. && target.closest('a')
  145. && isModMainLink(target.closest('a').href)
  146. ) {
  147. let a = target.closest('a');
  148. hoverOnLink = true;
  149. fetchGallery(a.href, target);
  150. a.addEventListener('mouseleave', function handler() {
  151. hoverOnLink = false;
  152. startPreviewTimeout();
  153. a.removeEventListener('mouseleave', handler);
  154. });
  155. }
  156. }, true);
  157.  
  158. // Remove preview when clicking elsewhere on the page
  159. document.addEventListener('mousedown', function(e) {
  160. if (previewDiv && !previewDiv.contains(e.target)) {
  161. removePreview();
  162. }
  163. });
  164.  
  165. })();