您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Fix player controls Space, Left, Right, Up, Down to behave consistently after page load or clicking individual controls. Not focusing the mute button anymore.
当前为
// ==UserScript== // @name YouTube arrow keys FIX // @version 1.3.0 // @description Fix player controls Space, Left, Right, Up, Down to behave consistently after page load or clicking individual controls. Not focusing the mute button anymore. // @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 // @include https://*.youtube.com/* // @include https://youtube.googleapis.com/embed* // @grant none // @require http://code.jquery.com/jquery-latest.js // ==/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 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); } function tryFocus(newFocus) { newFocus= $(newFocus); if (! newFocus.length) return null; if (! newFocus.is(':visible()')) return false; //var oldFocus= document.activeElement; newFocus.focus(); var done= (newFocus[0] === document.activeElement); if (! done) console.error("[YoutubeKeysFix] tryFocus(): Failed to focus newFocus=", [newFocus[0]], "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( $(areaFocusDefault[nextArea]) ); //if (! done) done= tryFocus( areaContainers[nextArea] ); if (! done) nextArea++; } while (!done && nextArea < areaContainers.length); return done; } function redirectEventTo(target, event, cloneEvent) { if (! target || ! $(target).is(':visible()')) return; cloneEvent= cloneEvent || new Event(event.type); //var cloneEvent= $.extend(cloneEvent, event); // 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]); // Space -> pause video except when writing a comment - Youtube takes care of this } 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); // Only capture events within player if (!isElementWithin(event.target, playerElem)) return; // 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 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() { $(document.head).append(` <style name="yt-fix-materialUI" type="text/css"> #player-container:focus-within { box-shadow: 0 0 20px 0px rgba(0,0,0,0.8); } /* Seekbar (when visible) gradient shadow is only as high as the seekbar instead of darkening the bottom 1/3 of the video */ /* Copied values from class .ytp-chrome-bottom in www-player.css */ .ytp-chrome-bottom { padding-top: 10px; left: 0 !important; width: 100% !important; background-image: linear-gradient(to top, rgb(0 0 0 / 70%), rgb(0 0 0 / 0%)); } .ytp-chrome-bottom > * { margin-inline: 12px; } .ytp-gradient-bottom { display: none; } /* 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; } </style> `); } 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 initPlayer() { // Path (on page load): body > ytd-app > 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 // The movie player frame #movie_player is not part of the initial page load. playerElem= document.getElementById('movie_player'); if (! playerElem) { console.error("[YoutubeKeysFix] initPlayer(): Failed to find #movie_player element: not created yet"); return false; } if (previousPlayerReadyCallback) { try { previousPlayerReadyCallback.call(arguments); } catch (err) { console.error("[YoutubeKeysFix] initPlayer(): Original onYouTubePlayerReady():", onYouTubePlayerReady, "threw error:", err); } previousPlayerReadyCallback = null; } 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]); // Movie player frame (element) is focused when loading the page to get movie player keyboard controls. if (window.location.pathname === "/watch") playerElem.focus(); 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() { //let $$= document.querySelectorAll; //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 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 && window.MutationObserver) { subtitleObserver = new window.MutationObserver( mutationHandler ); // Observe movie_player because subtitle container is not created yet subtitleObserver.observe(subtitleContainer || playerElem, { childList: true, subtree: !subtitleContainer }); } } console.log("[YoutubeKeysFix] loading: onYouTubePlayerReady=", window.onYouTubePlayerReady); // Run initPlayer() on onYouTubePlayerReady (#movie_player created) let previousPlayerReadyCallback = window.onYouTubePlayerReady; window.onYouTubePlayerReady = initPlayer; //let playerReadyPromise = new Promise( function(resolve, reject) { window.onYouTubePlayerReady = resolve; } ); //playerReadyPromise.then( previousPlayerReadyCallback ).then( initPlayer ); //initPlayer(); initDom(); initEvents(); initStyle(); })();