// ==UserScript==
// @name GitHub Reveal Header
// @version 0.1.4
// @description A userscript that reveals the header when hovering near the top of the screen
// @license MIT
// @author Rob Garrison
// @namespace https://github.com/Mottie
// @match https://github.com/*
// @match https://gist.github.com/*
// @run-at document-idle
// @grant GM_addStyle
// @icon https://github.githubassets.com/pinned-octocat.svg
// @supportURL https://github.com/Mottie/GitHub-userscripts/issues
// ==/UserScript==
(() => {
"use strict";
const topZone = 75, // px from the top of the viewport (add
transitionDelay = 200, // ms before header hides
revealAttr = "data-reveal-header", // attribute added to minimize redraw
// body class names to trigger animation
revealStart = "reveal-header-start",
revealAnimate = "reveal-animated",
// selectors from https://github.com/StylishThemes/GitHub-FixedHeader
headers = [
// .header = logged-in
"body{attr}.logged-in .header",
// .site-header = not-logged-in (removed 8/2017)
"{attr} .site-header",
// .Header = logged-in or not-logged-in (added 8/2017)
"{attr} .Header",
// .Header removed, use .js-header-wrapper with header/.Header-old (3/2019)
"{attr} .js-header-wrapper header",
// #com #header = help.github.com
"{attr} #com #header"
// extra
],
$body = $("body");
let timer, timer2;
GM_addStyle(`
${headers.join(",").replace(/\{attr}/g, "")} {
width: 100%;
z-index: 1000;
}
${headers.join(",").replace(/\{attr}/g, `.${revealAnimate}`)} {
-webkit-transition: transform ease-in ${transitionDelay}ms,
marginTop ease-in ${transitionDelay}ms;
transition: transform ease-in ${transitionDelay}ms,
marginTop ease-in ${transitionDelay}ms;
}
${headers.join(",").replace(/\{attr}/g, `.${revealStart}`)} {
position: fixed;
transform: translate3d(0, -100%, 0);
}
${headers.join(",").replace(/\{attr}/g, `[${revealAttr}]`)} {
position: fixed;
transform: translate3d(0, 0%, 0);
}
body.${revealAnimate} {
-webkit-transition: marginTop ease-in ${transitionDelay}ms;
transition: marginTop ease-in ${transitionDelay}ms;
}
`);
function getHeader() {
return $(`${headers.join(",").replace(/\{attr}/g, "")}`);
}
function getScrollTop() {
// needed for Chrome/Firefox
return window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop || 0;
}
function onTransitionEnd(el, callback) {
const listener = () => {
callback();
// remove listener after event fired
el.removeEventListener("transitionend", listener);
el.removeEventListener("webkitTransitionEnd", listener);
};
el.addEventListener("transitionend", listener);
el.addEventListener("webkitTransitionEnd", listener);
}
// A margin top is needed to prevent
function clearMarginTop() {
const $header = getHeader();
$body.style.marginTop = "";
if ($header) {
$header.style.marginTop = "";
}
}
function slideDown(event) {
if (event.clientY < topZone) {
const $header = getHeader();
if ($header) {
onTransitionEnd($header, () => {
$body.classList.remove(revealStart);
});
// add 1px for the border
const headerHeight = ($header.clientHeight + 1) + "px";
$body.style.marginTop = headerHeight;
$header.style.marginTop = "-" + headerHeight;
}
// move header to start position instantly
$body.classList.remove(revealAnimate);
$body.classList.add(revealStart);
clearTimeout(timer);
timer = setTimeout(() => {
$body.classList.add(revealAnimate);
$body.setAttribute(revealAttr, true);
}, transitionDelay * 0.2);
}
}
function slideUp() {
clearTimeout(timer);
clearTimeout(timer2);
if (getScrollTop() > topZone) {
$body.classList.add(...[revealStart, revealAnimate]);
onTransitionEnd(getHeader(), () => {
$body.classList.remove(...[revealStart, revealAnimate]);
clearMarginTop();
});
} else {
clearMarginTop();
}
$body.removeAttribute(revealAttr);
}
function clearTimer() {
clearTimeout(timer);
}
function mouseLeave(event) {
clearTimeout(timer);
// don't slideUp when "mouseleave" triggers on children in header
if (event.target === getHeader()) {
timer = setTimeout(() => {
slideUp();
}, transitionDelay * 1.2);
}
}
function bindHeader() {
const $header = getHeader();
if ($header) {
$header.removeEventListener("mouseenter", clearTimer);
$header.removeEventListener("mouseleave", mouseLeave);
$header.addEventListener("mouseenter", clearTimer);
$header.addEventListener("mouseleave", mouseLeave);
}
}
function init() {
document.addEventListener("mousemove", event => {
if (
event.clientY < topZone &&
getScrollTop() > topZone &&
!$body.hasAttribute(revealAttr)
) {
clearTimeout(timer);
timer = setTimeout(() => {
slideDown(event);
}, transitionDelay * 0.2);
}
clearTimeout(timer2);
// check location of mouse... if outside of header, slideUp
timer2 = setTimeout(() => {
const el = document.elementFromPoint(event.clientX, event.clientY);
if (
$body.hasAttribute(revealAttr) &&
!closest(`${headers.join(",").replace(/\{attr}/g, "")}`, el)
) {
slideUp();
}
}, 2000);
});
document.addEventListener("mouseleave", () => {
if ($body.hasAttribute(revealAttr)) {
slideUp();
}
});
bindHeader();
}
function $(str, el) {
return (el || document).querySelector(str);
}
function closest(selector, el) {
while (el && el.nodeType === 1) {
if (el.matches(selector)) {
return el;
}
el = el.parentNode;
}
return null;
}
document.addEventListener("ghmo:container", bindHeader);
init();
})();