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