Unfix Fixed Elements

Intelligently reverses ill-conceived element fixing on sites like Medium.com

当前为 2019-05-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Unfix Fixed Elements
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @description Intelligently reverses ill-conceived element fixing on sites like Medium.com
  6. // @author alienfucker
  7. // @match *://*/*
  8. // @grant none
  9. // @noframes
  10. // @run-at document_start
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15. const className = "anti-fixing"; // Odds of colliding with another class must be low
  16.  
  17. class FixedWatcher {
  18. constructor() {
  19. this.watcher = new MutationObserver(this.onMutation.bind(this));
  20. this.elementTypes = ["div", "header", "footer", "nav"];
  21. this.awaitingTick = false;
  22. this.top = [];
  23. this.bottom = [];
  24. this.processedMiddle = false;
  25. }
  26.  
  27. start() {
  28. this.trackAll();
  29. this.watcher.observe(document, {
  30. childList: true,
  31. attributes: true,
  32. subtree: true,
  33. attributeFilter: ["class", "style"],
  34. attributeOldValue: true
  35. });
  36. window.addEventListener("scroll", this.onScroll.bind(this));
  37. }
  38. onScroll(){
  39. if(this.awaitingTick) return;
  40. this.awaitingTick = true;
  41. window.requestAnimationFrame(() => {
  42. const max = document.body.scrollHeight - window.innerHeight;
  43. const y = window.scrollY;
  44.  
  45. for(const item of this.top){
  46. item.className = item.el.className;
  47. if(y === 0){
  48. item.el.classList.remove(className);
  49. }else if(!item.el.classList.contains(className)){
  50. item.el.classList.add(className);
  51. }
  52. }
  53.  
  54. for(const item of this.bottom){
  55. item.className = item.el.className;
  56. if(y === max){
  57. item.el.classList.remove(className);
  58. }else if(!item.el.classList.contains(className)){
  59. item.el.classList.add(className);
  60. }
  61. }
  62. this.awaitingTick = false;
  63. })
  64. }
  65. onMutation(mutations) {
  66. for (let mutation of mutations) {
  67. if (mutation.type === "childList") {
  68. for(let node of mutation.removedNodes)
  69. this.untrack(node)
  70. for (let node of mutation.addedNodes) {
  71. if (node.nodeType !== Node.ELEMENT_NODE) continue;
  72.  
  73. if (this.elementTypes.findIndex(selector => node.matches(selector)) !== -1) this.track(node);
  74. node.querySelectorAll(this.elementTypes.join(",")).forEach(el => this.track(el));
  75. }
  76. } else if (mutation.type === "attributes") {
  77. if (this.friendlyMutation(mutation)) continue;
  78.  
  79.  
  80. if (this.elementTypes.findIndex(selector => mutation.target.matches(selector)) !== -1) {
  81. this.track(mutation.target);
  82. }
  83. }
  84. }
  85.  
  86. }
  87.  
  88. friendlyMutation(mutation){ // Mutation came from us
  89. if(mutation.attributeName === "class"){
  90. if(this.top.findIndex(({el, className}) => el === mutation.target && className === mutation.oldValue) !== -1) return true;
  91. if(this.bottom.findIndex(({el, className}) => el === mutation.target && className === mutation.oldValue) !== -1) return true;
  92. }
  93. return false;
  94. }
  95. untrack(_el){
  96. let i = this.top.findIndex(({el}) => el.isSameNode(_el) || _el.contains(el));
  97. if(i !== -1) return !!this.top.splice(i, 1);
  98. i = this.bottom.findIndex(({el}) => el.isSameNode(_el) || _el.contains(el));
  99. if(i !== -1) return !!this.bottom.splice(i, 1);
  100. return false;
  101. }
  102. trackAll(){
  103. const els = document.querySelectorAll(this.elementTypes.join(","));
  104. for(const el of els)
  105. this.track(el);
  106. }
  107. getClassAttribs(el){
  108. // Last-ditch effort to help figure out if the developer intended the fixed element to be fullscreen
  109. // i.e. explicitly defined both the top and bottom rules. If they did, then we leave the element alone.
  110. // Unfortunately, we can't get this info from .style or computedStyle, since .style only
  111. // applies when the rules are added directly to the element, and computedStyle automatically generates a value
  112. // for top/bottom if the opposite is set. Leaving us no way to know if the developer actually set the other value.
  113. const rules = [];
  114. for(const styleSheet of document.styleSheets){
  115. try{
  116. for(const rule of styleSheet.rules){
  117. if(el.matches(rule.selectorText)){
  118. rules.push({height: rule.style.height, top: rule.style.top, bottom: rule.style.bottom});
  119. }
  120. }
  121. }catch(e) {
  122. continue;
  123. }
  124. }
  125.  
  126. return rules.reduce((current, next) => ({
  127. height: next.height || current.height,
  128. top: next.top || current.top,
  129. bottom: next.bottom || current.bottom
  130. }),{
  131. height: "",
  132. top: "",
  133. bottom: ""
  134. });
  135. }
  136.  
  137. isAutoBottom(el, style){
  138. if(style.bottom === "auto") return true;
  139. if(style.bottom === "0px") return false;
  140. if(el.style.bottom.length) return false;
  141. const {height, bottom} = this.getClassAttribs(el);
  142.  
  143. if(height === "100%" || bottom.length) return false;
  144.  
  145. return true;
  146. }
  147. isAutoTop(el, style){
  148. if(style.top === "auto") return true;
  149. if(style.top === "0px") return false;
  150. if(el.style.top.length) return false;
  151. const {height, top} = this.getClassAttribs(el);
  152.  
  153. if(height === "100%" || top.length) return false;
  154.  
  155. return true;
  156. }
  157. topTracked(el){
  158. return this.top.findIndex(({el: _el}) => _el === el) !== -1
  159. }
  160. bottomTracked(el){
  161. return this.bottom.findIndex(({el: _el}) => _el === el) !== -1
  162. }
  163. track(el){
  164.  
  165. const style = window.getComputedStyle(el);
  166.  
  167. if (style.position === "fixed" || style.position === "sticky") {
  168. console.log(el, style.top === "0px", style.top.indexOf("-") === 0, !this.topTracked(el), this.isAutoBottom(el, style));
  169. if((style.top === "0px" || style.top.indexOf("-") === 0) && !this.topTracked(el) && this.isAutoBottom(el, style)){
  170. this.top.push({el, className: el.className});
  171. this.onScroll();
  172. }else if((style.bottom === "0px" || style.bottom.indexOf("-") === 0) && !this.bottomTracked(el) && this.isAutoTop(el, style)){
  173. this.bottom.push({el, className: el.className});
  174. this.onScroll();
  175. }
  176. }
  177. }
  178.  
  179. stop() {
  180. this.watcher.disconnect();
  181. window.removeEventListener("scroll", this.onScroll.bind(this));
  182. }
  183.  
  184. restore() {
  185. let els = document.querySelectorAll("." + className);
  186. for (let el of els) {
  187. el.classList.remove(className);
  188. }
  189. }
  190.  
  191. }
  192.  
  193. document.documentElement.appendChild((() => {
  194. let el = document.createElement("style");
  195. el.setAttribute("type", "text/css");
  196. el.appendChild(document.createTextNode(`.${className}{ display: none !important }`));
  197. //el.appendChild(document.createTextNode(`.${className}{ position: static !important }`));
  198. return el;
  199. })())
  200. window.addEventListener("keypress", e => {
  201. if(e.key === "F"){
  202. if(window.fixer){
  203. console.log("Removing fixer");
  204. fixer.stop();
  205. fixer.restore();
  206. window.fixer = null;
  207. }else{
  208. console.log("Adding fixer");
  209. fixer = new FixedWatcher();
  210. fixer.start();
  211. window.fixer = fixer;
  212. }
  213. }
  214. });
  215. let fixer = new FixedWatcher();
  216. fixer.start();
  217.  
  218. // Make globally accessible, for debugging purposes
  219. window.fixer = fixer;
  220. })()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址