Monorepo for Tangled tangled.org

Dropdown menus fail to appear #4716

Open opened by sin-shadow-fox.tngl.sh

I’ve been running into an issue where dropdown menus on tangled.org either don’t appear or, once opened, won’t reopen correctly.

As a temporary workaround I wrote a small userscript that polyfills a few things and forces the menus to display and reposition. It’s not particularly clean, it’s just the minimum needed to make the UI usable on my end. I’m sharing it in case any of the approach is useful as a reference for how the current behavior is failing.

// ==UserScript== // @name Tangled Compat Polyfills // @namespace http://tampermonkey.net/ // @version 2.6 // @description Working dropdowns that reopen correctly // @author Sin Shadow Fox // @match https://tangled.org/* // @match https://.tangled.org/ // @run-at document-start // @grant none // @license MIT // ==/UserScript== (function() { 'use strict';

// ---- Polyfills ----
if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "undefined") {
    function Segmenter(locale, options) {
        if (!(this instanceof Segmenter)) return new Segmenter(locale, options);
        this.locale = locale;
        this.options = options || {};
    }
    Segmenter.prototype.segment = function(input) {
        const str = String(input);
        return {
            [Symbol.iterator]: function* () {
                for (let i = 0; i < str.length; i++) {
                    yield { segment: str[i], index: i, input: str, isWordLike: true };
                }
            },
            containing(index) { return { segment: str[index] || "", index, input: str }; }
        };
    };
    Segmenter.prototype.resolvedOptions = function() {
        return { locale: this.locale || "en", granularity: this.options.granularity || "grapheme" };
    };
    Intl.Segmenter = Segmenter;
}
if (typeof URL !== "undefined" && typeof URL.parse === "undefined") {
    URL.parse = function(url, base) { try { return new URL(url, base); } catch (e) { return null; } };
}
if (typeof Promise.withResolvers !== "function") {
    Promise.withResolvers = function() {
        let resolve, reject;
        const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
        return { promise, resolve, reject };
    };
}

const OPEN = "data-popover-open";
const MENU_ID = "data-tangled-menu-id";
if (typeof Element !== "undefined" && !Element.prototype.showPopover) {
    Element.prototype.showPopover = function() { this.setAttribute(OPEN, ""); };
    Element.prototype.hidePopover = function() { this.removeAttribute(OPEN); };
    Element.prototype.togglePopover = function() {
        this.hasAttribute(OPEN) ? this.hidePopover() : this.showPopover();
    };
}

function sanitize(sel) { return typeof sel === "string" ? sel.replace(/:popover-open/g, "[" + OPEN + "]") : sel; }
function patch(obj, m) {
    if (!obj || typeof obj[m] !== "function") return;
    const o = obj[m];
    obj[m] = function(s) {
        try { return o.call(this, s); }
        catch (e) {
            if (e && /valid selector/i.test(String(e.message || e))) {
                try { return o.call(this, sanitize(s)); } catch (e2) { return null; }
            }
            throw e;
        }
    };
}
if (typeof Element !== "undefined") {
    ["matches", "querySelector", "querySelectorAll", "closest"].forEach(m => patch(Element.prototype, m));
}
if (typeof Document !== "undefined") {
    patch(Document.prototype, "querySelector");
    patch(Document.prototype, "querySelectorAll");
}

function closeAll() {
    document.querySelectorAll("[" + OPEN + "]").forEach(function(m) {
        m.removeAttribute(OPEN);
        m.style.display = "none";
    });
}

function findMenu(btn) {
    // Previously remembered
    const remembered = btn.getAttribute(MENU_ID);
    if (remembered) {
        const el = document.getElementById(remembered);
        if (el && el.hasAttribute("popover")) return el;
    }
    // popovertarget
    const targetId = btn.getAttribute("popovertarget");
    if (targetId) {
        const el = document.getElementById(targetId);
        if (el && el.hasAttribute("popover")) return el;
    }
    // siblings
    let sib = btn.nextElementSibling;
    if (sib && sib.hasAttribute("popover")) return sib;
    sib = btn.previousElementSibling;
    if (sib && sib.hasAttribute("popover")) return sib;
    // inside parent
    const parent = btn.parentElement;
    if (parent) {
        const inside = parent.querySelector("[popover]");
        if (inside && inside !== btn) return inside;
    }
    return null;
}

function positionMenu(menu, clientX, clientY) {
    if (menu.parentElement !== document.body) {
        document.body.appendChild(menu);
    }
    menu.setAttribute(OPEN, "");
    menu.style.cssText = `
        display: block !important;
        position: fixed !important;
        top: ${clientY + 4}px !important;
        left: ${Math.max(4, clientX - 20)}px !important;
        z-index: 2147483647 !important;
        background: #1f2937 !important;
        color: #f9fafb !important;
        border: 1px solid #374151 !important;
        border-radius: 6px !important;
        padding: 4px !important;
        min-width: 140px !important;
        box-shadow: 0 4px 16px rgba(0,0,0,.4) !important;
        opacity: 1 !important;
        visibility: visible !important;
    `;
}

document.addEventListener("click", function(e) {
    const btn = e.target.closest("button");
    if (!btn) {
        closeAll();
        return;
    }

    if (btn.closest("[" + OPEN + "]")) return;

    const menu = findMenu(btn);
    if (!menu) {
        closeAll();
        return;
    }

    // Remember the association
    if (menu.id && !btn.getAttribute(MENU_ID)) {
        btn.setAttribute(MENU_ID, menu.id);
    }

    closeAll();
    positionMenu(menu, e.clientX, e.clientY);
    e.preventDefault();
    e.stopPropagation();
}, true);

console.log("Tangled polyfills v2.6 loaded");

})();

💭 sidenote maybe i'm still a polyfill wizard as muscle memory from my first job that had to ensure that our app would work on IE7, it was crazy lol

Sign up or Sign in to add to the discussion.