utils

helper functions, intersection & mutation observers, lazyloader

// ==UserScript==
// @name         utils
// @description  helper functions, intersection & mutation observers, lazyloader
// @namespace    http://tampermonkey.net/
// @author       smartacephale
// @license      MIT
// @version      1.8.3
// @match        *://*/*
// ==/UserScript==

function findNextSibling(el) {
    if (el.nextElementSibling) return el.nextElementSibling;
    if (el.parentElement) return findNextSibling(el.parentElement);
    return null;
}

function parseDOM(html) {
    const parsed = new DOMParser().parseFromString(html, 'text/html').body;
    return parsed.children.length > 1 ? parsed : parsed.firstElementChild;
}

const MOBILE_UA = [
    'Mozilla/5.0 (Linux; Android 10; K)',
    'AppleWebKit/537.36 (KHTML, like Gecko)',
    'Chrome/114.0.0.0 Mobile Safari/537.36'].join(' ');

function fetchWith(url, options = { html: false, mobile: false }) {
    const reqOpts = {};
    if (options.mobile) Object.assign(reqOpts, { headers: new Headers({ "User-Agent": MOBILE_UA }) });
    return fetch(url, reqOpts).then((r) => r.text()).then(r => options.html ? parseDOM(r) : r);
}

const fetchHtml = (url) => fetchWith(url, { html: true });
const fetchText = (url) => fetchWith(url);

function wait(milliseconds) {
    return new Promise(resolve => setTimeout(resolve, milliseconds));
}

// do async one at time
class SyncPull {
    pull = [];
    lock = false;

    getHighPriorityFirst(p = 0) {
        if (p > 3 || this.pull.length === 0) return undefined;
        const i = this.pull.findIndex(e => e.p === p);
        if (i >= 0) {
            const res = this.pull[i].v;
            this.pull = this.pull.slice(0,i).concat(this.pull.slice(i+1));
            return res;
        }
        else return this.getHighPriorityFirst(p+1);
    }

    *pullGenerator() {
        while(this.pull.length > 0) {
            yield this.getHighPriorityFirst();
        }
    }

    async processPull() {
        if (!this.lock) {
            this.lock = true;
            for await (const f of this.pullGenerator()) {
                await f();
            }
            this.lock = false;
        }
    }

    push(x) {
        this.pull.push(x);
        this.processPull();
    }
}

// https://2ality.com/2016/10/asynchronous-iteration.html
async function computeAsyncOneAtTime(iterable) {
    const res = [];
    for await (const f of iterable) {
        res.push(await f());
    }
    return res;
}

function timeToSeconds(t) {
    return (t?.match(/\d+/gm) || [0])
        .reverse()
        .map((s, i) => parseInt(s) * 60 ** i)
        .reduce((a, b) => a + b);
}

function parseIntegerOr(n, or) {
    return Number.isInteger(parseInt(n)) ? parseInt(n) : or;
}

function stringToWords(s) {
    return s.split(",").map(s => s.trim().toLowerCase()).filter(_ => _);
}

function parseCSSUrl(s) { return s.replace(/url\("|\"\).*/g, ''); }

function circularShift(n, c = 6, s = 1) { return (n + s) % c || c; }

function range(size, startAt = 1) {
    return [...Array(size).keys()].map(i => i + startAt);
}

function listenEvents(dom, events, callback) {
    for (const e of events) {
        dom.addEventListener(e, callback, true);
    }
}

class Observer {
    constructor(callback) {
        this.callback = callback;
        this.observer = new IntersectionObserver(this.handleIntersection.bind(this));
    }

    observe(target) {
        this.observer.observe(target);
    }

    throttle(target, throttleTime) {
        this.observer.unobserve(target);
        setTimeout(() => this.observer.observe(target), throttleTime);
    }

    handleIntersection(entries) {
        for (const entry of entries) {
            if (entry.isIntersecting) {
                this.callback(entry.target);
            }
        }
    }

    static observeWhile(target, callback, throttleTime) {
        const observer_ = new Observer(async (target) => {
            const condition = await callback();
            if (condition) observer_.throttle(target, throttleTime);
        });
        observer_.observe(target);
        return observer_;
    }
}

class LazyImgLoader {
    constructor(callback, attributeName = 'data-lazy-load', removeTagAfter = true) {
        this.attributeName = attributeName;
        this.removeTagAfter = removeTagAfter;
        this.lazyImgObserver = new Observer((target) => {
            callback(target, this.delazify);
        });
    }

    lazify(target, img, imgSrc) {
        if (!img || !imgSrc) return;
        img.setAttribute(this.attributeName, imgSrc);
        img.src = '';
        this.lazyImgObserver.observe(img);
    }

    delazify = (target) => {
        this.lazyImgObserver.observer.unobserve(target);
        target.src = target.getAttribute(this.attributeName);
        if (this.removeTagAfter) target.removeAttribute(this.attributeName);
    }

    static create(callback) {
        const lazyImgLoader = new LazyImgLoader((target, delazify) => {
            if (callback(target)) {
                delazify(target);
            }
        });
        return lazyImgLoader;
    }
}

function waitForElementExists(parent, selector, callback) {
    const observer = new MutationObserver((mutations) => {
        const el = parent.querySelector(selector);
        if (el) {
            observer.disconnect();
            callback(el);
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
}

function watchElementChildrenCount(element, callback) {
    let count = element.children.length;
    const observer = new MutationObserver((mutationList, observer) => {
        for (const mutation of mutationList) {
            if (mutation.type === "childList") {
                if (count !== element.children.length) {
                    count = element.children.length;
                    callback(observer, count);
                }
            }
        }
    });
    observer.observe(element, { childList: true });
}

function watchDomChangesWithThrottle(element, callback, throttle = 1000, options = { childList: true, subtree: true, attributes: true }) {
    let lastMutationTime;
    let timeout;
    const observer = new MutationObserver((mutationList, observer) => {
        const now = Date.now();
        if (lastMutationTime && now - lastMutationTime < throttle) {
            timeout && clearTimeout(timeout);
        }
        timeout = setTimeout(callback, throttle);
        lastMutationTime = now;
    });
    observer.observe(element, options);
}

class Tick {
    constructor(delay, startImmediate = true) {
        this.tick = null;
        this.delay = delay;
        this.startImmediate = startImmediate;
    }

    start(callback, callbackFinal = null) {
        this.stop();
        this.callbackFinal = callbackFinal;
        if (this.startImmediate) callback();
        this.tick = setInterval(callback, this.delay);
    }

    stop() {
        if(this.tick !== null) {
            clearInterval(this.tick);
            this.tick = null;
        }
        if (this.callbackFinal) {
            this.callbackFinal();
            this.callbackFinal = null;
        }
    }
}

function copyAttributes(target, source) {
    for (const attr of source.attributes) {
        target.setAttribute(attr.nodeName, attr.nodeValue);
    }
}

function replaceElementTag(e, tagName) {
    const newTagElement = document.createElement(tagName);
    copyAttributes(newTagElement, e);
    newTagElement.innerHTML = e.innerHTML;
    e.parentNode.replaceChild(newTagElement, e);
    return newTagElement;
}

function getAllUniqueParents(elements) {
    return Array.from(elements).reduce((acc, v) => acc.includes(v.parentElement) ? acc : [...acc, v.parentElement], []);
}

function isMob() { return /iPhone|Android/i.test(navigator.userAgent); }

function objectToFormData(object) {
    const formData = new FormData();
    Object.keys(object).forEach(key => formData.append(key, object[key]));
    return formData;
}

// "data:02;body+head:async;void:;zero:;"
function parseDataParams(str) {
    const params = str.split(';').flatMap(s => {
        const parsed = s.match(/([\+\w+]+):(\w+)?/);
        const value = parsed?.[2];
        if (value) return parsed[1].split('+').map(p => ({[p]: value}));
    }).filter(_ => _);
    return Object.assign({}, ...params);
}

function sanitizeStr(str) {
    return str?.replace(/\n|\t/, ' ').replace(/ {2,}/, ' ').trim().toLowerCase() || "";
}

function chunks(arr, n) {
    const res = [];
    for (let i = 0; i < arr.length; i += n) {
        res.push(arr.slice(i, i + n));
    }
    return res;
}

function downloader(options = { append: "", after: "", button: "", cbBefore: () => {} }) {
    const btn = parseDOM(options.button);

    if (options.append) document.querySelector(options.append).append(btn);
    if (options.after) document.querySelector(options.after).after(btn);

    btn.addEventListener('click', (e) => {
        e.preventDefault();

        if (options.cbBefore) options.cbBefore();

        waitForElementExists(document.body, 'video', (video) => {
            window.location.href = video.getAttribute('src');
        });
    });
}