您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Fix YouTube keyboard controls (arrow keys) to be more consistent (Left,Right - jump, Up,Down - volume) after page load or clicking individual controls.
/* eslint-disable userscripts/use-download-and-update-url */ /* -eslint-disable userscripts/better-use-match -- Is this a thing? */ // ==UserScript== // @name YouTube arrow keys FIX // @version 2.0.0 // @description Fix YouTube keyboard controls (arrow keys) to be more consistent (Left,Right - jump, Up,Down - volume) after page load or clicking individual controls. // @author Calcifer // @license MIT // @namespace https://github.com/Calciferz // @homepageURL https://github.com/Calciferz/YoutubeKeysFix // @supportURL https://github.com/Calciferz/YoutubeKeysFix/issues // @icon http://youtube.com/yts/img/favicon_32-vflOogEID.png // @match https://*.youtube.com/* // @match https://youtube.googleapis.com/embed* // @grant none // ==/UserScript== /* eslint-disable no-multi-spaces */ /* eslint-disable no-multi-str */ (function () { 'use strict'; var playerContainer; // = document.getElementById('player-container') || document.getElementById('player') in embeds var playerElem; // = document.getElementById('movie_player') var playerObserver; var isEmbeddedUI; var subtitleObserver; var subtitleContainer; var lastFocusedPageArea; var areaOrder= [ null ], areaContainers= [ null ], areaFocusDefault= [ null ], areaFocusedSubelement= [ null ]; function formatElemIdOrClass(elem) { return elem.id ? '#' + elem.id : elem.className ? '.' + elem.className.replace(' ', '.') : elem.tagName; } function formatElemIdOrTag(elem) { return elem.id ? '#' + elem.id : elem.tagName; } function isElementWithin(elementWithin, ancestor) { if (! ancestor) return null; for (; elementWithin; elementWithin= elementWithin.parentElement) { if (elementWithin === ancestor) return true; } return false; } function getAreaOf(elementWithin) { for (var i= 1; i<areaContainers.length; i++) { if (isElementWithin(elementWithin, areaContainers[i])) return i; } return 0; } function getFocusedArea() { return getAreaOf(document.activeElement); } // Source: jquery function isVisible(elem) { return !elem ? null : !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); } function tryFocus(newFocus) { if (!newFocus) return null; if (!isVisible(newFocus)) return false; //var oldFocus= document.activeElement; newFocus.focus(); var done= (newFocus === document.activeElement); if (! done) console.error("[YoutubeKeysFix] tryFocus(): Failed to focus newFocus=", [newFocus], "activeElement=", [document.activeElement]); return done; } function focusNextArea() { // Focus next area's areaFocusedSubelement (activeElement) var currentArea= getFocusedArea() || 0; var nextArea= (lastFocusedPageArea && lastFocusedPageArea !== currentArea) ? lastFocusedPageArea : currentArea + 1; // captureFocus() will store lastFocusedPageArea again if moving to a non-player area // if moving to the player then lastFocusedPageArea resets, Shift-Esc will move to search bar (area 2) lastFocusedPageArea= null; // To enter player after last area: nextArea= 1; To skip player: nextArea= 2; if (nextArea >= areaContainers.length) nextArea= 2; let done = false; do { done= tryFocus( areaFocusedSubelement[nextArea] ); if (! done) done= tryFocus( document.querySelector( areaFocusDefault[nextArea] ) ); //if (! done) done= tryFocus( areaContainers[nextArea] ); if (! done) nextArea++; } while (!done && nextArea < areaContainers.length); return done; } function redirectEventTo(target, event, cloneEvent) { if (!isVisible(target)) return; cloneEvent= cloneEvent || new Event(event.type); // shallow copy every property for (var k in event) if (! (k in cloneEvent)) cloneEvent[k]= event[k]; cloneEvent.originalEvent= event; event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); try { console.log("[YoutubeKeysFix] redirectEventTo(): type=" + cloneEvent.type, "key='" + cloneEvent.key + "' to=" + formatElemIdOrTag(target), "from=", [event.target, event, cloneEvent]); } catch (err) { console.error("[YoutubeKeysFix] redirectEventTo(): Error while logging=", err); } target.dispatchEvent(cloneEvent); } function handleShiftEsc(event) { // Shift-Esc only implemented for watch page if (window.location.pathname !== "/watch") return; // Not in fullscreen if (getFullscreen()) return; // Bring focus to next area focusNextArea(); event.preventDefault(); event.stopPropagation(); } // Tag list from YouTube Plus: https://github.com/ParticleCore/Particle/blob/master/src/Userscript/YouTubePlus.user.js#L885 var keyHandlingElements= { INPUT:1, TEXTAREA:1, IFRAME:1, OBJECT:1, EMBED:1 }; function onKeydown(event) { // Debug log of key event //if (event.key != 'Shift') console.log("[YoutubeKeysFix] onKeydown(): type=" + event.type, "key='" + event.key + "' target=", [event.target, event]); let keyCode = event.which; // Ignore redirected events to avoid recursion if (event.originalEvent) return; let redirect = false; let inTextbox= keyHandlingElements[event.target.tagName] || event.target.isContentEditable; //|| event.target.getAttribute('role') == 'textbox'; // event.target is the focused element that received the keypress // Space -> pause video except when writing a comment - Youtube takes care of this //if (keyCode == 32) redirect = !inTextbox; //if (keyCode == 32) return redirectEventTo(document.body, event); // Left,Right -> jump 5sec - Youtube takes care of this //if (keyCode == 37 || keyCode == 39) redirect = !inTextbox; // End,Home,Up,Down -> control the player if page is scrolled to the top, otherwise scroll the page if (keyCode == 35 || keyCode == 36 || keyCode == 38 || keyCode == 40) { redirect = !inTextbox && 0 == document.documentElement.scrollTop; } // Debug log of redirect //if (redirect) console.log("[YoutubeKeysFix] onKeydown(): redirect, type=" + event.type, "key='" + event.key + "' target=", [event.target, event]); if (redirect) { return redirectEventTo(playerElem, event); } } function captureKeydown(event) { // Debug log of key event //if (event.key != 'Shift') console.log("[YoutubeKeysFix] captureKeydown(): type=" + event.type, "key='" + event.key + "' target=", [event.target, event]); let keyCode = event.which; // Shift-Esc -> cycle through search box, videos, comments // Event is not received when fullscreen in Opera (already handled by browser) if (keyCode == 27 && event.shiftKey) { return handleShiftEsc(event); } // Ignore redirected events to avoid recursion if (event.originalEvent) { return; } // Only capture events within player if (!isElementWithin(event.target, playerElem)) return; // End,Home,Up,Down -> scroll the page if not scrolled to the top if (keyCode == 35 || keyCode == 36 || keyCode == 38 || keyCode == 40) { if (0 < document.documentElement.scrollTop) return redirectEventTo(document.body, event); } // Sliders' key handling behaviour is inconsistent with the default player behaviour // Redirect arrow keys (33-40: PageUp,PageDown,End,Home,Left,Up,Right,Down) to page scroll/video player (position/volume) if (33 <= keyCode && keyCode <= 40 && event.target !== playerElem && event.target.getAttribute('role') == 'slider') { return redirectEventTo(playerElem, event); } } function captureMouse(event) { // Called when mouse button is pressed/released over an element. // Debug log of mouse button event //console.log("[YoutubeKeysFix] captureMouse(): type=" + event.type, "button=" + event.button, "target=", [event.target, event]); } function onMouse(event) { // Called when mouse button is pressed over an element. // Debug log of mouse button event //console.log("[YoutubeKeysFix] onMouse(): type=" + event.type, "button=" + event.button, "target=", [event.target, event]); } function onWheel(event) { //console.log("[YoutubeKeysFix] onWheel(): deltaY=" + Math.round(event.deltaY), "phase=" + event.eventPhase, "target=", [event.currentTarget, event]); if (! playerElem || ! playerElem.contains(event.target)) return; var deltaY= null !== event.deltaY ? event.deltaY : event.wheelDeltaY; var up= deltaY <= 0; // null == 0 -> up var cloneEvent= new Event('keydown'); cloneEvent.which= cloneEvent.keyCode= up ? 38 : 40; cloneEvent.key= up ? 'ArrowUp': 'ArrowDown'; redirectEventTo(playerElem, event, cloneEvent); } function getFullscreen() { return document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; } function onFullscreen(event) { var fullscreen= getFullscreen(); if (fullscreen) { if ( !fullscreen.contains(document.activeElement) ) { onFullscreen.prevFocus= document.activeElement; fullscreen.focus(); } } else if (onFullscreen.prevFocus) { onFullscreen.prevFocus.focus(); onFullscreen.prevFocus= null; } } function captureFocus(event) { // Called when an element gets focus (by clicking or TAB) // Debug log of focused element //console.log("[YoutubeKeysFix] captureFocus(): target=", [event.target, event]); // Window will focus the activeElement, do nothing at the moment if (event.target === window) return; // Save focused element inside player or on page var area= getAreaOf(event.target); if (0 !== area) { areaFocusedSubelement[area]= event.target; //if (areaContainers[area]) areaContainers[area].activeElement= event.target; // store if not focusing player area if (area !== 1) lastFocusedPageArea= area; } } function initEvents() { // Handlers are capture type to see all events before they are consumed document.addEventListener('mousedown', captureMouse, true); //document.addEventListener('mouseup', captureMouse, true); // captureFocus captures focus changes before the event is handled // does not capture body.focus() in Opera, material design document.addEventListener('focus', captureFocus, true); //window.addEventListener('focusin', captureFocus); document.addEventListener('mousedown', onMouse); // mousewheel over player area adjusts volume // Passive event handler can call preventDefault() on wheel events to prevent scrolling the page //document.addEventListener('wheel', onWheel, { passive: false, capture: true }); // captureKeydown is run before original handlers to capture key presses before the player does document.addEventListener('keydown', captureKeydown, true); // onKeydown handles Tab,End,Home,Up,Down in the bubbling phase after other elements (textbox, button, link) got a chance. document.addEventListener('keydown', onKeydown); if (document.onfullscreenchange !== undefined) document.addEventListener('fullscreenchange', onFullscreen); else if (document.onwebkitfullscreenchange !== undefined) document.addEventListener('webkitfullscreenchange', onFullscreen); else if (document.onmozfullscreenchange !== undefined) document.addEventListener('mozfullscreenchange', onFullscreen); else if (document.MSFullscreenChange !== undefined) document.addEventListener('MSFullscreenChange', onFullscreen); } function initStyle() { let s= document.createElement('style'); s.name= 'YoutubeKeysFix-styles'; s.type= 'text/css'; s.textContent= ` /* Highlight focused button in player */ .ytp-probably-keyboard-focus :focus { background-color: rgba(120, 180, 255, 0.6); } /* Hide the obstructive video suggestions in the embedded player when paused */ .ytp-pause-overlay-container { display: none; } `; document.head.appendChild(s); } function initDom() { // Area names areaOrder= [ null, //'player', 'header', 'comments', 'videos', ]; // Areas' root elements areaContainers= [ null, //document.getElementById('player-container'), // player document.getElementById('masthead-container'), // header document.getElementById('sections'), // comments document.getElementById('related'), // videos ]; // Areas' default element to focus areaFocusDefault= [ null, //'#movie_player', // player '#masthead input#search', // header '#info #menu #top-level-buttons button:last()', // comments '#items a.ytd-compact-video-renderer:first()', // videos ]; } function observePlayer() { // The movie player frame #movie_player is not part of the initial page load. playerElem= document.getElementById('movie_player'); if (playerElem) return initPlayer(); // Player elem observer setup playerObserver = new MutationObserver( mutationHandler ); playerObserver.observe(document.body, { childList: true, subtree: true }); function mutationHandler(mutations, observer) { playerElem= document.getElementById('movie_player'); if (!playerElem) return; console.log("[YoutubeKeysFix] mutationHandler(): #movie_player created, stopped observing body", [playerElem]); // Stop playerObserver observer.disconnect(); initPlayer(); } } function initPlayer() { // Path (on page load): body > ytd-app // Path (DOMContentLoaded): > div#content > ytd-page-manager#page-manager // Path (created 1st step): > ytd-watch-flexy.ytd-page-manager > div#full-bleed-container > div#player-full-bleed-container // Path (created 2nd step): > div#player-container > ytd-player#ytd-player > div#container > div#movie_player.html5-video-player > html5-video-container // Path (created 3rd step): > video.html5-main-video isEmbeddedUI= playerElem.classList.contains('ytp-embed'); playerContainer= document.getElementById('player-container') // full-bleed-container > player-full-bleed-container > player-container > ytd-player > container > movie_player || isEmbeddedUI && document.getElementById('player'); // body > player > movie_player.ytp-embed console.log("[YoutubeKeysFix] initPlayer(): player=", [playerElem]); removeTabStops(); } // Disable focusing certain player controls: volume slider, progress bar, fine seeking bar, subtitle. // It was possible to focus these using TAB, but the controls (space, arrow keys) // change in a confusing manner, creating a miserable UX. // Maybe this is done for accessibility reasons? The irony... // Youtube should have rethought this design for a decade now. function removeTabStops() { //console.log("[YoutubeKeysFix] removeTabStops()"); function removeTabIndexWithSelector(rootElement, selector) { for (let elem of rootElement.querySelectorAll(selector)) { console.log("[YoutubeKeysFix] removeTabIndexWithSelector():", "tabindex=", elem.getAttribute('tabindex'), [elem]); elem.removeAttribute('tabindex'); } } // Remove tab stops from video player //playerElem.removeAttribute('tabindex'); //removeTabIndexWithSelector(document, '#' + playerElem.id + '[tabindex]'); //removeTabIndexWithSelector(document, '#' + playerElem.id); removeTabIndexWithSelector(document, '.html5-video-player'); //removeTabIndexWithSelector(playerElem, '.html5-video-container [tabindex]'); //removeTabIndexWithSelector(playerElem, '.html5-main-video[tabindex]'); removeTabIndexWithSelector(playerElem, '.html5-main-video'); // Remove tab stops from progress bar //removeTabIndexWithSelector(playerElem, '.ytp-progress-bar[tabindex]'); removeTabIndexWithSelector(playerElem, '.ytp-progress-bar'); // Remove tab stops from fine seeking bar //removeTabIndexWithSelector(playerElem, '.ytp-fine-scrubbing-container [tabindex]'); //removeTabIndexWithSelector(playerElem, '.ytp-fine-scrubbing-thumbnails[tabindex]'); removeTabIndexWithSelector(playerElem, '.ytp-fine-scrubbing-thumbnails'); // Remove tab stops from volume slider //removeTabIndexWithSelector(playerElem, '.ytp-volume-panel[tabindex]'); removeTabIndexWithSelector(playerElem, '.ytp-volume-panel'); // Remove tab stops of non-buttons and links (inclusive selector) //removeTabIndexWithSelector(playerElem, '[tabindex]:not(button):not(a):not(div.ytp-ce-element)'); // Make unfocusable all buttons in the player //removeTabIndexWithSelector(playerElem, '[tabindex]'); // Make unfocusable all buttons in the player controls (bottom bar) //removeTabIndexWithSelector(playerElem, '.ytp-chrome-bottom [tabindex]'); //removeTabIndexWithSelector(playerElem.querySelector('.ytp-chrome-bottom'), '[tabindex]'); // Remove tab stops from subtitle element when created function mutationHandler(mutations, observer) { for (let mut of mutations) { //console.log("[YoutubeKeysFix] mutationHandler():\n", mut); // spammy //removeTabIndexWithSelector(mut.target, '.caption-window[tabindex]'); removeTabIndexWithSelector(mut.target, '.caption-window'); if (subtitleContainer) continue; subtitleContainer = playerElem.querySelector('#ytp-caption-window-container'); // If subtitle container is created if (subtitleContainer) { console.log("[YoutubeKeysFix] mutationHandler(): Subtitle container created, stopped observing #movie_player", [subtitleContainer]); // Observe subtitle container instead of movie_player observer.disconnect(); observer.observe(subtitleContainer, { childList: true }); } } } // Subtitle container observer setup // #movie_player > #ytp-caption-window-container > .caption-window subtitleContainer = playerElem.querySelector('#ytp-caption-window-container'); if (!subtitleObserver) { subtitleObserver = new MutationObserver( mutationHandler ); // Observe movie_player because subtitle container is not created yet subtitleObserver.observe(subtitleContainer || playerElem, { childList: true, subtree: !subtitleContainer }); } } console.log("[YoutubeKeysFix] loading: version=" + GM_info.script.version, "sandboxMode=" + GM_info.sandboxMode, "onYouTubePlayerReady=", window.onYouTubePlayerReady); initDom(); initEvents(); initStyle(); // Run initPlayer() when #movie_player is created observePlayer(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址