Stuck Preview Fix

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
})();