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/1615478/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      0.9
// @grant        none
// @license      MIT
// ==/UserScript==

/* call this library with createMenu(id, heading, theme, views)
   id ......... config GUI ID chosen for the modal/popup
   heading .... title that appears in the Menu and on the modal/popup
   maxWidth ... for wide screens define the max with of the modal/popup
   views ...... array[] of all [view_id]s that need to be styled (components: window, colorselect)
 */

/* 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") {
            console.debug('webix is already loaded');
            resolve("success");
        }
        else {
            // 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(() => {
                console.debug("library has loaded webix successfully");
                resolve("success");
            })
            .catch((err) => {
                console.debug("library has failed to load webix", err);
                reject("failed");
            });
        }
    });
}

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

    console.debug("initGUI starting");
    // 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.darkmode[view_id="${x}"]`).join(', ');
        console.debug("view_ids:" + views);

        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>`);
    }

    webix.ui({
        view: "window",
        id: id,
        css: "darkmode",
        width: parseInt(getComputedStyle(q("body")).getPropertyValue("width")) * 0.9, // 90% of the screen
        maxWidth: maxWidth, // limit for wide monitors
        position: "top",
        head: heading,
        close: true,
        move: true,
        body: { }
    });
    console.debug("library sees webix object", $$(id));

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

// 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'; }
}