Better Keyboard Shortcuts for SharePoint

Simpler keyboard shorcuts with visual indicators for Microsoft SharePoint videos

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Better Keyboard Shortcuts for SharePoint
// @namespace    http://tampermonkey.net/
// @version      1.3.0
// @description  Simpler keyboard shorcuts with visual indicators for Microsoft SharePoint videos
// @author       kazcfz
// @include      https://*.sharepoint.com/*
// @icon         https://res-1.cdn.office.net/shellux/stream_24x.12dba766a9c30382b781c971070dc87c.svg
// @grant        none
// @license      MIT
// ==/UserScript==

/* eslint curly: "off" */

(function () {
    'use strict';

    // Wait until .oneplayer-root exists in the DOM, then run callback with it
    function waitForOnePlayerRoot(callback) {
        const root = document.querySelector('.oneplayer-root') || document.querySelector('.OnePlayer-container');
        if (root) {
            callback(root);
            return;
        }
        const docObserver = new MutationObserver((mutations, obs) => {
            const el = document.querySelector('.oneplayer-root') || document.querySelector('.OnePlayer-container');
            if (el) {
                obs.disconnect();
                callback(el);
            }
        });
        docObserver.observe(document.body || document.documentElement, { childList: true, subtree: true });
    }

    waitForOnePlayerRoot(videoRoot => {
        if (getComputedStyle(videoRoot).position === 'static')
            videoRoot.style.position = 'relative';

        // Overlay to indicate Skip
        const skipOverlay = document.createElement('div');
        skipOverlay.style.position = 'absolute';
        skipOverlay.style.top = '50%';
        skipOverlay.style.transform = 'translateY(-50%)';
        skipOverlay.style.padding = '15px 15px';
        skipOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.9)';
        skipOverlay.style.borderRadius = '50%';
        skipOverlay.style.pointerEvents = 'none';
        skipOverlay.style.opacity = '0';
        skipOverlay.style.transition = 'opacity 0.1s ease';
        skipOverlay.style.zIndex = '9999';
        skipOverlay.style.userSelect = 'none';
        skipOverlay.style.display = 'flex';
        skipOverlay.style.flexDirection = 'column';
        skipOverlay.style.alignItems = 'center';
        skipOverlay.style.justifyContent = 'center';
        skipOverlay.style.gap = '0.2em';
        skipOverlay.style.minWidth = '75px';
        skipOverlay.style.minHeight = '75px';
        skipOverlay.style.textAlign = 'center';
        videoRoot.appendChild(skipOverlay);

        // Overlay to indicate Volume
        const topMiddleOverlay = document.createElement('div');
        topMiddleOverlay.style.position = 'absolute';
        topMiddleOverlay.style.top = '10%';
        topMiddleOverlay.style.left = '0';
        topMiddleOverlay.style.right = '0';
        topMiddleOverlay.style.margin = 'auto';
        topMiddleOverlay.style.padding = '7px 11px';
        topMiddleOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
        topMiddleOverlay.style.borderRadius = '5px';
        topMiddleOverlay.style.pointerEvents = 'none';
        topMiddleOverlay.style.opacity = '0';
        topMiddleOverlay.style.transition = 'opacity 0.015s ease';
        topMiddleOverlay.style.zIndex = '9999';
        topMiddleOverlay.style.userSelect = 'none';
        topMiddleOverlay.style.display = 'flex';
        topMiddleOverlay.style.flexDirection = 'column';
        topMiddleOverlay.style.alignItems = 'center';
        topMiddleOverlay.style.justifyContent = 'center';
        topMiddleOverlay.style.gap = '0.2em';
        topMiddleOverlay.style.width = '50px';
        topMiddleOverlay.style.minHeight = '30px';
        topMiddleOverlay.style.textAlign = 'center';
        videoRoot.appendChild(topMiddleOverlay);

        // Overlay to indicate Play/Pause
        const centerOverlay = document.createElement('div');
        centerOverlay.style.position = 'absolute';
        centerOverlay.style.top = '50%';
        centerOverlay.style.left = '50%';
        centerOverlay.style.right = 'auto';
        centerOverlay.style.margin = '0';
        centerOverlay.style.transform = 'translate(-50%, -50%)';
        centerOverlay.style.padding = '15px';
        centerOverlay.style.fontVariantEmoji = 'text';
        centerOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
        centerOverlay.style.borderRadius = '50%';
        centerOverlay.style.pointerEvents = 'none';
        centerOverlay.style.opacity = '0';
        centerOverlay.style.zIndex = '9999';
        centerOverlay.style.userSelect = 'none';
        centerOverlay.style.display = 'flex';
        centerOverlay.style.flexDirection = 'column';
        centerOverlay.style.alignItems = 'center';
        centerOverlay.style.justifyContent = 'center';
        centerOverlay.style.textAlign = 'center';
        centerOverlay.style.color = 'rgba(255,255,255,0.85)';
        videoRoot.appendChild(centerOverlay);

        const centerSymbol = document.createElement('span');
        centerSymbol.style.opacity = '1';
        centerOverlay.appendChild(centerSymbol);

        // Overlay text for current volume
        const topMiddleText = document.createElement('div');
        topMiddleText.style.fontSize = '17px';
        topMiddleText.style.fontWeight = '500';
        topMiddleText.style.color = 'rgba(255,255,255,0.85)';
        topMiddleText.style.userSelect = 'none';
        topMiddleOverlay.appendChild(topMiddleText);

        // Overlay container for left/right triangles
        const trianglesContainer = document.createElement('div');
        trianglesContainer.style.display = 'flex';
        trianglesContainer.style.gap = '10px';
        skipOverlay.appendChild(trianglesContainer);

        // Add triangles into its container
        const triangles = [];
        for (let i = 0; i < 3; i++) {
            const tri = document.createElement('span');
            tri.textContent = '▶';
            tri.style.fontSize = '13px';
            tri.style.fontWeight = 'bold';
            trianglesContainer.appendChild(tri);
            triangles.push(tri);
        }

        // Overlay text for seconds skipped
        const secondsText = document.createElement('div');
        secondsText.style.fontSize = '14px';
        secondsText.style.fontWeight = 'normal';
        secondsText.style.color = 'rgba(255,255,255,0.85)';
        secondsText.style.userSelect = 'none';
        skipOverlay.appendChild(secondsText);

        let hideTimeoutSkipOverlay;
        let hideTimeoutTopMiddleOverlay;
        let hideTimeoutCenterOverlay;
        let animTimeouts = [];

        // Displays overlay triangle animation and seconds skipped
        const secondsToSkip = 5;
        function showAnimatedTriangles(side) {
            // Clear animation timers
            clearTimeout(hideTimeoutSkipOverlay);
            animTimeouts.forEach(t => clearTimeout(t));
            animTimeouts = [];

            const isLeft = side === 'left';
            const char = isLeft ? '◀' : '▶';
            const order = isLeft ? [2, 1, 0] : [0, 1, 2];

            triangles.forEach(t => {
                t.textContent = char;
                t.style.transition = 'none';
                t.style.color = 'rgba(255,255,255,0.3)';
            });

            // Force style flush to apply the color immediately before re-enabling transitions
            void skipOverlay.offsetHeight;
            triangles.forEach(t => { t.style.transition = 'color 0.3s ease'; });

            secondsText.textContent = `${secondsToSkip} seconds`;

            skipOverlay.style.opacity = '1';
            skipOverlay.style.left = isLeft ? '10%' : 'auto';
            skipOverlay.style.right = isLeft ? 'auto' : '10%';
            skipOverlay.style.textAlign = 'center';

            triangles[order[0]].style.color = 'rgba(255,255,255,0.75)';

            const interval = 200;

            for (let step = 2; step <= 3; step++) {
                animTimeouts.push(setTimeout(() => {
                    if (step === 2) {
                        triangles[order[0]].style.color = 'rgba(255,255,255,0.5)';
                        triangles[order[1]].style.color = 'rgba(255,255,255,0.75)';
                    }
                    else if (step === 3) {
                        triangles[order[0]].style.color = 'rgba(255,255,255,0.3)';
                        triangles[order[1]].style.color = 'rgba(255,255,255,0.5)';
                        triangles[order[2]].style.color = 'rgba(255,255,255,0.75)';
                    }
                }, (step - 1) * interval));
            }

            hideTimeoutSkipOverlay = setTimeout(() => { skipOverlay.style.opacity = '0'; }, 3.3 * interval);
        }

        // Checks that the video player is focused
        function isInOnePlayerRoot(element) {
            while (element) {
                if (element === videoRoot)
                    return true;
                element = element.parentElement;
            }
            return false;
        }

        let video = null;

        function setupVideo(v) {
            if (!v || video === v)
                return;
            video = v;
        }

        // Initial attempt to find video
        setupVideo(videoRoot.querySelector('video'));

        // Observe videoRoot subtree for added/removed video
        const observer = new MutationObserver(mutations => {
            for (const mutation of mutations)
                for (const node of mutation.addedNodes)
                    if (node.nodeType === 1)
                        if (node.tagName === 'VIDEO')
                            setupVideo(node);
                        else {
                            const v = node.querySelector && node.querySelector('video');
                            if (v)
                                setupVideo(v);
                        }
        });

        observer.observe(videoRoot, { childList: true, subtree: true });


        function triggerCenterSymbol(symbol) {
            clearTimeout(hideTimeoutCenterOverlay);

            centerOverlay.style.width = '75px';
            centerOverlay.style.height = '75px';
            centerOverlay.style.transition = 'opacity 0.1s ease';
            centerOverlay.style.opacity = '1';

            hideTimeoutCenterOverlay = setTimeout(() => {
                centerOverlay.style.transition = 'width 0.75s ease, height 0.75s ease, font-size 0.75s ease, opacity 0.75s ease';
                centerOverlay.style.width = `${parseFloat(getComputedStyle(centerOverlay).width) + 25}px`;
                centerOverlay.style.height = `${parseFloat(getComputedStyle(centerOverlay).height) + 25}px`;
                centerOverlay.style.fontSize = `${parseFloat(getComputedStyle(centerOverlay).fontSize) + 22.5}px`;
                centerOverlay.style.opacity = '0';
            }, 50);

            centerSymbol.textContent = symbol;
            if (symbol === '🔊' || symbol === '🔇') {
                centerSymbol.style.marginBottom = '9px';
                centerSymbol.style.marginLeft = '5px';
                centerOverlay.style.fontSize = '60px';
            } else if (symbol === '🔉') {
                centerSymbol.style.marginBottom = '9px';
                centerSymbol.style.marginLeft = '0px';
                centerOverlay.style.fontSize = '60px';
            } else if (symbol === '▶') {
                centerSymbol.style.marginBottom = '7px';
                centerSymbol.style.marginLeft = '12px';
                centerOverlay.style.fontSize = '50px';
            } else if (symbol === '⏸') {
                centerSymbol.style.marginBottom = '14px';
                centerSymbol.style.marginLeft = '4px';
                centerOverlay.style.fontSize = '55px';
            }
        }


        // Math time: MS Stream/SharePoint handles volume steps exponentially (cubically) rather than linearly
        // The sequence is (steps of 0.1)^3, counting down from 1 to 0:
        // volume (base 𝑛) = (1 − 0.1 × 𝑛)^3, where 𝑛 = 0, 1, 2, ..., 10.
        // Alternatively, could just keep it to linear volume steps
        let currentVolumeStepCount = 0;
        function triggerTopMiddleText(action) {
            if (action == 'volume') {
                const volumePercent = video.volume * 100;
                if (volumePercent >= 1 || video.volume == 0)
                    topMiddleText.textContent = `${Math.round(volumePercent)}%`;
                else
                    topMiddleText.textContent = `${volumePercent.toFixed(1)}%`;

            } else if (action == 'speed') {
                const speed = video.playbackRate;
                topMiddleText.textContent = (speed % 1 === 0) ? `${speed}x` : `${speed.toFixed(2)}x`;
                if (topMiddleText.textContent.endsWith('0x'))
                    topMiddleText.textContent = `${speed.toFixed(1)}x`;
            }

            topMiddleOverlay.style.opacity = '1';
            clearTimeout(hideTimeoutTopMiddleOverlay);
            hideTimeoutTopMiddleOverlay = setTimeout(() => { topMiddleOverlay.style.opacity = '0'; }, 500);
        }

        document.addEventListener('keydown', e => {
            if (!video)
                return;
            if (e.altKey || e.ctrlKey || e.metaKey)
                return;
            if (document.activeElement && !isInOnePlayerRoot(document.activeElement))
                return;

            e.preventDefault();

            // Home to jump to start
            if (e.code === 'Home')
                video.currentTime = 0;

            // End to jump to end
            else if (e.code === 'End')
                video.currentTime = video.duration;

            // Period to skip to next frame
            else if (e.code === 'Period' && !e.shiftKey)
                video.currentTime = Math.min(video.duration, video.currentTime + (1/30));

            // Comma to skip to previous frame
            else if (e.code === 'Comma' && !e.shiftKey)
                video.currentTime = Math.max(0, video.currentTime - (1/30));

            // > to speed up
            else if (e.key === '>') {
                document.querySelector('[aria-description*="Alt + X"]').click();
                const items = [...document.querySelectorAll('[role="menuitemradio"]')];
                const currentIndex = items.findIndex(item => item.getAttribute('aria-checked') === 'true');

                if (currentIndex > 0)
                    items[currentIndex - 1].click();
                else
                    document.querySelector('[aria-description*="Alt + X"]').click();
                triggerTopMiddleText('speed');
            }

            // < to slow down
            else if (e.key === '<') {
                document.querySelector('[aria-description*="Alt + Z"]').click();
                const items = [...document.querySelectorAll('[role="menuitemradio"]')];
                const currentIndex = items.findIndex(item => item.getAttribute('aria-checked') === 'true');

                if (currentIndex < items.length - 1)
                    items[currentIndex + 1].click();
                else
                    document.querySelector('[aria-description*="Alt + Z"]').click();
                triggerTopMiddleText('speed');
            }

            // 0–9 to skip to 0%–90% of video
            else if (/^Digit[0-9]$/.test(e.code))
                video.currentTime = (video.duration * (parseInt(e.code.replace('Digit', ''), 10))) / 10;

            // → to skip forward
            else if (e.code === 'ArrowRight') {
                video.currentTime = Math.min(video.duration, video.currentTime + secondsToSkip);
                showAnimatedTriangles('right');

            // ← to skip backward
            } else if (e.code === 'ArrowLeft') {
                video.currentTime = Math.max(0, video.currentTime - secondsToSkip);
                showAnimatedTriangles('left');

            // ↑ to increase volume
            } else if (e.code === 'ArrowUp') {
                if (currentVolumeStepCount > 0) {
                    currentVolumeStepCount--;
                    video.volume = Math.pow(1 - 0.1 * currentVolumeStepCount, 3);
                    //video.volume = Math.max(0, Math.min(1, Math.round((video.volume + 0.1) * 100) / 100));
                }
                triggerTopMiddleText('volume');
                triggerCenterSymbol('🔊');

            // ↓ to decrease volume
            } else if (e.code === 'ArrowDown') {
                if (currentVolumeStepCount < 10) {
                    currentVolumeStepCount++;
                    video.volume = Math.pow(1 - 0.1 * currentVolumeStepCount, 3);
                    //video.volume = Math.max(0, Math.min(1, Math.round((video.volume - 0.1) * 100) / 100));
                }
                triggerTopMiddleText('volume');

                if (video.volume == 0)
                    triggerCenterSymbol('🔇');
                else
                    triggerCenterSymbol('🔉');

            // / to go to search box
            } else if (e.key === '/') {
                const searchInput = document.querySelector('input[role="combobox"][type="search"][placeholder="Search"]');
                searchInput.focus();
                searchInput.select();
                return;

            // Trigger SharePoint's keyboard shortcuts / advanced features
            } else {
                // Adapted from [Sharepoint Keyboard Shortcuts] by [CyrilSLi], MIT License
                // https://greatest.deepsurf.us/en/scripts/524190-sharepoint-keyboard-shortcuts
                const keys = {
                    Space: document.querySelector('[aria-description*="Alt + K"]'),
                    KeyK: document.querySelector('[aria-description*="Alt + K"]'),
                    KeyJ: document.querySelector('[aria-description*="Alt + J"]'),
                    KeyL: document.querySelector('[aria-description*="Alt + L"]'),
                    KeyF: document.querySelector('[aria-description*="Alt + Enter"]'),
                    KeyM: document.querySelector('[aria-description*="Alt + M"]'),
                    KeyC: document.querySelector('[aria-description*="Alt + C"]'),
                    KeyA: document.querySelector('[aria-description*="Alt + A"]'),
                }

                if (keys.hasOwnProperty(e.code))
                    keys[e.code].click();


                if (e.code === 'Space' || e.code === 'KeyK') {
                    if (video.paused)
                        triggerCenterSymbol('⏸');
                    else
                        triggerCenterSymbol('▶');

                } else if (e.code === 'KeyC') {
                    const items = [...document.querySelectorAll('[role="menuitemradio"]')];
                    const menuitem = document.querySelector('button[role="menuitem"][aria-label="Captions"]');
                    const currentIndex = items.findIndex(item => item.getAttribute('aria-checked') === 'true');

                    if (currentIndex >= items.length - 1) {
                        items[0].click();
                        menuitem.setAttribute('aria-checked', 'false');

                    } else if (currentIndex < items.length - 1) {
                        items[currentIndex + 1].click();
                        menuitem.setAttribute('aria-checked', 'true');
                    }

                } else if (e.code === 'KeyM')
                    if (video.muted)
                        triggerCenterSymbol('🔊');
                    else
                        triggerCenterSymbol('🔇');
            }

            document.querySelector(".fluent-critical-ui-container").focus();
        });

    });
})();