AO3: Initialize webix GUI

library to load the webix JS from CDN only if it hasn't already, and create the menu

Este script no debería instalarse directamente. Es una biblioteca que utilizan otros scripts mediante la meta-directiva de inclusión // @require https://update.greasyfork.org/scripts/541008/1615720/AO3%3A%20Initialize%20webix%20GUI.js

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         AO3: Initialize webix GUI
// @namespace    https://greasyfork.org/en/users/906106-escctrl
// @description  library to load the webix JS from CDN only if it hasn't already, and create the menu
// @author       escctrl
// @version      1.2
// @grant        none
// @license      MIT
// ==/UserScript==

// HOW TO USE:
// please refer to https://greasyfork.org/en/scripts/541008-ao3-initialize-webix-gui for a full description how to use this library

/* global webix, $$ */
'use strict';

// utility to reduce verboseness
const q = (selector, node=document) => node.querySelector(selector);
const qa = (selector, node=document) => node.querySelectorAll(selector);

function createMenu(id, heading) {
    // if no other script has created it yet, write out a "Userscripts" option to the main navigation
    if (qa('#scriptconfig').length === 0) {
        qa('#header nav[aria-label="Site"] li.search')[0] // insert as last li before search
            .insertAdjacentHTML('beforebegin', `<li class="dropdown" id="scriptconfig">
                <a class="dropdown-toggle" href="/" data-toggle="dropdown" data-target="#">Userscripts</a>
                <ul class="menu dropdown-menu"></ul></li>`);
    }

    // then add this script's config option to navigation dropdown
    q('#scriptconfig .dropdown-menu').insertAdjacentHTML('beforeend', `<li><a href="javascript:void(0);" id="opencfg_${id}">${heading}</a></li>`);
}

function loadWebix() {
    return new Promise((resolve, reject) => {
        if (typeof webix !== "undefined") resolve("success");
        else {
            // based on https://stackoverflow.com/a/44807594
            new Promise((resolve, reject) => {
                const script = document.createElement('script');
                document.head.appendChild(script);
                script.onload = resolve;
                script.onerror = reject;
                script.async = true;
                script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/webix.min.js';
            })
            .then(() => resolve("success"))
            .catch((err) => reject(err));
        }
    });
}

async function initGUI(e, id, heading, maxWidth, views=null) {
    let loadSuccess = await loadWebix();
    if (loadSuccess === "success") {

        // setting up the GUI CSS (only if no other script has created it yet)
        if (!q("head link[href*='webix.min.css']")) {
            q("head").insertAdjacentHTML('beforeend',`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/webix.min.css" type="text/css">`);
            q("head").insertAdjacentHTML('beforeend',`<style type="text/css">/* webix stuff that's messed up by AO3 default skins */
                .webix_view {
                    label { margin-right: unset; }
                    button { box-shadow: unset; }
                    legend { display: block; }
                    .webix_template { padding: 0; }
                    &:has(>.webix_template) { border: 0; }
                }</style>`);
        }

        // automatic darkmode if the background is dark (e.g. Reversi)
        let theme = lightOrDark(getComputedStyle(q("body")).getPropertyValue("background-color"));
        if (theme === "dark") {
            if (views === null) views = [id];
            views = views.map((x) => `.webix_view[view_id="${x}"]`).join(', ');

            q("head").insertAdjacentHTML('beforeend',`<style type="text/css">/* switching webix colors to a dark mode if AO3 is dark */
            ${views} {
                --text-on-dark: #ddd;
                --handles-on-dark: #bbb;
                --highlight-on-dark: #0c6a82;
                --background-dark: #222;
                --border-on-dark: #555;
                --no-border: transparent;
                --button-dark: #333;

                background-color: var(--background-dark);
                color: var(--text-on-dark);
                border-color: var(--border-on-dark);

                &.webix_popup, > fieldset { border: 1px solid var(--border-on-dark); }
                .webix_win_head { border-bottom-color: var(--border-on-dark); }
                .webix_icon_button:hover::before { background-color: var(--highlight-on-dark); }

                .webix_view.webix_form, .webix_view.webix_header, .webix_win_body>.webix_view, .webix_view.webix_control.webix_el_tabbar { background-color: var(--background-dark); }
                .webix_secondary .webix_button, .webix_slider_box .webix_slider_right, .webix_el_colorpicker .webix_inp_static, .webix_color_out_text, .webix_switch_box { background-color: var(--button-dark); }
                .webix_primary .webix_button, .webix_slider_box .webix_slider_left, .webix_switch_box.webix_switch_on { background-color: var(--highlight-on-dark); }
                .webix_switch_handle, .webix_slider_box .webix_slider_handle { background-color: var(--handles-on-dark); }
                .webix_el_colorpicker .webix_inp_static, .webix_color_out_block, .webix_color_out_text,
                .webix_switch_handle, .webix_slider_box .webix_slider_handle { border-color: var(--border-on-dark); }
                .webix_switch_box, .webix_slider_box .webix_slider_left, .webix_slider_box .webix_slider_right { border-color: var(--no-border); }
                * { color: var(--text-on-dark); }
            }</style>`);
        }

        let dialogwidth = parseInt(getComputedStyle(q("body")).getPropertyValue("width"));

        webix.ui({
            view: "window",
            id: id,
            width: dialogwidth > maxWidth ? maxWidth : dialogwidth * 0.9,
            position: "top",
            head: heading,
            close: true,
            move: true,
            body: { type:"wide", id:"container", rows:[ ] }
        });

        // event listener for reopening the dialog on subsequent menu clicks (without recreating the whole GUI)
        e.target.addEventListener("click", function(e) { $$(id).show(); });

        // if the window resizes the dialog would move off of the screen
        window.addEventListener('resize', function(e) {
            let dialogwidth = parseInt(getComputedStyle(q("body")).getPropertyValue("width")); // get the new browser width
            $$(id).config.width = dialogwidth > maxWidth ? maxWidth : dialogwidth * 0.9; // reoptimize dialog width
            $$(id).resize(); // repaint the GUI and its components
        });

        return $$(id).queryView((view) => view.config.id === "container");
    }
    else {
        console.debug("library has failed to load webix", loadSuccess);
        return false;
    }
}

// helper function to determine whether a color (the background in use) is light or dark
// https://awik.io/determine-color-bright-dark-using-javascript/
function lightOrDark(color) {
    var r, g, b, hsp;
    if (color.match(/^rgb/)) { color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
        r = color[1]; g = color[2]; b = color[3]; }
    else { color = +("0x" + color.slice(1).replace(color.length < 5 && /./g, '$&$&'));
        r = color >> 16; g = color >> 8 & 255; b = color & 255; }
    hsp = Math.sqrt( 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) );
    if (hsp>127.5) { return 'light'; } else { return 'dark'; }
}