// ==UserScript== // @name 图片快速保存 // @namespace https://docs.scriptcat.org/ // @version 0.0.4 // @description 按 Alt/Option 高亮可下载图片,左键点击快速保存,支持 img、blob、background-image // @author You // @match *://*/* // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @connect * // @noframes // @run-at document-start // ==/UserScript== (function () { 'use strict'; /****************************************************************** * 配置区 ******************************************************************/ const CONFIG = { /** * 触发快捷键 * * 可选值: * alt :Windows/Linux 的 Alt,macOS 的 Option * ctrl :Ctrl / Control * shift :Shift * meta :Windows 键 / macOS Command */ modifierKey: 'alt', /** * 文件名格式 * * 可用变量: * {site} 当前网站名,例如 ChatGPT / XChina / Fuliba2024 * {datetime} 当前日期时间 * {ext} 图片后缀,例如 jpg / png / webp * * 示例: * '{site}-{datetime}.{ext}' * '{datetime}-{site}.{ext}' * '{site}_图片_{datetime}.{ext}' */ filenameFormat: '{site}-{datetime}.{ext}', /** * 时间格式 * * 可用变量: * YYYY 年 * MM 月 * DD 日 * HH 时 * mm 分 * ss 秒 * * 注意: * Windows 文件名不能包含英文冒号 : * 所以默认用 HH-mm-ss,而不是 HH:mm:ss */ datetimeFormat: 'YYYY-MM-DD HH-mm-ss', /** * 高亮颜色 */ highlightColor: '#409EFF', /** * 是否启用按快捷键高亮可下载图片 */ enableHighlight: true, /** * 防止网站点击事件继续跳转的拦截时间,单位毫秒 */ suppressDuration: 1500, /** * 图片 URL 兜底下载(GM_download)。 * ⚠️ 已禁用:GM_download 无法校验响应内容类型, * 当图片服务器返回 403/302 时会把错误页(HTML)当作图片保存。 * 如需启用请改为 true,但请自行承担风险。 */ allowImageUrlFallback: false, /** * 动态页面重新扫描间隔,单位毫秒 */ scanDelay: 300, /** * 按住快捷键时,刷新 overlay 位置的频率,单位毫秒 * 数值越小,滚动/布局变化时跟随越及时 */ overlayUpdateInterval: 120, /** * 最小识别尺寸,避免高亮特别小的图标 */ minImageWidth: 8, minImageHeight: 8, /** * 是否在控制台打印调试信息 */ debug: false }; /** * 站点名特殊映射 * 只影响文件名,不影响下载功能 * * 例如: * chatgpt.com 默认可能生成 Chatgpt * 这里可以美化成 ChatGPT */ const SITE_NAME_MAP = { chatgpt: 'ChatGPT', xchina: 'XChina', github: 'GitHub', youtube: 'YouTube', bilibili: 'Bilibili' }; /****************************************************************** * 常量区 ******************************************************************/ const IMAGE_EXT_REG = /\.(jpg|jpeg|png|webp|gif|bmp|avif|svg)(\?|#|$)/i; const HIGHLIGHT_CLASS = 'image-quick-save-overlay'; const HIGHLIGHT_STYLE_ID = 'image-quick-save-style'; const HIGHLIGHT_CONTAINER_ID = 'image-quick-save-overlay-container'; /****************************************************************** * 状态区 ******************************************************************/ let modifierMode = false; let suppressUntil = 0; let scanTimer = null; let overlayUpdateTimer = null; /** * Map 结构: * key = 原始图片元素 * value = overlay 高亮元素 */ const highlightedElements = new Map(); init(); /****************************************************************** * 初始化 ******************************************************************/ function init() { injectHighlightStyle(); bindKeyboardEvents(); bindMouseEvents(); bindPageEvents(); startObserver(); log('已启用:按快捷键高亮图片,点击快速保存,overlay 稳定高亮版'); } function log(...args) { if (!CONFIG.debug) return; console.log('[图片快速保存]', ...args); } function warn(...args) { console.warn('[图片快速保存]', ...args); } /****************************************************************** * 高亮样式 ******************************************************************/ function injectHighlightStyle() { const style = document.createElement('style'); style.id = HIGHLIGHT_STYLE_ID; style.textContent = ` #${HIGHLIGHT_CONTAINER_ID} { position: fixed !important; left: 0 !important; top: 0 !important; width: 0 !important; height: 0 !important; pointer-events: none !important; z-index: 2147483647 !important; } .${HIGHLIGHT_CLASS} { position: fixed !important; pointer-events: none !important; box-sizing: border-box !important; border: 3px solid ${CONFIG.highlightColor} !important; box-shadow: 0 0 12px 4px ${CONFIG.highlightColor} !important; border-radius: 4px !important; transition: none !important; z-index: 2147483647 !important; } `; const append = () => { if (!document.getElementById(HIGHLIGHT_STYLE_ID)) { document.documentElement.appendChild(style); } }; if (document.documentElement) { append(); } else { document.addEventListener('DOMContentLoaded', append, { once: true }); } } function getOverlayContainer() { let container = document.getElementById(HIGHLIGHT_CONTAINER_ID); if (!container) { container = document.createElement('div'); container.id = HIGHLIGHT_CONTAINER_ID; document.documentElement.appendChild(container); } return container; } function isInternalHighlightNode(node) { if (!node) return false; const element = node.nodeType === 1 ? node : node.parentElement; if (!element) return false; return ( element.id === HIGHLIGHT_STYLE_ID || element.id === HIGHLIGHT_CONTAINER_ID || element.classList?.contains(HIGHLIGHT_CLASS) || Boolean(element.closest?.(`#${HIGHLIGHT_CONTAINER_ID}`)) ); } /****************************************************************** * 快捷键判断 ******************************************************************/ function isModifierPressed(event) { switch (CONFIG.modifierKey) { case 'alt': return event.altKey; case 'ctrl': return event.ctrlKey; case 'shift': return event.shiftKey; case 'meta': return event.metaKey; default: return event.altKey; } } function getModifierEventKeyName() { switch (CONFIG.modifierKey) { case 'alt': return 'Alt'; case 'ctrl': return 'Control'; case 'shift': return 'Shift'; case 'meta': return 'Meta'; default: return 'Alt'; } } function isModifierLeftClick(event) { return event.button === 0 && isModifierPressed(event); } /****************************************************************** * 事件拦截 ******************************************************************/ function shouldSuppress() { return Date.now() < suppressUntil; } function suppressEvent(event) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); } /****************************************************************** * 图片识别 ******************************************************************/ function getBackgroundImageUrl(element) { if (!element || element.nodeType !== 1) return ''; const style = window.getComputedStyle(element); const bg = style.backgroundImage || ''; if (!bg || bg === 'none') return ''; /** * 支持: * url("xxx") * url('xxx') * url(xxx) */ const match = bg.match(/url\(["']?(.*?)["']?\)/i); if (!match || !match[1]) return ''; return match[1]; } function isDownloadableImageUrl(url) { if (!url) return false; return ( url.startsWith('blob:') || url.startsWith('data:image/') || IMAGE_EXT_REG.test(url) ); } function isDownloadableImageAbsoluteUrl(url) { if (!url) return false; return ( url.startsWith('blob:') || url.startsWith('data:image/') || IMAGE_EXT_REG.test(new URL(url, location.href).pathname) ); } function getImgUrl(img) { if (!img) return ''; return ( img.currentSrc || img.src || img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-original') || img.getAttribute('data-lazy-src') || img.getAttribute('data-url') || '' ); } function findDownloadableDescendant(element) { if (!element || element.nodeType !== 1) return null; if (element === document.body || element === document.documentElement) return null; const img = Array.from(element.querySelectorAll?.('img') || []).find(getImgUrl); if (img) return img; return Array.from( element.querySelectorAll?.('[style*="background-image"], [role="img"], .img') || [] ).find(getBackgroundImageUrl) || null; } function getImageUrl(element) { if (!element || element.nodeType !== 1) return ''; if (element.tagName === 'IMG') { return getImgUrl(element); } const bgUrl = getBackgroundImageUrl(element); if (bgUrl) { return bgUrl; } const link = element.closest?.('a[href]'); if (link) { const href = link.href || link.getAttribute('href') || ''; if (isDownloadableImageUrl(href)) { return href; } } const descendant = findDownloadableDescendant(element); if (descendant) { return getImageUrl(descendant); } return ''; } function findDownloadElement(target) { if (!target || target.nodeType !== 1) return null; const img = target.closest?.('img'); if (img && getImgUrl(img)) { return img; } const linkContainer = target.closest?.('a[href]'); if (linkContainer) { const href = linkContainer.href || linkContainer.getAttribute('href') || ''; if (isDownloadableImageUrl(href)) { return linkContainer; } const linkDescendant = findDownloadableDescendant(linkContainer); if (linkDescendant) { return linkDescendant; } } const imageContainer = target.closest?.('.photo-image, [role="link"]'); if (imageContainer) { const imageDescendant = findDownloadableDescendant(imageContainer); if (imageDescendant) { return imageDescendant; } } let el = target; while (el && el !== document.documentElement && el.nodeType === 1) { const bgUrl = getBackgroundImageUrl(el); if (bgUrl) { return el; } const link = el.closest?.('a[href]'); if (link) { const href = link.href || link.getAttribute('href') || ''; if (isDownloadableImageUrl(href)) { return link; } const descendant = findDownloadableDescendant(link); if (descendant) { return descendant; } } el = el.parentElement; } return null; } /****************************************************************** * 高亮扫描:overlay 方案,稳定常亮 ******************************************************************/ function scanAndHighlightImages() { if (!modifierMode || !CONFIG.enableHighlight) return; const candidates = collectDownloadableElements(); highlightedElements.forEach((overlay, el) => { if ( !candidates.has(el) || !isVisibleElement(el) || !document.documentElement.contains(el) ) { removeHighlight(el); } }); candidates.forEach(el => { if (!isVisibleElement(el)) return; if (!highlightedElements.has(el)) { addHighlight(el); } else { updateHighlightPosition(el); } }); } function collectDownloadableElements() { const candidates = new Set(); document.querySelectorAll('img').forEach(img => { if (getImgUrl(img)) { candidates.add(img); } }); document.querySelectorAll('[style*="background-image"], [role="img"], .img').forEach(el => { if (getBackgroundImageUrl(el)) { candidates.add(el); } }); document.querySelectorAll('a[href]').forEach(a => { const href = a.href || a.getAttribute('href') || ''; if (isDownloadableImageUrl(href)) { candidates.add(a); } }); return candidates; } function addHighlight(el) { try { const container = getOverlayContainer(); const overlay = document.createElement('div'); overlay.className = HIGHLIGHT_CLASS; overlay.setAttribute('data-image-quick-save-overlay', 'true'); container.appendChild(overlay); highlightedElements.set(el, overlay); updateHighlightPosition(el); } catch (err) { warn('添加高亮失败:', err); } } function removeHighlight(el) { const overlay = highlightedElements.get(el); if (overlay) { try { overlay.remove(); } catch (_) {} } highlightedElements.delete(el); } function clearHighlights() { highlightedElements.forEach(overlay => { try { overlay.remove(); } catch (_) {} }); highlightedElements.clear(); const container = document.getElementById(HIGHLIGHT_CONTAINER_ID); if (container) { try { container.remove(); } catch (_) {} } } function updateHighlightPosition(el) { const overlay = highlightedElements.get(el); if (!overlay) return; const rect = el.getBoundingClientRect(); const left = Math.round(rect.left); const top = Math.round(rect.top); const width = Math.round(rect.width); const height = Math.round(rect.height); const nextRect = `${left},${top},${width},${height}`; if (overlay.__imageQuickSaveRect === nextRect) return; overlay.__imageQuickSaveRect = nextRect; overlay.style.left = `${left}px`; overlay.style.top = `${top}px`; overlay.style.width = `${width}px`; overlay.style.height = `${height}px`; } function updateAllHighlightPositions() { highlightedElements.forEach((overlay, el) => { if (!isVisibleElement(el) || !document.documentElement.contains(el)) { removeHighlight(el); return; } updateHighlightPosition(el); }); } function isVisibleElement(el) { if (!el || el.nodeType !== 1) return false; const rect = el.getBoundingClientRect(); if (rect.width < CONFIG.minImageWidth) return false; if (rect.height < CONFIG.minImageHeight) return false; /** * 完全在视口外的元素不高亮,避免长页面上创建太多 overlay */ if (rect.bottom < 0) return false; if (rect.right < 0) return false; if (rect.top > window.innerHeight) return false; if (rect.left > window.innerWidth) return false; const style = window.getComputedStyle(el); if (style.display === 'none') return false; if (style.visibility === 'hidden') return false; if (Number(style.opacity) === 0) return false; return true; } function scheduleScan() { if (!modifierMode || !CONFIG.enableHighlight) return; clearTimeout(scanTimer); scanTimer = setTimeout(scanAndHighlightImages, CONFIG.scanDelay); } function startOverlayPositionUpdater() { stopOverlayPositionUpdater(); overlayUpdateTimer = setInterval(() => { if (!modifierMode) return; updateAllHighlightPositions(); }, CONFIG.overlayUpdateInterval); } function stopOverlayPositionUpdater() { if (overlayUpdateTimer) { clearInterval(overlayUpdateTimer); overlayUpdateTimer = null; } } /****************************************************************** * 文件名生成 ******************************************************************/ function sanitizeFilename(name) { return String(name || '') .replace(/[\\/:*?"<>|]/g, '_') .replace(/\s+/g, ' ') .trim() .slice(0, 150); } function getExtFromMime(mimeType) { const map = { 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/png': 'png', 'image/webp': 'webp', 'image/gif': 'gif', 'image/svg+xml': 'svg', 'image/bmp': 'bmp', 'image/avif': 'avif' }; return map[String(mimeType || '').toLowerCase()] || ''; } function isImageMimeType(mimeType) { return /^image\//i.test(String(mimeType || '')); } function isFetchBlockedBeforeResponse(err) { return err instanceof TypeError; } function getExtFromUrl(url) { try { const u = new URL(url, location.href); let match = u.pathname.match(/\.(jpg|jpeg|png|webp|gif|bmp|avif|svg)$/i); if (match) { return match[1].toLowerCase() === 'jpeg' ? 'jpg' : match[1].toLowerCase(); } /** * 兼容这种代理图片: * https://img03.sogoucdn.com/.../?appid=122&url=https://xxx.jpg */ const innerUrl = u.searchParams.get('url'); if (innerUrl) { match = innerUrl.match(/\.(jpg|jpeg|png|webp|gif|bmp|avif|svg)(\?|#|$)/i); if (match) { return match[1].toLowerCase() === 'jpeg' ? 'jpg' : match[1].toLowerCase(); } } } catch (_) {} return ''; } function getSiteName() { const hostname = location.hostname || 'image'; if (hostname === 'localhost') { return 'Localhost'; } if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { return sanitizeFilename(hostname); } const host = hostname.replace(/^www\./i, ''); const parts = host.split('.').filter(Boolean); let core = parts[0] || 'image'; if (parts.length >= 2) { core = parts[parts.length - 2]; const last = parts[parts.length - 1]; const secondLast = parts[parts.length - 2]; /** * 兼容类似: * example.co.uk * example.com.cn */ if ( parts.length >= 3 && ['uk', 'jp', 'cn', 'au', 'hk'].includes(last) && ['co', 'com', 'net', 'org', 'gov', 'edu'].includes(secondLast) ) { core = parts[parts.length - 3]; } } const lowerCore = core.toLowerCase(); if (SITE_NAME_MAP[lowerCore]) { return sanitizeFilename(SITE_NAME_MAP[lowerCore]); } const pretty = core .split(/[-_]+/) .filter(Boolean) .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(''); return sanitizeFilename(pretty || 'Image'); } function pad2(value) { return String(value).padStart(2, '0'); } function formatDateTime(date, format) { const tokens = { YYYY: String(date.getFullYear()), MM: pad2(date.getMonth() + 1), DD: pad2(date.getDate()), HH: pad2(date.getHours()), mm: pad2(date.getMinutes()), ss: pad2(date.getSeconds()) }; return String(format || 'YYYY-MM-DD HH-mm-ss').replace( /YYYY|MM|DD|HH|mm|ss/g, token => tokens[token] ); } function getDateTimeString() { return formatDateTime(new Date(), CONFIG.datetimeFormat); } function buildFilename(url, mimeType = '') { const site = getSiteName(); const datetime = getDateTimeString(); const ext = getExtFromMime(mimeType) || getExtFromUrl(url) || 'jpg'; const filename = CONFIG.filenameFormat .replaceAll('{site}', site) .replaceAll('{datetime}', datetime) .replaceAll('{ext}', ext); return sanitizeFilename(filename); } /****************************************************************** * 下载逻辑 ******************************************************************/ function triggerDownloadByA(href, filename) { const a = document.createElement('a'); a.href = href; a.download = filename; a.rel = 'noopener noreferrer'; a.style.display = 'none'; document.documentElement.appendChild(a); a.click(); a.remove(); } function downloadByGM(url, filename) { return new Promise((resolve, reject) => { if (typeof GM_download !== 'function') { reject(new Error('GM_download 不可用')); return; } GM_download({ url, name: filename, saveAs: false, onload: resolve, onerror: reject, ontimeout: reject }); }); } function getGMXmlHttpRequest() { if (typeof GM_xmlhttpRequest === 'function') { return GM_xmlhttpRequest; } if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest === 'function') { return GM.xmlHttpRequest.bind(GM); } if (typeof GM !== 'undefined' && typeof GM.xmlhttpRequest === 'function') { return GM.xmlhttpRequest.bind(GM); } return null; } function getResponseHeader(headers, name) { const target = String(name || '').toLowerCase(); return String(headers || '') .split(/\r?\n/) .map(line => line.split(':')) .find(parts => String(parts[0] || '').trim().toLowerCase() === target) ?.slice(1) .join(':') .trim() || ''; } function createBlobFromResponse(response, contentType) { const body = response.response; if (body instanceof Blob) { return body; } if (body instanceof ArrayBuffer || typeof body === 'string') { return new Blob([body], { type: contentType || '' }); } return null; } function downloadByGMRequest(url, filename) { return new Promise((resolve, reject) => { const request = getGMXmlHttpRequest(); if (!request) { reject(new Error('GM_xmlhttpRequest 不可用')); return; } const onload = response => { const status = response.status || response.statusCode || 0; if (status < 200 || status >= 300) { reject(new Error(`HTTP ${status}`)); return; } const contentType = getResponseHeader(response.responseHeaders, 'content-type'); if (contentType && !isImageMimeType(contentType)) { const err = new Error(`非图片响应:${contentType}`); err.isNonImageResponse = true; reject(err); return; } const blob = createBlobFromResponse(response, contentType); if (!blob) { reject(new Error('GM_xmlhttpRequest 没有返回可下载内容')); return; } if (blob.type && !isImageMimeType(blob.type)) { const err = new Error(`非图片响应:${blob.type}`); err.isNonImageResponse = true; reject(err); return; } const objectUrl = URL.createObjectURL(blob); triggerDownloadByA(objectUrl, buildFilename(url, blob.type || contentType)); setTimeout(() => { URL.revokeObjectURL(objectUrl); }, 3000); resolve(); }; const details = { method: 'GET', url, responseType: 'blob', timeout: 30000, headers: { 'Referer': location.href }, onload, onerror: reject, ontimeout: reject }; try { const result = request(details); if (result && typeof result.then === 'function') { result.then(response => { if (response) { onload(response); } }).catch(reject); } } catch (err) { reject(err); } }); } async function downloadByFetch(url) { const response = await fetch(url, { credentials: 'omit', cache: 'no-store', referrer: location.href, referrerPolicy: 'no-referrer-when-downgrade' }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const blob = await response.blob(); if (blob.type && !isImageMimeType(blob.type)) { const err = new Error(`非图片响应:${blob.type}`); err.isNonImageResponse = true; throw err; } const objectUrl = URL.createObjectURL(blob); const filename = buildFilename(url, blob.type); triggerDownloadByA(objectUrl, filename); setTimeout(() => { URL.revokeObjectURL(objectUrl); }, 3000); } async function saveImageUrl(url) { if (!url) { warn('没有找到图片地址'); return; } const absoluteUrl = new URL(url, location.href).href; if (!isDownloadableImageAbsoluteUrl(absoluteUrl)) { warn('跳过非图片地址:', absoluteUrl); return; } const filename = buildFilename(absoluteUrl); log('尝试下载:', absoluteUrl); if (absoluteUrl.startsWith('data:image/')) { triggerDownloadByA(absoluteUrl, filename); return; } if (absoluteUrl.startsWith('blob:')) { try { await downloadByFetch(absoluteUrl); return; } catch (err) { if (err.isNonImageResponse) { warn('跳过非图片响应:', absoluteUrl, err); return; } warn('blob fetch 失败,尝试直接下载:', err); triggerDownloadByA(absoluteUrl, filename); return; } } try { await downloadByGMRequest(absoluteUrl, filename); return; } catch (err) { if (err.isNonImageResponse) { warn('跳过非图片响应:', absoluteUrl, err); return; } warn('GM_xmlhttpRequest 失败,尝试 fetch:', err); } try { await downloadByFetch(absoluteUrl); return; } catch (err) { if (err.isNonImageResponse) { warn('跳过非图片响应:', absoluteUrl, err); return; } if (!isFetchBlockedBeforeResponse(err)) { warn('fetch 未取得有效图片响应,已跳过无校验下载:', absoluteUrl, err); return; } warn('fetch 被 CORS 或网络层拦截:', err); } /** * GM_download 兜底已移除:它无法校验响应内容类型, * 当图片服务器返回 403/302 时会把错误页(HTML)当作图片保存。 * 如果 fetch 也失败(CORS),说明该图片确实无法下载,不再尝试。 */ warn('所有下载方式均失败,已跳过:', absoluteUrl); } /****************************************************************** * 事件绑定 ******************************************************************/ function bindKeyboardEvents() { window.addEventListener( 'keydown', function (event) { if (event.key !== getModifierEventKeyName()) return; event.preventDefault(); if (modifierMode) return; modifierMode = true; scanAndHighlightImages(); startOverlayPositionUpdater(); }, true ); window.addEventListener( 'keyup', function (event) { if (event.key !== getModifierEventKeyName()) return; event.preventDefault(); modifierMode = false; stopOverlayPositionUpdater(); clearHighlights(); }, true ); window.addEventListener( 'blur', function () { modifierMode = false; stopOverlayPositionUpdater(); clearHighlights(); }, true ); } function bindPageEvents() { window.addEventListener( 'scroll', function () { if (!modifierMode) return; updateAllHighlightPositions(); scheduleScan(); }, true ); window.addEventListener( 'resize', function () { if (!modifierMode) return; updateAllHighlightPositions(); scheduleScan(); }, true ); } function startObserver() { const start = () => { if (!document.documentElement) return; const observer = new MutationObserver(function (mutations) { const hasPageMutation = mutations.some(mutation => { if (isInternalHighlightNode(mutation.target)) return false; if (mutation.type === 'attributes') return true; const changedNodes = [ ...mutation.addedNodes, ...mutation.removedNodes ]; return changedNodes.some(node => !isInternalHighlightNode(node)); }); if (hasPageMutation) { scheduleScan(); } }); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, /** * 不监听 class,避免网站频繁改 class 导致反复扫描。 * style 仍保留,因为 background-image 通常在 style 中。 */ attributeFilter: ['src', 'srcset', 'style'] }); }; if (document.documentElement) { start(); } else { document.addEventListener('DOMContentLoaded', start, { once: true }); } } function bindMouseEvents() { function handleModifierImageEvent(event) { if (!isModifierLeftClick(event)) return; if (shouldSuppress()) { suppressEvent(event); return; } const element = findDownloadElement(event.target); if (!element) { const link = event.target.closest?.('a[href]'); const href = link?.href || link?.getAttribute?.('href') || ''; if (link && !isDownloadableImageUrl(href)) { suppressEvent(event); } return; } suppressUntil = Date.now() + CONFIG.suppressDuration; suppressEvent(event); const url = getImageUrl(element); if (!url) return; saveImageUrl(url); } /** * pointerdown 比 click 更早,可以抢在网站跳转之前处理。 */ window.addEventListener('pointerdown', handleModifierImageEvent, true); document.addEventListener('pointerdown', handleModifierImageEvent, true); window.addEventListener('mousedown', handleModifierImageEvent, true); document.addEventListener('mousedown', handleModifierImageEvent, true); window.addEventListener('click', handleModifierImageEvent, true); document.addEventListener('click', handleModifierImageEvent, true); /** * 阻止网站后续 click / mouseup / pointerup 打开新页面。 */ [ 'pointerup', 'mouseup', 'click', 'auxclick', 'dblclick' ].forEach(eventName => { window.addEventListener( eventName, function (event) { if (!shouldSuppress()) return; const element = findDownloadElement(event.target); if (!element) return; suppressEvent(event); }, true ); document.addEventListener( eventName, function (event) { if (!shouldSuppress()) return; const element = findDownloadElement(event.target); if (!element) return; suppressEvent(event); }, true ); }); } })();