Greasy Fork is available in English.

Unfix Fixed Elements

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

اعتبارا من 2019-05-20 11:03:31 UTC. شاهد أحدث إصدار.

// ==UserScript==
// @name         Unfix Fixed Elements
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Intelligently reverses ill-conceived element fixing on sites like Medium.com
// @author       alienfucker
// @match        *://*/*
// @grant        none
// @noframes
// @run-at       document_start
// ==/UserScript==

(function () {
    'use strict';
    const className = "anti-fixing"; // Odds of colliding with another class must be low

    class FixedWatcher {
        constructor() {
            this.watcher = new MutationObserver(this.onMutation.bind(this));
            this.elementTypes = ["div", "header", "footer", "nav"];
            this.awaitingTick = false;
            this.top = [];
            this.bottom = [];
            this.processedMiddle = false;
        }

        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 (this.elementTypes.findIndex(selector => node.matches(selector)) !== -1) this.track(node);
                        node.querySelectorAll(this.elementTypes.join(",")).forEach(el => this.track(el));
                    }
                } else if (mutation.type === "attributes") {
                    if (this.friendlyMutation(mutation)) continue;


                    if (this.elementTypes.findIndex(selector => mutation.target.matches(selector)) !== -1) {
                        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.elementTypes.join(","));
            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") {
                console.log(el, style.top === "0px", style.top.indexOf("-") === 0, !this.topTracked(el), this.isAutoBottom(el, style));
                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("keypress", e => {
        if(e.key === "F"){
            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;
})()