// ==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();
});
});
})();