AO3: Initialize webix GUI

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

От 28.06.2025. Виж последната версия.

Този скрипт не може да бъде инсталиран директно. Това е библиотека за други скриптове и може да бъде използвана с мета-директива // @require https://update.greasyfork.org/scripts/541008/1615671/AO3%3A%20Initialize%20webix%20GUI.js

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==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.0
// @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; }
                }</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 { 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 { 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'; }
}