// ==UserScript==
// @name Unfix Fixed Elements
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Intelligently reverses ill-conceived element fixing on sites like Medium.com
// @author alienfucker
// @match *://*/*
// @grant none
// @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 === "X"){
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;
})()