Brazen UI Generator

Helper methods to generate a control panel UI for scripts

Versione datata 04/09/2023. Vedi la nuova versione l'ultima versione.

Questo script non dovrebbe essere installato direttamente. È una libreria per altri script da includere con la chiave // @require https://update.sleazyfork.org/scripts/416104/1245295/Brazen%20UI%20Generator.js

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Brazen UI Generator
// @namespace    brazenvoid
// @version      2.0.2
// @author       brazenvoid
// @license      GPL-3.0-only
// @description  Helper methods to generate a control panel UI for scripts
// @grant        GM_addStyle
// ==/UserScript==

/**
 * @function GM_addStyle
 * @param {string} style
 */
GM_addStyle(`@keyframes brazen-fade{from{opacity:0}to{opacity:1}}#restore-settings.bv-input{margin-bottom:1rem}#settings-wrapper{overflow:auto;resize:horizontal;top:5vh;z-index:1001}.show-settings{top:5vh}.show-settings.bv-section{padding:0.5rem}.bv-actions{display:inline-flex;justify-content:center;padding:0 0.25rem;text-align:center}.bv-actions .bv-button{padding:0.5rem 1rem;width:auto}.bv-bg-colour{background-color:#ffa31a}.bv-border-primary{border:1px solid black}.bv-button{background-color:revert;padding:0 5px;width:100%}.bv-flex-column{flex-direction:column}.bv-font-primary{color:black}.bv-font-secondary{color:white}.bv-group{align-items:start;display:flex;min-height:20px}.bv-group + .bv-group{margin-top:1rem}.bv-group.bv-input{box-sizing:border-box;margin:0;padding:0.5rem}.bv-group.bv-range-group > input{padding:0 5px;width:75px}.bv-group.bv-range-group > input + input{margin-left:5px}.bv-group.bv-textarea-group{flex-direction:column}.bv-group.bv-textarea-group > textarea.bv-input{margin-top:0.5rem;resize:vertical;width:100%}input.bv-input,select.bv-input,textarea.bv-input{box-sizing:border-box;margin:0;padding:0.5rem}.bv-input.bv-checkbox-radio{margin-right:5px;scale:2}.bv-input.bv-text{width:100%}.bv-label{flex-grow:1;text-align:start}.bv-section{display:flex;flex-direction:column;font-family:"roboto";font-size:1rem;font-weight:normal;left:0;padding:1rem;position:fixed;z-index:1000}.bv-section > div + div{margin-top:1rem}.bv-section hr{margin:1rem 0}.bv-section > .bv-title{text-align:center}.bv-section button + button{margin-left:0.25rem}.bv-section .bv-title{display:block;height:20px;width:100%}.bv-show-settings{border:0;font-size:0.7rem;height:90vh;left:0;margin:0;padding:0;position:fixed;top:5vh;width:0.2vw;writing-mode:sideways-lr;z-index:999}.bv-show-settings .bv-title{display:block;height:20px;width:100%}.bv-tab-button{background-color:inherit;border-bottom:0;border-top-left-radius:3px;border-top-right-radius:3px;cursor:pointer;outline:none;padding:0.5rem 0.75rem;transition:0.3s}.bv-tab-button.bv-active,.bv-tab-button:hover{color:black;background-color:white}.bv-tab-panel{animation:brazen-fade 1s;display:none;flex-direction:column;max-height:70vh;overflow:auto;padding:1rem}.bv-tab-panel.bv-active{display:flex}.bv-tabs-nav{display:flex;overflow:hidden}`)

class BrazenUIGenerator
{
    /**
     * @param {JQuery} nodes
     */
    static appendToBody (nodes)
    {
        $('body').append(nodes)
    }

    /**
     * @param {string} selectorPrefix
     */
    constructor (selectorPrefix)
    {
        /**
         * @type {JQuery}
         * @private
         */
        this._section = null

        /**
         * @type {SelectorGenerator}
         * @private
         */
        this._selectorGenerator = new SelectorGenerator(selectorPrefix)

        /**
         * @type {string}
         * @private
         */
        this._selectorPrefix = selectorPrefix

        /**
         * @type {JQuery}
         * @private
         */
        this._statusLine = null

        /**
         * @type {string}
         * @private
         */
        this._statusText = ''
    }

    /**
     * @param {JQuery} node
     * @param {string} text
     * @return {JQuery}
     * @private
     */
    _addHelpTextOnHover (node, text)
    {
        if (text !== '') {
            node.on('mouseover', () => this.updateStatus(text, true))
            node.on('mouseout', () => this.resetStatus())
        }
        return node
    }

    /**
     * @return {JQuery}
     */
    createBreakSeparator ()
    {
        return $('<br/>')
    }

    /**
     * @param {JQuery|JQuery[]} children
     * @param {string} wrapperClasses
     * @return {JQuery}
     */
    createFormActions (children, wrapperClasses = '')
    {
        return $('<div class="bv-actions"/>').addClass(wrapperClasses).append(children)
    }

    /**
     * @param {string} caption
     * @param {JQuery.EventHandler} onClick
     * @param {string} hoverHelp
     * @return {JQuery}
     */
    createFormButton (caption, hoverHelp, onClick)
    {
        let button = $('<button class="bv-button">').text(caption).on('click', onClick)
        return this._addHelpTextOnHover(button, hoverHelp)
    }

    createFormCheckBoxesGroupSection (label, keyValuePairs, hoverHelp)
    {
        let section = this.createFormSection(label)
        for (const element of keyValuePairs) {
            section.append(
                this.createFormGroup().append([
                    this.createFormGroupLabel(element[0], 'checkbox'),
                    this.createFormGroupInput('checkbox').attr('data-value', element[1]),
                ]),
            )
        }
        return this._addHelpTextOnHover(section, hoverHelp)
    }

    /**
     * @return {JQuery}
     */
    createFormGroup ()
    {
        return $('<div class="bv-group"/>')
    }

    /**
     * @param {string} id
     * @param {Array} keyValuePairs
     *
     * @return {JQuery}
     */
    createFormGroupDropdown (id, keyValuePairs)
    {
        let dropdown = $('<select>').attr('id', id).addClass('bv-input')

        for (let i = 0; i < keyValuePairs.length; i++) {
            dropdown.append($('<option>').attr('value', keyValuePairs[i][0]).text(keyValuePairs[i][1]).prop('selected', (i === 0)))
        }
        return dropdown
    }

    /**
     * @param {string} type
     *
     * @return {JQuery}
     */
    createFormGroupInput (type)
    {
        let input = $('<input class="bv-input">').attr('type', type)
        switch (type) {
            case 'number':
            case 'text':
                input.addClass('bv-text')
                break

            case 'radio':
            case 'checkbox':
                input.addClass('bv-checkbox-radio')
                break
        }
        return input
    }

    /**
     * @param {string} label
     * @param {string} inputType
     * @return {JQuery}
     */
    createFormGroupLabel (label, inputType = '')
    {
        let labelFormGroup = $('<label class="bv-label">').text(label)
        if (inputType !== '') {
            switch (inputType) {
                case 'number':
                case 'text':
                    labelFormGroup.addClass('bv-text')
                    labelFormGroup.text(labelFormGroup.text() + ': ')
                    break
                case 'radio':
                case 'checkbox':
                    labelFormGroup.addClass('bv-checkbox-radio')
                    break
            }
        }
        return labelFormGroup
    }

    /**
     * @param {string} statisticType
     * @return {JQuery}
     */
    createFormGroupStatLabel (statisticType)
    {
        return $('<label class="bv-stat-label">').
            attr('id', this._selectorGenerator.getStatLabelSelector(statisticType)).
            text('0')
    }

    /**
     * @param {string} label
     * @param {string} inputType
     * @param {string} hoverHelp
     * @return {JQuery}
     */
    createFormInputGroup (label, inputType, hoverHelp = '')
    {
        return this._addHelpTextOnHover(
            this.createFormGroup().append([
                this.createFormGroupLabel(label, inputType),
                this.createFormGroupInput(inputType),
            ]),
            hoverHelp,
        )
    }

    createFormRadiosGroupSection (label, keyValuePairs, hoverHelp)
    {
        let section = this.createFormSection(label)
        for (let i = 0; i < keyValuePairs.length; i++) {
            section.append(
                this.createFormGroup().append([
                    this.createFormGroupLabel(keyValuePairs[i][0], 'radio'),
                    this.createFormGroupInput('radio').prop('checked', i === 0).attr('data-value', keyValuePairs[i][1]).on('change', (event) => {
                        $(event.currentTarget).parents('.bv-section').first().find('input').each((index, element) => {
                            if(!element.isSameNode(event.currentTarget)) {
                                $(element).prop('checked', false)
                            }
                        })
                    }),
                ]),
            )
        }
        return this._addHelpTextOnHover(section, hoverHelp)
    }

    /**
     * @param {string} label
     * @param {string} inputsType
     * @param {number} minimum
     * @param {number} maximum
     * @param {string} hoverHelp
     * @return {JQuery}
     */
    createFormRangeInputGroup (label, inputsType, minimum, maximum, hoverHelp)
    {
        return this._addHelpTextOnHover(
            this.createFormGroup().addClass('bv-range-group').append([
                this.createFormGroupLabel(label, inputsType),
                this.createFormGroupInput(inputsType).attr('min', minimum).attr('max', maximum),
                this.createFormGroupInput(inputsType).attr('min', minimum).attr('max', maximum),
            ]),
            hoverHelp,
        )
    }

    /**
     * @param {string} title
     * @return {JQuery}
     */
    createFormSection (title = '')
    {
        let sectionDiv = $('<div class="bv-section">')
        if (title !== '') {
            sectionDiv.append($('<label class="bv-title">').text(title))
        }
        return sectionDiv
    }

    /**
     * @param {string} label
     * @param {int} rows
     * @param {string} hoverHelp
     * @return {JQuery}
     */
    createFormTextAreaGroup (label, rows, hoverHelp = '')
    {
        return this._addHelpTextOnHover(
            this.createFormGroup().addClass('bv-textarea-group').append([
                this.createFormGroupLabel(label),
                $('<textarea class="bv-input">').attr('rows', rows),
            ]),
            hoverHelp,
        )
    }

    /**
     * @return {JQuery}
     */
    createSection ()
    {
        this._section = $('<section class="bv-section bv-font-primary">')
        return this._section
    }

    /**
     * @return {JQuery}
     */
    createSeparator ()
    {
        return $('<hr/>')
    }

    /**
     * @return {JQuery}
     */
    createSettingsHideButton ()
    {
        return this.createFormButton('<< Hide', '', () => this._section.css('display', 'none'))
    }

    /**
     * @return {JQuery}
     */
    createSettingsSection ()
    {
        return this.createSection()
            .attr('id', 'settings-wrapper')
            .addClass('bv-bg-colour bv-border-primary')
            .hide()
    }

    /**
     * @param {string} caption
     * @param {JQuery} settingsSection
     *
     * @return {JQuery}
     */
    createSettingsShowButton (caption, settingsSection)
    {
        return $('<button class="show-settings bv-section bv-bg-colour">')
            .text(caption)
            .on('click', () => settingsSection.toggle(300))
    }

    /**
     * @param {string} statisticsType
     * @param {string} label
     * @return {JQuery}
     */
    createStatisticsFormGroup (statisticsType, label = '')
    {
        return this.createFormGroup().addClass('bv-stat-group').append([
            this.createFormGroupLabel((label === '' ? statisticsType : label) + ' Filter'),
            this.createFormGroupStatLabel(statisticsType),
        ])
    }

    /**
     * @return {JQuery}
     */
    createStatisticsTotalsGroup ()
    {
        return this.createFormGroup().append([
            this.createFormGroupLabel('Total'),
            this.createFormGroupStatLabel('Total'),
        ])
    }

    /**
     * @return {JQuery}
     */
    createStatusSection ()
    {
        this._statusLine = this.createFormGroupLabel('Status').attr('id', this._selectorGenerator.getSelector('status'))
        return this._statusLine
    }

    /**
     * @param {string} tabName
     * @param {boolean} isFirst
     * @return {JQuery}
     */
    createTabButton (tabName, isFirst)
    {
        let tabButton = $('<button class="bv-tab-button bv-border-primary">')
            .text(tabName)
            .on('click', (event) => {
                
                let button = $(event.currentTarget)
                let tabSection = button.parents('.bv-tabs-section:first')
                
                tabSection.find('.bv-tab-button')
                    .removeClass('bv-active bv-font-secondary')
                    .addClass('bv-font-primary')
                
                tabSection.find('.bv-tab-panel').removeClass('bv-active')
            
                button.removeClass('bv-font-primary').addClass('bv-active bv-font-secondary')
            
                $('#' + Utilities.toKebabCase(button.text())).addClass('bv-active')
            })
            .on('mouseenter', (event) => $(event.currentTarget).addClass('bv-font-secondary'))
            .on('mouseleave', (event) => $(event.currentTarget).removeClass('bv-font-secondary'))

        return isFirst ? tabButton.addClass('bv-active bv-font-secondary') : tabButton.addClass('bv-font-primary')
    }

    /**
     * @param {string} tabName
     * @param {boolean} isFirst
     * @return {JQuery}
     */
    createTabPanel (tabName, isFirst = false)
    {
        let tabPanel = $('<div class="bv-tab-panel bv-border-primary">').attr('id', Utilities.toKebabCase(tabName))
        if (isFirst) {
            tabPanel.addClass('bv-active')
        }
        return tabPanel
    }

    /**
     * @param {string[]} tabNames
     * @param {JQuery[]} tabPanels
     * @return {JQuery}
     */
    createTabsSection (tabNames, tabPanels)
    {
        let tabButtons = []
        for (let i = 0; i < tabNames.length; i++) {
            tabButtons.push(this.createTabButton(tabNames[i], i === 0))
        }
        let nav = $('<div class="bv-tabs-nav">').append(tabButtons)
        return $('<div class="bv-tabs-section">').append(nav).append(...tabPanels)
    }

    /**
     * @return {JQuery}
     */
    getSelectedSection ()
    {
        return this._section
    }

    resetStatus ()
    {
        this._statusLine.text(this._statusText)
    }

    /**
     * @param {string} status
     * @param {boolean} transient
     */
    updateStatus (status, transient = false)
    {
        if (!transient) {
            this._statusText = status
        }
        this._statusLine.text(status)
    }
}