AO3: Initialize webix GUI

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

目前為 2025-06-28 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/541008/1615500/AO3%3A%20Initialize%20webix%20GUI.js

// ==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.13
// @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>`);
    }

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

    webix.ui({
        view: "window",
        id: id,
        css: "darkmode",
        width: dialogwidth > maxWidth ? maxWidth : dialogwidth * 0.9,
        position: "top",
        head: heading,
        close: true,
        move: true,
        body: { type:"wide", id:"container", rows:[ ] }
    });
    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'; }
}