Stuck Preview Fix

Stops stuck video previews on YouTube after middle-click or right-clicking a video to open it in a new tab.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Stuck Preview Fix
// @namespace    https://github.com/sameraskar/Stuck-Preview-Fix-for-YouTube
// @version      1.0.0
// @description  Stops stuck video previews on YouTube after middle-click or right-clicking a video to open it in a new tab.
// @match        https://www.youtube.com/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  const DEBUG = false;
  const CARD_SELECTOR = [
    'ytd-rich-item-renderer',
    'ytd-rich-grid-media',
    'ytd-video-renderer',
    'ytd-grid-video-renderer',
    'yt-lockup-view-model'
  ].join(',');

  const VIDEO_LINK_SELECTOR = [
    'a[href^="/watch"]',
    'a[href^="/shorts/"]',
    'a[href^="/live/"]',
    'a[href*="youtube.com/watch"]',
    'a[href*="youtube.com/shorts/"]',
    'a[href*="youtube.com/live/"]'
  ].join(',');

  const log = (...args) => DEBUG && console.debug('[Stuck Preview Fix]', ...args);

  function findVideoContext(target) {
    if (!(target instanceof Element)) return null;
    const directLink = target.closest(VIDEO_LINK_SELECTOR);
    const card = target.closest(CARD_SELECTOR);
    const link = directLink || card?.querySelector(VIDEO_LINK_SELECTOR);
    return link ? { link, card: card || link.closest(CARD_SELECTOR) } : null;
  }

  function dispatchLeave(element) {
    if (!element?.isConnected) return;
    const common = {
      bubbles: true,
      cancelable: false,
      composed: true,
      relatedTarget: document.body,
      view: window
    };
    try {
      element.dispatchEvent(new PointerEvent('pointerout', common));
      element.dispatchEvent(new PointerEvent('pointerleave', { ...common, bubbles: false }));
    } catch (_) {}
    element.dispatchEvent(new MouseEvent('mouseout', common));
    element.dispatchEvent(new MouseEvent('mouseleave', { ...common, bubbles: false }));
  }

  function clearStuckPreview({ link, card }) {
    const active = document.activeElement;
    if (active instanceof HTMLElement && active !== document.body) active.blur();

    if (card?.isConnected) {
      card.querySelectorAll('video').forEach((video) => {
        try { video.pause(); } catch (_) {}
      });
    }

    dispatchLeave(link);
    dispatchLeave(card);

    const neutral = document.querySelector('ytd-app') || document.body;
    neutral?.dispatchEvent(new MouseEvent('click', {
      bubbles: true,
      cancelable: true,
      composed: true,
      view: window
    }));

    log('Preview cleanup applied');
  }

  function scheduleCleanup(context) {
    [0, 60, 180].forEach((delay) => {
      window.setTimeout(() => clearStuckPreview(context), delay);
    });
  }

  document.addEventListener('auxclick', (event) => {
    if (event.button !== 1) return;
    const context = findVideoContext(event.target);
    if (context) scheduleCleanup(context);
  }, true);

  document.addEventListener('contextmenu', (event) => {
    const context = findVideoContext(event.target);
    if (context) scheduleCleanup(context);
  }, true);
})();