- // ==UserScript==
- // @name Unfix Fixed Elements
- // @namespace http://tampermonkey.net/
- // @version 1.4
- // @description Intelligently reverses ill-conceived element fixing on sites like Medium.com
- // @author reagent
- // @match *://*/*
- // @noframes
- // @grant none
- // @run-at document_start
- // ==/UserScript==
-
- (function () {
- 'use strict';
- const className = "anti-fixing"; // Odds of colliding with another class must be low
- const inlineElements = [ // Non-block elements (along with html & body) which we will ignore
- "html", "script", "head", "meta", "title", "style", "script", "body",
- "a", "b", "label", "form", "abbr", "legend", "address", "link",
- "area", "mark", "audio", "meter", "b", "nav", "cite", "optgroup",
- "code", "option", "del", "q", "details", "small", "dfn", "select",
- "command", "source", "datalist", "span", "em", "strong", "font",
- "sub", "i", "summary", "iframe", "sup", "img", "tbody", "input",
- "td", "ins", "time", "kbd", "var"
- ];
-
- const fullBlockSelector = inlineElements.map(tag => ":not(" + tag + ")").join("");
- const ltdBlockSelector = "div,header,footer,nav";
-
- class FixedWatcher {
- constructor(thorough = false) {
-
- this.watcher = new MutationObserver(this.onMutation.bind(this));
- this.selector = thorough ? fullBlockSelector : ltdBlockSelector;
- this.awaitingTick = false;
- this.top = [];
- this.bottom = [];
- }
-
- start() {
- this.trackAll();
- this.watcher.observe(document, {
- childList: true,
- attributes: true,
- subtree: true,
- attributeFilter: ["class", "style"],
- attributeOldValue: true
- });
- window.addEventListener("scroll", this.onScroll.bind(this));
- }
- onScroll(){
- if(this.awaitingTick) return;
- this.awaitingTick = true;
- window.requestAnimationFrame(() => {
- const max = document.body.scrollHeight - window.innerHeight;
- const y = window.scrollY;
-
- for(const item of this.top){
- item.className = item.el.className;
- if(y === 0){
- item.el.classList.remove(className);
- }else if(!item.el.classList.contains(className)){
- item.el.classList.add(className);
- }
- }
-
- for(const item of this.bottom){
- item.className = item.el.className;
- if(y === max){
- item.el.classList.remove(className);
- }else if(!item.el.classList.contains(className)){
- item.el.classList.add(className);
- }
- }
- this.awaitingTick = false;
- })
- }
- onMutation(mutations) {
- for (let mutation of mutations) {
- if (mutation.type === "childList") {
- for(let node of mutation.removedNodes)
- this.untrack(node)
- for (let node of mutation.addedNodes) {
- if (node.nodeType !== Node.ELEMENT_NODE) continue;
-
- if (node.matches(this.selector)) this.track(node);
- node.querySelectorAll(this.selector).forEach(el => this.track(el));
- }
- } else if (mutation.type === "attributes") {
- if (this.friendlyMutation(mutation)) continue;
-
-
- if (mutation.target.matches(this.selector)) {
- this.track(mutation.target);
- }
- }
- }
-
- }
-
- friendlyMutation(mutation){ // Mutation came from us
- if(mutation.attributeName === "class"){
- if(this.top.findIndex(({el, className}) => el === mutation.target && className === mutation.oldValue) !== -1) return true;
- if(this.bottom.findIndex(({el, className}) => el === mutation.target && className === mutation.oldValue) !== -1) return true;
- }
- return false;
- }
- untrack(_el){
- let i = this.top.findIndex(({el}) => el.isSameNode(_el) || _el.contains(el));
- if(i !== -1) return !!this.top.splice(i, 1);
- i = this.bottom.findIndex(({el}) => el.isSameNode(_el) || _el.contains(el));
- if(i !== -1) return !!this.bottom.splice(i, 1);
- return false;
- }
- trackAll(){
- const els = document.querySelectorAll(this.selector);
- for(const el of els)
- this.track(el);
- }
- getClassAttribs(el){
- // Last-ditch effort to help figure out if the developer intended the fixed element to be fullscreen
- // i.e. explicitly defined both the top and bottom rules. If they did, then we leave the element alone.
- // Unfortunately, we can't get this info from .style or computedStyle, since .style only
- // applies when the rules are added directly to the element, and computedStyle automatically generates a value
- // for top/bottom if the opposite is set. Leaving us no way to know if the developer actually set the other value.
- const rules = [];
- for(const styleSheet of document.styleSheets){
- try{
- for(const rule of styleSheet.rules){
- if(el.matches(rule.selectorText)){
- rules.push({height: rule.style.height, top: rule.style.top, bottom: rule.style.bottom});
- }
- }
- }catch(e) {
- continue;
- }
- }
-
- return rules.reduce((current, next) => ({
- height: next.height || current.height,
- top: next.top || current.top,
- bottom: next.bottom || current.bottom
- }),{
- height: "",
- top: "",
- bottom: ""
- });
- }
-
- isAutoBottom(el, style){
- if(style.bottom === "auto") return true;
- if(style.bottom === "0px") return false;
- if(el.style.bottom.length) return false;
- const {height, bottom} = this.getClassAttribs(el);
-
- if(height === "100%" || bottom.length) return false;
-
- return true;
- }
- isAutoTop(el, style){
- if(style.top === "auto") return true;
- if(style.top === "0px") return false;
- if(el.style.top.length) return false;
- const {height, top} = this.getClassAttribs(el);
-
- if(height === "100%" || top.length) return false;
-
- return true;
- }
- topTracked(el){
- return this.top.findIndex(({el: _el}) => _el === el) !== -1
- }
- bottomTracked(el){
- return this.bottom.findIndex(({el: _el}) => _el === el) !== -1
- }
- track(el){
-
- const style = window.getComputedStyle(el);
-
- if (style.position === "fixed" || style.position === "sticky") {
- if((style.top === "0px" || style.top.indexOf("-") === 0) && !this.topTracked(el) && this.isAutoBottom(el, style)){
- this.top.push({el, className: el.className});
- this.onScroll();
- }else if((style.bottom === "0px" || style.bottom.indexOf("-") === 0) && !this.bottomTracked(el) && this.isAutoTop(el, style)){
- this.bottom.push({el, className: el.className});
- this.onScroll();
- }
- }
- }
-
- stop() {
- this.watcher.disconnect();
- window.removeEventListener("scroll", this.onScroll.bind(this));
- }
-
- restore() {
- let els = document.querySelectorAll("." + className);
- for (let el of els) {
- el.classList.remove(className);
- }
- }
-
- }
-
- document.documentElement.appendChild((() => {
- let el = document.createElement("style");
- el.setAttribute("type", "text/css");
- el.appendChild(document.createTextNode(`.${className}{ display: none !important }`));
- //el.appendChild(document.createTextNode(`.${className}{ position: static !important }`));
- return el;
- })())
- window.addEventListener("keydown", e => {
- if(e.altKey && e.key === "F"){ // ALT + SHIFT + F
- e.preventDefault();
- if(window.fixer){
- console.log("Removing fixer");
- fixer.stop();
- fixer.restore();
- window.fixer = null;
- }else{
- console.log("Adding fixer");
- fixer = new FixedWatcher();
- fixer.start();
- window.fixer = fixer;
- }
- }
- });
- let fixer = new FixedWatcher();
- fixer.start();
-
- // Make globally accessible, for debugging purposes
- window.fixer = fixer;
- })()