PageSourceSearch

https://mantyke.icu/js/main.js?v=3

js mantyke.icu collected 2026-09-25 19:25:15 UTC 11,165 bytes, 364 lines download raw bytes

1/**
2 * Image link preview functionality
3 */
4$(document).ready(function() {
5  var previewBox = null;
6  var imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
7  var currentPreviewSrc = null;
8  var currentLink = null;
9  var imageCache = {};
10  var hoverTimer = null;
11  var HOVER_DELAY = 120;
12
13  function createPreviewBox() {
14    if (!previewBox) {
15      previewBox = document.createElement('div');
16      previewBox.className = 'image-preview';
17      document.body.appendChild(previewBox);
18    }
19    return previewBox;
20  }
21
22  function isImageLink(href) {
23    if (!href) return false;
24    var ext = href.split('.').pop().toLowerCase().split('?')[0];
25    return imageExtensions.indexOf(ext) !== -1;
26  }
27
28  function preloadImage(href) {
29    if (imageCache[href]) return imageCache[href];
30    
31    var promise = new Promise(function(resolve, reject) {
32      var tempImg = new Image();
33      tempImg.onload = function() {
34        imageCache[href] = { loaded: true, width: tempImg.naturalWidth, height: tempImg.naturalHeight };
35        resolve(imageCache[href]);
36      };
37      tempImg.onerror = function() {
38        imageCache[href] = { loaded: false, error: true };
39        reject(imageCache[href]);
40      };
41      tempImg.src = href;
42    });
43    
44    imageCache[href] = promise;
45    return promise;
46  }
47
48  /* 站点在 ≥600px 用 body { zoom: 1.2 } 放大版式。getBoundingClientRect 读到的
49     是布局像素,而写回 style 的值还会再被放大一次;两套单位混用会让 clamp 失效、
50     预览卡冲出视口。这里用一根探针实测换算比例,全部先归到视觉像素再摆位
51     (与站内 hoverquote 悬浮卡片同一套做法)。 */
52  var zoomCache = null;
53  function zoomMetrics() {
54    if (zoomCache) return zoomCache;
55    var z = parseFloat(window.getComputedStyle(document.body).zoom) || 1;
56    var k = 1;
57    if (z !== 1) {
58      var probe = document.createElement('i');
59      probe.style.cssText = 'position:fixed;left:0;top:0;width:0;height:0;visibility:hidden;pointer-events:none;';
60      document.body.appendChild(probe);
61      var zero = probe.getBoundingClientRect().left;
62      probe.style.left = '100px';
63      var readScale = (probe.getBoundingClientRect().left - zero) / 100;
64      probe.remove();
65      if (readScale > 0) k = z / readScale;
66    }
67    zoomCache = { z: z, k: k };
68    return zoomCache;
69  }
70
71  /* 预览尺寸按「图片比例 + 视口」算,不再写死 300×300:固定上限会把长截图
72     压成窄条,同一张图在不同视口下也会忽大忽小。 */
73  var PREVIEW_MAX_W = 540;
74  function sizePreview(img, href) {
75    var meta = imageCache[href];
76    var nw = (meta && meta.width) || img.naturalWidth || 0;
77    var nh = (meta && meta.height) || img.naturalHeight || 0;
78    if (!nw || !nh) return;
79
80    var m = zoomMetrics();
81    var viewW = document.documentElement.clientWidth || window.innerWidth;
82    var viewH = document.documentElement.clientHeight || window.innerHeight;
83    var pad = 16; // 卡片左右内边距合计
84    var maxW = Math.max(120, Math.min(PREVIEW_MAX_W, viewW * 0.92) - pad);
85    var maxH = Math.max(120, viewH * 0.8 - pad);
86
87    var w = maxW;
88    var h = w * nh / nw;
89    if (h > maxH) {
90      h = maxH;
91      w = h * nw / nh;
92    }
93    img.style.width = (w / m.z) + 'px';
94    img.style.height = 'auto';
95  }
96
97  /* 摆位:宽度由图片比例决定,不能再用固定值算边界,否则宽图会被拽出视口。 */
98  function placePreview(link) {
99    if (!previewBox || !link) return;
100    var m = zoomMetrics();
101    var box = previewBox.getBoundingClientRect();
102    var boxW = box.width * m.k;
103    var boxH = box.height * m.k;
104    if (!boxW || !boxH) return;
105
106    // 可用范围取可视区(clientWidth/clientHeight),不含滚动条
107    var viewW = document.documentElement.clientWidth || window.innerWidth;
108    var viewH = document.documentElement.clientHeight || window.innerHeight;
109    var edge = 8;
110    var gap = 10;
111
112    var rect = link.getBoundingClientRect();
113    var linkLeft = rect.left * m.k;
114    var linkTop = rect.top * m.k;
115    var linkW = rect.width * m.k;
116    var linkBottom = rect.bottom * m.k;
117
118    var left = linkLeft + linkW / 2 - boxW / 2;
119    left = Math.max(edge, Math.min(left, viewW - boxW - edge));
120
121    // 优先压在链接上方,上方放不下才落到下方
122    var top = linkTop - gap - boxH;
123    if (top < edge) top = linkBottom + gap;
124    top = Math.max(edge, Math.min(top, viewH - boxH - edge));
125
126    previewBox.style.transform = 'none';
127    previewBox.style.left = (left / m.z) + 'px';
128    previewBox.style.top = (top / m.z) + 'px';
129  }
130
131  function revealImage(img, preview, href) {
132    if (currentPreviewSrc !== href) return;
133    function show() {
134      if (currentPreviewSrc !== href) return;
135      img.style.visibility = 'visible';
136      // 自然尺寸此时才可靠:按视口重算尺寸,再摆一次位置
137      sizePreview(img, href);
138      if (currentLink) placePreview(currentLink);
139    }
140    if (img.decode && typeof img.decode === 'function') {
141      img.decode().then(show).catch(show);
142    } else {
143      show();
144    }
145  }
146
147  function showPreview(e, link) {
148    var href = link.getAttribute('href');
149    if (!isImageLink(href)) return;
150
151    currentPreviewSrc = href;
152    currentLink = link;
153
154    var preview = createPreviewBox();
155    var img = preview.querySelector('img') || document.createElement('img');
156    
157    img.style.visibility = 'hidden';
158
159    var cached = imageCache[href];
160    var isCachedReady = cached && cached.loaded === true;
161    
162    if (isCachedReady) {
163      img.src = href;
164      revealImage(img, preview, href);
165    } else {
166      img.onload = function() {
167        revealImage(img, preview, href);
168      };
169      img.onerror = function() {
170        img.style.visibility = 'hidden';
171      };
172      img.src = href;
173    }
174    
175    if (!preview.contains(img)) {
176      preview.appendChild(img);
177    }
178
179    preloadImage(href);
180    // 已有自然尺寸时先定好大小,避免卡片先以默认尺寸闪一下
181    sizePreview(img, href);
182    placePreview(link);
183
184    requestAnimationFrame(function() {
185      preview.classList.add('visible');
186    });
187  }
188
189  function hidePreview() {
190    if (hoverTimer) {
191      clearTimeout(hoverTimer);
192      hoverTimer = null;
193    }
194    if (previewBox) {
195      previewBox.classList.remove('visible');
196      currentPreviewSrc = null;
197      currentLink = null;
198      var img = previewBox.querySelector('img');
199      if (img) {
200        img.style.visibility = 'hidden';
201      }
202    }
203  }
204
205  var links = document.querySelectorAll('.article-content a, .content a');
206  var imageLinks = [];
207  links.forEach(function(link) {
208    if (isImageLink(link.getAttribute('href'))) {
209      imageLinks.push(link);
210      link.addEventListener('mouseenter', function(e) {
211        var self = this;
212        var evt = e;
213        if (hoverTimer) clearTimeout(hoverTimer);
214        hoverTimer = setTimeout(function() {
215          showPreview(evt, self);
216        }, HOVER_DELAY);
217      });
218      link.addEventListener('mouseleave', hidePreview);
219    }
220  });
221
222  // 断点切换会改 body zoom,矩形↔视觉像素的换算比例要重算
223  function resetZoom() {
224    zoomCache = null;
225    if (previewBox && currentLink && previewBox.classList.contains('visible')) {
226      var im = previewBox.querySelector('img');
227      if (im) sizePreview(im, currentPreviewSrc);
228      placePreview(currentLink);
229    }
230  }
231  var zoomMq = window.matchMedia ? window.matchMedia('(min-width: 600px)') : null;
232  if (zoomMq) {
233    if (zoomMq.addEventListener) zoomMq.addEventListener('change', resetZoom);
234    else if (zoomMq.addListener) zoomMq.addListener(resetZoom);
235  }
236  window.addEventListener('resize', resetZoom, { passive: true });
237
238  if ('requestIdleCallback' in window && imageLinks.length > 0) {
239    requestIdleCallback(function() {
240      imageLinks.forEach(function(link) {
241        var href = link.getAttribute('href');
242        if (href && !imageCache[href]) {
243          preloadImage(href);
244        }
245      });
246    }, { timeout: 3000 });
247  } else if (imageLinks.length > 0) {
248    setTimeout(function() {
249      imageLinks.forEach(function(link) {
250        var href = link.getAttribute('href');
251        if (href && !imageCache[href]) {
252          preloadImage(href);
253        }
254      });
255    }, 1500);
256  }
257});
258
259/**
260 * Sets up Justified Gallery.
261 */
262if (!!$.prototype.justifiedGallery) {
263  var options = {
264    rowHeight: 140,
265    margins: 4,
266    lastRow: "justify"
267  };
268  $(".article-gallery").justifiedGallery(options);
269}
270
271$(document).ready(function() {
272
273  /**
274   * Shows the responsive navigation menu on mobile.
275   */
276  $("#header > #nav > ul > .icon").click(function() {
277    $("#header > #nav > ul").toggleClass("responsive");
278  });
279
280
281  /**
282   * Controls the different versions of  the menu in blog post articles 
283   * for Desktop, tablet and mobile.
284   */
285  if ($(".post").length) {
286    var menu = $("#menu");
287    var nav = $("#menu > #nav");
288    var menuIcon = $("#menu-icon, #menu-icon-tablet");
289
290    /**
291     * Display the menu on hi-res laptops and desktops.
292     */
293    if ($(document).width() >= 1440) {
294      menu.css("visibility", "visible");
295      menuIcon.addClass("active");
296    }
297
298    /**
299     * Display the menu if the menu icon is clicked.
300     */
301    menuIcon.click(function() {
302      if (menu.css("visibility") === "hidden") {
303        menu.css("visibility", "visible");
304        menuIcon.addClass("active");
305      } else {
306        menu.css("visibility", "hidden");
307        menuIcon.removeClass("active");
308      }
309      return false;
310    });
311
312    /**
313     * Add a scroll listener to the menu to hide/show the navigation links.
314     */
315    if (menu.length) {
316      $(window).on("scroll", function() {
317        var topDistance = menu.offset().top;
318
319        // hide only the navigation links on desktop
320        if (!nav.is(":visible") && topDistance < 50) {
321          nav.show();
322        } else if (nav.is(":visible") && topDistance > 100) {
323          nav.hide();
324        }
325
326        // on tablet, hide the navigation icon as well and show a "scroll to top
327        // icon" instead
328        if ( ! $( "#menu-icon" ).is(":visible") && topDistance < 50 ) {
329          $("#menu-icon-tablet").show();
330          $("#top-icon-tablet").hide();
331        } else if (! $( "#menu-icon" ).is(":visible") && topDistance > 100) {
332          $("#menu-icon-tablet").hide();
333          $("#top-icon-tablet").show();
334        }
335      });
336    }
337
338    /**
339     * Show mobile navigation menu after scrolling upwards,
340     * hide it again after scrolling downwards.
341     */
342    if ($( "#footer-post").length) {
343      var lastScrollTop = 0;
344      $(window).on("scroll", function() {
345        var topDistance = $(window).scrollTop();
346
347        if (topDistance > lastScrollTop){
348          // downscroll -> show menu
349          $("#footer-post").hide();
350        } else {
351          // upscroll -> hide menu
352          $("#footer-post").show();
353        }
354        lastScrollTop = topDistance;
355
356        // close all submenu"s on scroll
357        $("#nav-footer").hide();
358        $("#toc-footer").hide();
359        $("#share-footer").hide();
360
361      });
362    }
363  }
364});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.