GitHub Reveal Header

A userscript that reveals the header when hovering near the top of the screen

  1. // ==UserScript==
  2. // @name GitHub Reveal Header
  3. // @version 0.1.4
  4. // @description A userscript that reveals the header when hovering near the top of the screen
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @match https://github.com/*
  9. // @match https://gist.github.com/*
  10. // @run-at document-idle
  11. // @grant GM_addStyle
  12. // @icon https://github.githubassets.com/pinned-octocat.svg
  13. // @supportURL https://github.com/Mottie/GitHub-userscripts/issues
  14. // ==/UserScript==
  15.  
  16. (() => {
  17. "use strict";
  18.  
  19. const topZone = 75, // px from the top of the viewport (add
  20. transitionDelay = 200, // ms before header hides
  21. revealAttr = "data-reveal-header", // attribute added to minimize redraw
  22.  
  23. // body class names to trigger animation
  24. revealStart = "reveal-header-start",
  25. revealAnimate = "reveal-animated",
  26.  
  27. // selectors from https://github.com/StylishThemes/GitHub-FixedHeader
  28. headers = [
  29. // .header = logged-in
  30. "body{attr}.logged-in .header",
  31. // .site-header = not-logged-in (removed 8/2017)
  32. "{attr} .site-header",
  33. // .Header = logged-in or not-logged-in (added 8/2017)
  34. "{attr} .Header",
  35. // .Header removed, use .js-header-wrapper with header/.Header-old (3/2019)
  36. "{attr} .js-header-wrapper header",
  37. // #com #header = help.github.com
  38. "{attr} #com #header"
  39. // extra
  40. ],
  41. $body = $("body");
  42.  
  43. let timer, timer2;
  44.  
  45. GM_addStyle(`
  46. ${headers.join(",").replace(/\{attr}/g, "")} {
  47. width: 100%;
  48. z-index: 1000;
  49. }
  50. ${headers.join(",").replace(/\{attr}/g, `.${revealAnimate}`)} {
  51. -webkit-transition: transform ease-in ${transitionDelay}ms,
  52. marginTop ease-in ${transitionDelay}ms;
  53. transition: transform ease-in ${transitionDelay}ms,
  54. marginTop ease-in ${transitionDelay}ms;
  55. }
  56. ${headers.join(",").replace(/\{attr}/g, `.${revealStart}`)} {
  57. position: fixed;
  58. transform: translate3d(0, -100%, 0);
  59. }
  60. ${headers.join(",").replace(/\{attr}/g, `[${revealAttr}]`)} {
  61. position: fixed;
  62. transform: translate3d(0, 0%, 0);
  63. }
  64. body.${revealAnimate} {
  65. -webkit-transition: marginTop ease-in ${transitionDelay}ms;
  66. transition: marginTop ease-in ${transitionDelay}ms;
  67. }
  68. `);
  69.  
  70. function getHeader() {
  71. return $(`${headers.join(",").replace(/\{attr}/g, "")}`);
  72. }
  73.  
  74. function getScrollTop() {
  75. // needed for Chrome/Firefox
  76. return window.pageYOffset ||
  77. document.documentElement.scrollTop ||
  78. document.body.scrollTop || 0;
  79. }
  80.  
  81. function onTransitionEnd(el, callback) {
  82. const listener = () => {
  83. callback();
  84. // remove listener after event fired
  85. el.removeEventListener("transitionend", listener);
  86. el.removeEventListener("webkitTransitionEnd", listener);
  87. };
  88. el.addEventListener("transitionend", listener);
  89. el.addEventListener("webkitTransitionEnd", listener);
  90. }
  91.  
  92. // A margin top is needed to prevent
  93. function clearMarginTop() {
  94. const $header = getHeader();
  95. $body.style.marginTop = "";
  96. if ($header) {
  97. $header.style.marginTop = "";
  98. }
  99. }
  100.  
  101. function slideDown(event) {
  102. if (event.clientY < topZone) {
  103. const $header = getHeader();
  104. if ($header) {
  105. onTransitionEnd($header, () => {
  106. $body.classList.remove(revealStart);
  107. });
  108. // add 1px for the border
  109. const headerHeight = ($header.clientHeight + 1) + "px";
  110. $body.style.marginTop = headerHeight;
  111. $header.style.marginTop = "-" + headerHeight;
  112. }
  113.  
  114. // move header to start position instantly
  115. $body.classList.remove(revealAnimate);
  116. $body.classList.add(revealStart);
  117. clearTimeout(timer);
  118. timer = setTimeout(() => {
  119. $body.classList.add(revealAnimate);
  120. $body.setAttribute(revealAttr, true);
  121. }, transitionDelay * 0.2);
  122. }
  123. }
  124.  
  125. function slideUp() {
  126. clearTimeout(timer);
  127. clearTimeout(timer2);
  128. if (getScrollTop() > topZone) {
  129. $body.classList.add(...[revealStart, revealAnimate]);
  130. onTransitionEnd(getHeader(), () => {
  131. $body.classList.remove(...[revealStart, revealAnimate]);
  132. clearMarginTop();
  133. });
  134. } else {
  135. clearMarginTop();
  136. }
  137. $body.removeAttribute(revealAttr);
  138. }
  139.  
  140. function clearTimer() {
  141. clearTimeout(timer);
  142. }
  143.  
  144. function mouseLeave(event) {
  145. clearTimeout(timer);
  146. // don't slideUp when "mouseleave" triggers on children in header
  147. if (event.target === getHeader()) {
  148. timer = setTimeout(() => {
  149. slideUp();
  150. }, transitionDelay * 1.2);
  151. }
  152. }
  153.  
  154. function bindHeader() {
  155. const $header = getHeader();
  156. if ($header) {
  157. $header.removeEventListener("mouseenter", clearTimer);
  158. $header.removeEventListener("mouseleave", mouseLeave);
  159. $header.addEventListener("mouseenter", clearTimer);
  160. $header.addEventListener("mouseleave", mouseLeave);
  161. }
  162. }
  163.  
  164. function init() {
  165. document.addEventListener("mousemove", event => {
  166. if (
  167. event.clientY < topZone &&
  168. getScrollTop() > topZone &&
  169. !$body.hasAttribute(revealAttr)
  170. ) {
  171. clearTimeout(timer);
  172. timer = setTimeout(() => {
  173. slideDown(event);
  174. }, transitionDelay * 0.2);
  175. }
  176. clearTimeout(timer2);
  177. // check location of mouse... if outside of header, slideUp
  178. timer2 = setTimeout(() => {
  179. const el = document.elementFromPoint(event.clientX, event.clientY);
  180. if (
  181. $body.hasAttribute(revealAttr) &&
  182. !closest(`${headers.join(",").replace(/\{attr}/g, "")}`, el)
  183. ) {
  184. slideUp();
  185. }
  186. }, 2000);
  187. });
  188. document.addEventListener("mouseleave", () => {
  189. if ($body.hasAttribute(revealAttr)) {
  190. slideUp();
  191. }
  192. });
  193. bindHeader();
  194. }
  195.  
  196. function $(str, el) {
  197. return (el || document).querySelector(str);
  198. }
  199.  
  200. function closest(selector, el) {
  201. while (el && el.nodeType === 1) {
  202. if (el.matches(selector)) {
  203. return el;
  204. }
  205. el = el.parentNode;
  206. }
  207. return null;
  208. }
  209.  
  210. document.addEventListener("ghmo:container", bindHeader);
  211. init();
  212.  
  213. })();