Greasy Fork is available in English.

Youtube key shortcuts FIX

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 key shortcuts FIX
// @namespace    https://github.com/h93
// @homepageURL  https://github.com/h93/YoutubeKeysFix
// @supportURL   https://github.com/h93/YoutubeKeysFix/issues
// @version      1.1.2
// @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.
// @icon         http://youtube.com/yts/img/favicon_32-vflOogEID.png
// @author       Greg Herendi
// @license      MIT
// @copyright    2018, Greg Herendi (https://github.com/GregHerendi)
// @match        *://www.youtube.com/*
// @match        *://*.youtube.com/*
// @exclude      *://*.youtube.com/tv*
// @exclude      *://*.youtube.com/live_chat*
// @grant        none
// @require      http://code.jquery.com/jquery-latest.js
// ==/UserScript==

/* To test classicUI add the appropriate one of the following to Youtube url:
&disable_polymer=1
?disable_polymer=1
*/

(function () {
  "use strict";

  var playerContainer; // = document.getElementById('player-container') || document.getElementById('player') in embeds
  var playerElem; // = document.getElementById('movie_player') || playerContainer.querySelector('.html5-video-player') in embeds
  //var playerFocused;
  var isMaterialUI, isClassicUI;
  var lastFocusedPageArea;
  var areaOrder = [null],
    areaContainers = [null],
    areaFocusDefault = [null],
    areaFocusedSubelement = [null];
  //var areaContainers= {}, areaFocusedSubelement= {};

  function isSubelementOf(elementWithin, ancestor) {
    if (!ancestor) return null;
    for (; elementWithin; elementWithin = elementWithin.parentElement) {
      if (elementWithin.id == ancestor) return true;
    }
    return false;
  }

  function getAreaOf(elementWithin) {
    for (var i = 1; i < areaContainers.length; i++)
      if (isSubelementOf(elementWithin, areaContainers[i])) return i;
    return 0;
    //for (var area in areaContainers)  if (isSubelementOf(document.activeElement, areaContainers[area]))  return area;
  }
  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;
    window.scrollTo(0,0);
    newFocus.focus();
    var done = newFocus[0] === document.activeElement;
    if (!done)
      console.log(
        "YoutubeKeysFix: failed to focus newFocus=, activeElement=",
        newFocus[0],
        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;

    var done = tryFocus(areaFocusedSubelement[nextArea]);
    if (!done) done = tryFocus($(areaFocusDefault[nextArea]));
    //if (! done)  done= tryFocus( areaContainers[nextArea] );
    return done;
  }

  function focusPlayer() {
    var player = $(areaFocusDefault[1]);
    if (!player[0]) return false;
    // If focus was outside player
    var focusSubelement =
      !player[0].contains(document.activeElement) && areaFocusedSubelement[1];
    // And focusSubelement is inside player then focus that finally
    if (
      focusSubelement === player[0] ||
      (focusSubelement && !player[0].contains(focusSubelement))
    )
      focusSubelement = null;

    // Focus player first to scroll into view, then the subelement
    var done = tryFocus(player);
    if (!done) return false;

    // Focus player's areaFocusedSubelement if focus was outside player area
    done = focusSubelement && tryFocus(focusSubelement);
    // Show that focus indicator blue frame and background if subelement got focus
    if (done) player.addClass("ytp-probably-keyboard-focus");

    return true;
  }

  function handleEsc(event) {
    if (event.shiftKey) {
      // Shift-Esc only implemented for watch page
      if (window.location.pathname !== "/watch") return;
      // Not in fullscreen
      if (getFullscreen()) return;
      // show focus outline when navigating focus
      $(document.documentElement).removeClass("no-focus-outline");
      // Bring focus to next area
      focusNextArea();
    } else {
      var handled = focusPlayer();
      if (!handled) return;
    }

    event.preventDefault();
    event.stopPropagation();
  }

  function onKeydown(event) {
    // Debug log of key event
    //if (event.key != 'Shift')  console.log("YoutubeKeysFix: " + event.type + " " + event.which + " ->", event.target, event);
    if (event.which == 9) {
      // show focus outline when navigating focus
      $(document.documentElement).removeClass("no-focus-outline");
    }

    // event.target is the focused element (that received the keypress)
    // event not received when fullscreen in Opera (already handled by browser)
    if (event.which == 27) return handleEsc(event);
  }

  function redirectEvent(event, cloneEvent) {
    if (!playerElem) initPlayer();
    if (!playerElem || !$(playerElem).is(":visible()")) return;
    cloneEvent = cloneEvent || new Event(event.type);
    //var cloneEvent= $.extend(cloneEvent, event);
    cloneEvent.redirectedEvent = event;
    // shallow copy every property
    for (var k in event) if (!(k in cloneEvent)) cloneEvent[k] = event[k];

    event.preventDefault();
    event.stopPropagation();
    event.stopImmediatePropagation();
    //console.log('YoutubeKeysFix: dispatch cloneEvent=', cloneEvent);
    playerElem.dispatchEvent(cloneEvent);
  }

  // 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 captureKeydown(event) {
    // Debug log of key event
    //if (event.key != 'Shift')  console.log("YoutubeKeysFix: capture " + event.type + " " + event.which + " ->", event);  //, event);

    // Esc switches focus between player(player-api/player-container(movie_player)) and webpage outside the player(masthead(buttons)/main(related/info/meta/comments))
    // event.target is focused (received the keypress)
    // captureKeydown not executed when fullscreen, and this should not handle Esc
    var textbox =
      keyHandlingElements[event.target.tagName] ||
      event.target.isContentEditable; //  ||  event.target.getAttribute('role') == 'textbox';
    // capture only in textboxes to override their behaviour, general handling in onKeydown()
    if (event.which == 27 && textbox) return handleEsc(event);

    // Tab: do the default
    //if (event.which == 9)  return;

    // Ignore events for the playerElem to avoid recursion
    //if (playerElem == document.activeElement)  return;
    if (playerElem === event.target) return;

    // Redirect Space (32) to pause video, if not in a textbox
    var redirectSpace = 32 == event.which && !textbox;
    // Sliders' key handling behaviour is inconsistent with the default player behaviour. To disable them
    // arrowkey events (33-40: PageUp/PageDown/End/Home/Left/Up/Right/Down) are redirected to page scroll/video position/volume
    var redirectArrows =
      33 <= event.which &&
      event.which <= 40 &&
      event.target.getAttribute("role") == "slider" &&
      isSubelementOf(event.target, "player-container");
    if (redirectSpace || redirectArrows) return redirectEvent(event);
  }

  function captureMouse(event) {
    // Called when mouse button is pressed/released over an element.
    // Debug log of mouse button event
    //console.log("YoutubeKeysFix: capture " + event.type + " ->", event.target);

    // hide focus outline when clicking
    $(document.documentElement).addClass("no-focus-outline");
  }

  function redirectFocus(event, newFocus) {
    if (!newFocus) return;
    event.preventDefault();
    event.stopPropagation();
    event.stopImmediatePropagation();
    //console.log('YoutubeKeysFix: redirect focus=', newFocus);
    newFocus.focus();
  }

  function onMouse(event) {
    // Called when mouse button is pressed over an element.
    // Debug log of mouse button event
    //console.log("YoutubeKeysFix: " + event.type + " ->", event.target);

    // click outside of areas focuses player
    if (0 === getAreaOf(event.target)) return redirectFocus(event, playerElem);
  }

  function onWheel(event) {
    console.log(
      "YoutubeKeysFix: " +
        event.type +
        " " +
        event.deltaY +
        " phase " +
        event.eventPhase +
        " ->",
      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";
    redirectEvent(event, cloneEvent);
  }

  function getFullscreen() {
    return (
      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: capture " + event.type + " ->", event.target);

    // Window will focus the activeElement, do nothing at the moment
    if (event.target === window) return;

    // Focus player if focusing body (default focus, eg. pressing Esc)
    if (event.target === document.body)
      return redirectFocus(event, areaFocusedSubelement[1] || playerElem);

    // Save focused element inside player or on page
    var area = getAreaOf(event.target);
    //if (0 === area)  return redirectFocus(event, playerElem);
    if (0 !== area) {
      areaFocusedSubelement[area] = event.target;
      //if (areaContainers[area])  document.getElementById(areaContainers[area]).activeElement= event.target;
      // store if not focusing player area
      if (area !== 1) lastFocusedPageArea = area;
    }
  }

  function chainInitFunc(f1, f2) {
    return function () {
      //$(document).ready(f1);
      if (f1) f1.apply(this, arguments);
      if (f2) f2.apply(this, arguments);
    };
  }

  // Run init on onYouTubePlayerReady ('#movie_player' created).
  //console.log("YoutubeKeysFix: loading, onYouTubePlayerReady=", window.onYouTubePlayerReady);
  window.onYouTubePlayerReady = chainInitFunc(
    initPlayer,
    window.onYouTubePlayerReady
  );
  initEvents();
  initStyle();
  initDom();

  //document.addEventListener("DOMContentLoaded", function() {
  $(document).ready(function () {
    //console.log("YoutubeKeysFix: $(document).ready()");
    //initDom();
    initPlayer();
  });

  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
    //document.addEventListener('wheel', onWheel, true);
    // captureKeydown is run before original handlers to have a chance to modify the events
    document.addEventListener("keydown", captureKeydown, true);
    // onKeydown handles keypress in the bubbling phase to handle Esc if not handled by the focused element
    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() {
    // Style for materialUI player, video list item, comment highlight:
    // #masthead-container is present on all materialUI pages: index, watch, etc.
    if (document.getElementById("masthead"))
      $(document.head).append(
        '<style name="yt-fix-materialUI type="text/css">\n\
#player-container:focus-within { box-shadow: 0 0 0px 0px rgba(0,0,0,0.8); }\n\
.ytp-probably-keyboard-focus :focus { background-color: rgba(120, 180, 255, 0.6); }\n\
//html:not(.no-focus-outline) ytd-video-primary-info-renderer > #container > #info:focus-within, \n\
//html:not(.no-focus-outline) ytd-video-secondary-info-renderer > #container > #top-row:focus-within, \n\
//html:not(.no-focus-outline) ytd-video-secondary-info-renderer > #container > .description:focus-within \n\
//{ box-shadow: 0 0 10px 0px rgba(0,0,0,0.4); }\n\
html:not(.no-focus-outline) ytd-compact-video-renderer #dismissable:focus-within { box-shadow: 0 0 15px 1px rgba(0,0,100,0.4); }\n\
a.yt-simple-endpoint.ytd-compact-video-renderer { margin-top: 3px; }\n\
</style>'
      );

    // Style for classicUI player, video list item, comment-simplebox highlight and layout rearranging for the highlight:
    // #yt-masthead-container is present on all classicUI pages: index, watch, etc.
    if (document.getElementById("yt-masthead-container"))
      $(document.head).append(
        '<style name="yt-fix-classicUI" type="text/css">\n\
#player-api:focus-within { box-shadow: 0 0 20px 0px rgba(0,0,0,0.8); }\n\
.ytp-probably-keyboard-focus :focus { background-color: rgba(120, 180, 255, 0.6); }\n\
#masthead-search-terms.masthead-search-terms-border:focus-within { border: 1px solid #4d90fe; box-shadow: inset 0px 0px 8px 0px #4d90fe; }\n\
html:not(.no-focus-outline) #watch-header:focus-within, \n\
html:not(.no-focus-outline) #action-panel-details:focus-within, \n\
html:not(.no-focus-outline) #watch-discussion:focus-within \n\
{ box-shadow: 0 0 10px 0px rgba(0,0,0,0.4); }\n\
html:not(.no-focus-outline) .video-list-item:focus-within { box-shadow: 0 0 15px 1px rgba(0,0,100,0.4); }\n\
html:not(.no-focus-outline) .video-list-item:focus-within .related-item-action-menu .yt-uix-button { opacity: 1; }\n\
html:not(.no-focus-outline) .video-list-item:focus-within .video-actions { right: 2px; }\n\
html:not(.no-focus-outline) .video-list-item:focus-within .video-time, \n\
html:not(.no-focus-outline) .related-list-item:focus-within .video-time-overlay { right: -60px; }\n\
#watch7-sidebar-contents { padding-right: 10px; }\n\
#watch7-sidebar-contents .checkbox-on-off { margin-right: 5px; }\n\
#watch7-sidebar .watch-sidebar-head { margin-bottom: 5px; margin-left: 0; }\n\
#watch7-sidebar .watch-sidebar-section { padding-left: 5px; margin-bottom: 0; }\n\
#watch7-sidebar .watch-sidebar-separation-line { margin: 10px 5px; }\n\
.video-list-item .thumb-wrapper { margin: 0; }\n\
.video-list-item { margin-left: 5px; }\n\
.video-list-item .content-wrapper a { padding-top: 3px; min-height: 91px; }\n\
.related-list-item .content-wrapper { margin-left: 176px; margin-right: 5px; }\n\
.related-list-item .related-item-action-menu { top: 3px; right: 0; }\n\
.related-item-dismissable .related-item-action-menu .yt-uix-button { margin: 0; height: 20px; width: 20px; }\n\
</style>'
      );
  }

  function initDom() {
    isMaterialUI = null !== document.getElementById("masthead");
    isClassicUI = null !== document.getElementById("yt-masthead-container");
    // MaterialUI has an extra  #player.skeleton > #player-api element, remnant of the classicUI, different from the one expected here
    // The one with the video:  ytd-watch > #top > #player > #player-container.ytd-watch (> #movie_player.html5-video-player)
    playerContainer = isMaterialUI
      ? document.getElementById("player-container")
      : isClassicUI
      ? document.getElementById("player-api")
      : document.getElementById("player");
    // isEmbeddedUI= !isMaterialUI && !isClassicUI;

    // Areas' root elements
    areaOrder = [null, "player", "masthead", "videos", "content"];
    areaContainers = isMaterialUI
      ? [null, "player-container", "masthead-container", "related", "main"]
      : [
          null,
          "player-api",
          "yt-masthead-container",
          "watch7-sidebar",
          "watch7-content",
        ];

    // Areas' default element to focus
    areaFocusDefault[0] = null;
    areaFocusDefault[1] =
      isMaterialUI || isClassicUI
        ? "#movie_player"
        : "#player .html5-video-player";
    areaFocusDefault[2] = isMaterialUI
      ? "#masthead input#search"
      : "#masthead-search-term";
    areaFocusDefault[3] = isMaterialUI
      ? "#items a.ytd-compact-video-renderer:first()"
      : "#watch7-sidebar-modules a.content-link:first()";
    areaFocusDefault[4] = isMaterialUI
      ? "#info #menu #top-level-buttons button:last()"
      : "#watch8-action-buttons button:first()";
    areaFocusDefault.length = 5;
  }

  function initPlayer() {
    if (playerElem) return;

    // The movie player frame '#movie_player', might not be generated yet.
    playerElem =
      document.getElementById("movie_player") ||
      $("#player .html5-video-player")[0];
    if (!playerElem) {
      console.log(
        "YoutubeKeysFix failed to find '#movie_player' element: not created yet"
      );
      return false;
    }

    //console.log("YoutubeKeysFix: initPlayer()");
    // Movie player frame (element) is focused when loading the page to get movie player keyboard controls.
    if (window.location.pathname === "/watch") playerElem.focus();

    $("#player .caption-window").attr("tabindex", "-1");
    //var caption= playerElem.querySelector && playerElem.querySelector('.caption-window');  if (caption)  caption.setAttribute('tabindex', -1);
  }
})();