// ==UserScript== // @name YouTube arrow keys FIX // @version 1.3.1 // @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 // @include https://*.youtube.com/* // @include https://youtube.googleapis.com/embed* // @grant none // @downloadURL 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 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 #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; } `); } 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():", previousPlayerReadyCallback, "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() { //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(); })();